Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/presenters/packages')
-rw-r--r--app/presenters/packages/composer/packages_presenter.rb71
-rw-r--r--app/presenters/packages/conan/package_presenter.rb114
-rw-r--r--app/presenters/packages/detail/package_presenter.rb75
-rw-r--r--app/presenters/packages/go/module_version_presenter.rb19
-rw-r--r--app/presenters/packages/npm/package_presenter.rb87
-rw-r--r--app/presenters/packages/nuget/package_metadata_presenter.rb25
-rw-r--r--app/presenters/packages/nuget/packages_metadata_presenter.rb63
-rw-r--r--app/presenters/packages/nuget/packages_versions_presenter.rb15
-rw-r--r--app/presenters/packages/nuget/presenter_helpers.rb113
-rw-r--r--app/presenters/packages/nuget/search_results_presenter.rb56
-rw-r--r--app/presenters/packages/nuget/service_index_presenter.rb85
-rw-r--r--app/presenters/packages/pypi/package_presenter.rb75
12 files changed, 798 insertions, 0 deletions
diff --git a/app/presenters/packages/composer/packages_presenter.rb b/app/presenters/packages/composer/packages_presenter.rb
new file mode 100644
index 00000000000..84f266989e9
--- /dev/null
+++ b/app/presenters/packages/composer/packages_presenter.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module Packages
+ module Composer
+ class PackagesPresenter
+ include API::Helpers::RelatedResourcesHelpers
+
+ def initialize(group, packages)
+ @group = group
+ @packages = packages
+ end
+
+ def root
+ path = api_v4_group___packages_composer_package_name_path({ id: @group.id, package_name: '%package%', format: '.json' }, true)
+ { 'packages' => [], 'provider-includes' => { 'p/%hash%.json' => { 'sha256' => provider_sha } }, 'providers-url' => path }
+ end
+
+ def provider
+ { 'providers' => providers_map }
+ end
+
+ def package_versions(packages = @packages)
+ { 'packages' => { packages.first.name => package_versions_map(packages) } }
+ end
+
+ private
+
+ def package_versions_map(packages)
+ packages.each_with_object({}) do |package, map|
+ map[package.version] = package_metadata(package)
+ end
+ end
+
+ def package_metadata(package)
+ json = package.composer_metadatum.composer_json
+
+ json.merge('dist' => package_dist(package), 'uid' => package.id, 'version' => package.version)
+ end
+
+ def package_dist(package)
+ sha = package.composer_metadatum.target_sha
+ archive_api_path = api_v4_projects_packages_composer_archives_package_name_path({ id: package.project_id, package_name: package.name, format: '.zip' }, true)
+
+ {
+ 'type' => 'zip',
+ 'url' => expose_url(archive_api_path) + "?sha=#{sha}",
+ 'reference' => sha,
+ 'shasum' => ''
+ }
+ end
+
+ def providers_map
+ map = {}
+
+ @packages.group_by(&:name).each_pair do |package_name, packages|
+ map[package_name] = { 'sha256' => package_versions_sha(packages) }
+ end
+
+ map
+ end
+
+ def package_versions_sha(packages)
+ Digest::SHA256.hexdigest(package_versions(packages).to_json)
+ end
+
+ def provider_sha
+ Digest::SHA256.hexdigest(provider.to_json)
+ end
+ end
+ end
+end
diff --git a/app/presenters/packages/conan/package_presenter.rb b/app/presenters/packages/conan/package_presenter.rb
new file mode 100644
index 00000000000..5141c450412
--- /dev/null
+++ b/app/presenters/packages/conan/package_presenter.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+module Packages
+ module Conan
+ class PackagePresenter
+ include API::Helpers::RelatedResourcesHelpers
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :params
+
+ def initialize(recipe, user, project, params = {})
+ @recipe = recipe
+ @user = user
+ @project = project
+ @params = params
+ end
+
+ def recipe_urls
+ map_package_files do |package_file|
+ build_recipe_file_url(package_file) if package_file.conan_file_metadatum.recipe_file?
+ end
+ end
+
+ def recipe_snapshot
+ map_package_files do |package_file|
+ package_file.file_md5 if package_file.conan_file_metadatum.recipe_file?
+ end
+ end
+
+ def package_urls
+ map_package_files do |package_file|
+ next unless package_file.conan_file_metadatum.package_file? && matching_reference?(package_file)
+
+ build_package_file_url(package_file)
+ end
+ end
+
+ def package_snapshot
+ map_package_files do |package_file|
+ next unless package_file.conan_file_metadatum.package_file? && matching_reference?(package_file)
+
+ package_file.file_md5
+ end
+ end
+
+ private
+
+ def build_recipe_file_url(package_file)
+ expose_url(
+ api_v4_packages_conan_v1_files_export_path(
+ package_name: package.name,
+ package_version: package.version,
+ package_username: package.conan_metadatum.package_username,
+ package_channel: package.conan_metadatum.package_channel,
+ recipe_revision: package_file.conan_file_metadatum.recipe_revision,
+ file_name: package_file.file_name
+ )
+ )
+ end
+
+ def build_package_file_url(package_file)
+ expose_url(
+ api_v4_packages_conan_v1_files_package_path(
+ package_name: package.name,
+ package_version: package.version,
+ package_username: package.conan_metadatum.package_username,
+ package_channel: package.conan_metadatum.package_channel,
+ recipe_revision: package_file.conan_file_metadatum.recipe_revision,
+ conan_package_reference: package_file.conan_file_metadatum.conan_package_reference,
+ package_revision: package_file.conan_file_metadatum.package_revision,
+ file_name: package_file.file_name
+ )
+ )
+ end
+
+ def map_package_files
+ package_files.to_a.map do |package_file|
+ key = package_file.file_name
+ value = yield(package_file)
+ next unless key && value
+
+ [key, value]
+ end.compact.to_h
+ end
+
+ def package_files
+ return unless package
+
+ @package_files ||= package.package_files.preload_conan_file_metadata
+ end
+
+ def package
+ strong_memoize(:package) do
+ name, version = @recipe.split('@')[0].split('/')
+
+ @project.packages
+ .conan
+ .with_name(name)
+ .with_version(version)
+ .order_created
+ .last
+ end
+ end
+
+ def matching_reference?(package_file)
+ package_file.conan_file_metadatum.conan_package_reference == conan_package_reference
+ end
+
+ def conan_package_reference
+ params[:conan_package_reference]
+ end
+ end
+ end
+end
diff --git a/app/presenters/packages/detail/package_presenter.rb b/app/presenters/packages/detail/package_presenter.rb
new file mode 100644
index 00000000000..f6e068302c1
--- /dev/null
+++ b/app/presenters/packages/detail/package_presenter.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+module Packages
+ module Detail
+ class PackagePresenter
+ def initialize(package)
+ @package = package
+ end
+
+ def detail_view
+ package_detail = {
+ id: @package.id,
+ created_at: @package.created_at,
+ name: @package.name,
+ package_files: @package.package_files.map { |pf| build_package_file_view(pf) },
+ package_type: @package.package_type,
+ project_id: @package.project_id,
+ tags: @package.tags.as_json,
+ updated_at: @package.updated_at,
+ version: @package.version
+ }
+
+ package_detail[:maven_metadatum] = @package.maven_metadatum if @package.maven_metadatum
+ package_detail[:nuget_metadatum] = @package.nuget_metadatum if @package.nuget_metadatum
+ package_detail[:dependency_links] = @package.dependency_links.map(&method(:build_dependency_links))
+ package_detail[:pipeline] = build_pipeline_info(@package.build_info.pipeline) if @package.build_info
+
+ package_detail
+ end
+
+ private
+
+ def build_package_file_view(package_file)
+ {
+ created_at: package_file.created_at,
+ download_path: package_file.download_path,
+ file_name: package_file.file_name,
+ size: package_file.size
+ }
+ end
+
+ def build_pipeline_info(pipeline_info)
+ {
+ created_at: pipeline_info.created_at,
+ id: pipeline_info.id,
+ sha: pipeline_info.sha,
+ ref: pipeline_info.ref,
+ git_commit_message: pipeline_info.git_commit_message,
+ user: build_user_info(pipeline_info.user),
+ project: {
+ name: pipeline_info.project.name,
+ web_url: pipeline_info.project.web_url
+ }
+ }
+ end
+
+ def build_user_info(user)
+ return unless user
+
+ {
+ avatar_url: user.avatar_url,
+ name: user.name
+ }
+ end
+
+ def build_dependency_links(link)
+ {
+ name: link.dependency.name,
+ version_pattern: link.dependency.version_pattern,
+ target_framework: link.nuget_metadatum&.target_framework
+ }.compact
+ end
+ end
+ end
+end
diff --git a/app/presenters/packages/go/module_version_presenter.rb b/app/presenters/packages/go/module_version_presenter.rb
new file mode 100644
index 00000000000..4c86eae46cd
--- /dev/null
+++ b/app/presenters/packages/go/module_version_presenter.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Packages
+ module Go
+ class ModuleVersionPresenter
+ def initialize(version)
+ @version = version
+ end
+
+ def name
+ @version.name
+ end
+
+ def time
+ @version.commit.committed_date
+ end
+ end
+ end
+end
diff --git a/app/presenters/packages/npm/package_presenter.rb b/app/presenters/packages/npm/package_presenter.rb
new file mode 100644
index 00000000000..a3ab10d3913
--- /dev/null
+++ b/app/presenters/packages/npm/package_presenter.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+module Packages
+ module Npm
+ class PackagePresenter
+ include API::Helpers::RelatedResourcesHelpers
+
+ attr_reader :name, :packages
+
+ NPM_VALID_DEPENDENCY_TYPES = %i[dependencies devDependencies bundleDependencies peerDependencies].freeze
+
+ def initialize(name, packages)
+ @name = name
+ @packages = packages
+ end
+
+ def versions
+ package_versions = {}
+
+ packages.each do |package|
+ package_file = package.package_files.last
+
+ next unless package_file
+
+ package_versions[package.version] = build_package_version(package, package_file)
+ end
+
+ package_versions
+ end
+
+ def dist_tags
+ build_package_tags.tap { |t| t["latest"] ||= sorted_versions.last }
+ end
+
+ private
+
+ def build_package_tags
+ Hash[
+ package_tags.map { |tag| [tag.name, tag.package.version] }
+ ]
+ end
+
+ def build_package_version(package, package_file)
+ {
+ name: package.name,
+ version: package.version,
+ dist: {
+ shasum: package_file.file_sha1,
+ tarball: tarball_url(package, package_file)
+ }
+ }.tap do |package_version|
+ package_version.merge!(build_package_dependencies(package))
+ end
+ end
+
+ def tarball_url(package, package_file)
+ expose_url "#{api_v4_projects_path(id: package.project_id)}" \
+ "/packages/npm/#{package.name}" \
+ "/-/#{package_file.file_name}"
+ end
+
+ def build_package_dependencies(package)
+ dependencies = Hash.new { |h, key| h[key] = {} }
+ dependency_links = package.dependency_links
+ .with_dependency_type(NPM_VALID_DEPENDENCY_TYPES)
+ .includes_dependency
+
+ dependency_links.find_each do |dependency_link|
+ dependency = dependency_link.dependency
+ dependencies[dependency_link.dependency_type][dependency.name] = dependency.version_pattern
+ end
+
+ dependencies
+ end
+
+ def sorted_versions
+ versions = packages.map(&:version).compact
+ VersionSorter.sort(versions)
+ end
+
+ def package_tags
+ Packages::Tag.for_packages(packages)
+ .preload_package
+ end
+ end
+ end
+end
diff --git a/app/presenters/packages/nuget/package_metadata_presenter.rb b/app/presenters/packages/nuget/package_metadata_presenter.rb
new file mode 100644
index 00000000000..500fc982e11
--- /dev/null
+++ b/app/presenters/packages/nuget/package_metadata_presenter.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Packages
+ module Nuget
+ class PackageMetadataPresenter
+ include Packages::Nuget::PresenterHelpers
+
+ def initialize(package)
+ @package = package
+ end
+
+ def json_url
+ json_url_for(@package)
+ end
+
+ def archive_url
+ archive_url_for(@package)
+ end
+
+ def catalog_entry
+ catalog_entry_for(@package)
+ end
+ end
+ end
+end
diff --git a/app/presenters/packages/nuget/packages_metadata_presenter.rb b/app/presenters/packages/nuget/packages_metadata_presenter.rb
new file mode 100644
index 00000000000..5f22d5dd8a1
--- /dev/null
+++ b/app/presenters/packages/nuget/packages_metadata_presenter.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module Packages
+ module Nuget
+ class PackagesMetadataPresenter
+ include Packages::Nuget::PresenterHelpers
+ include Gitlab::Utils::StrongMemoize
+
+ COUNT = 1.freeze
+
+ def initialize(packages)
+ @packages = packages
+ end
+
+ def count
+ COUNT
+ end
+
+ def items
+ [summary]
+ end
+
+ private
+
+ def summary
+ {
+ json_url: json_url,
+ lower_version: lower_version,
+ upper_version: upper_version,
+ packages_count: @packages.count,
+ packages: @packages.map { |pkg| metadata_for(pkg) }
+ }
+ end
+
+ def metadata_for(package)
+ {
+ json_url: json_url_for(package),
+ archive_url: archive_url_for(package),
+ catalog_entry: catalog_entry_for(package)
+ }
+ end
+
+ def json_url
+ json_url_for(@packages.first)
+ end
+
+ def lower_version
+ sorted_versions.first
+ end
+
+ def upper_version
+ sorted_versions.last
+ end
+
+ def sorted_versions
+ strong_memoize(:sorted_versions) do
+ versions = @packages.map(&:version).compact
+ VersionSorter.sort(versions)
+ end
+ end
+ end
+ end
+end
diff --git a/app/presenters/packages/nuget/packages_versions_presenter.rb b/app/presenters/packages/nuget/packages_versions_presenter.rb
new file mode 100644
index 00000000000..7f4ce4dbb2f
--- /dev/null
+++ b/app/presenters/packages/nuget/packages_versions_presenter.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Packages
+ module Nuget
+ class PackagesVersionsPresenter
+ def initialize(packages)
+ @packages = packages
+ end
+
+ def versions
+ @packages.pluck_versions.sort
+ end
+ end
+ end
+end
diff --git a/app/presenters/packages/nuget/presenter_helpers.rb b/app/presenters/packages/nuget/presenter_helpers.rb
new file mode 100644
index 00000000000..cc7e8619220
--- /dev/null
+++ b/app/presenters/packages/nuget/presenter_helpers.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+module Packages
+ module Nuget
+ module PresenterHelpers
+ include ::API::Helpers::RelatedResourcesHelpers
+
+ BLANK_STRING = ''
+ PACKAGE_DEPENDENCY_GROUP = 'PackageDependencyGroup'
+ PACKAGE_DEPENDENCY = 'PackageDependency'
+
+ private
+
+ def json_url_for(package)
+ path = api_v4_projects_packages_nuget_metadata_package_name_package_version_path(
+ {
+ id: package.project_id,
+ package_name: package.name,
+ package_version: package.version,
+ format: '.json'
+ },
+ true
+ )
+
+ expose_url(path)
+ end
+
+ def archive_url_for(package)
+ path = api_v4_projects_packages_nuget_download_package_name_package_version_package_filename_path(
+ {
+ id: package.project_id,
+ package_name: package.name,
+ package_version: package.version,
+ package_filename: package.package_files.last&.file_name
+ },
+ true
+ )
+
+ expose_url(path)
+ end
+
+ def catalog_entry_for(package)
+ {
+ json_url: json_url_for(package),
+ authors: BLANK_STRING,
+ dependency_groups: dependency_groups_for(package),
+ package_name: package.name,
+ package_version: package.version,
+ archive_url: archive_url_for(package),
+ summary: BLANK_STRING,
+ tags: tags_for(package),
+ metadatum: metadatum_for(package)
+ }
+ end
+
+ def dependency_groups_for(package)
+ base_nuget_id = "#{json_url_for(package)}#dependencyGroup"
+
+ dependency_links_grouped_by_target_framework(package).map do |target_framework, dependency_links|
+ nuget_id = target_framework_nuget_id(base_nuget_id, target_framework)
+ {
+ id: nuget_id,
+ type: PACKAGE_DEPENDENCY_GROUP,
+ target_framework: target_framework,
+ dependencies: dependencies_for(nuget_id, dependency_links)
+ }.compact
+ end
+ end
+
+ def dependency_links_grouped_by_target_framework(package)
+ package
+ .dependency_links
+ .includes_dependency
+ .preload_nuget_metadatum
+ .group_by { |dependency_link| dependency_link.nuget_metadatum&.target_framework }
+ end
+
+ def dependencies_for(nuget_id, dependency_links)
+ return [] if dependency_links.empty?
+
+ dependency_links.map do |dependency_link|
+ dependency = dependency_link.dependency
+ {
+ id: "#{nuget_id}/#{dependency.name.downcase}",
+ type: PACKAGE_DEPENDENCY,
+ name: dependency.name,
+ range: dependency.version_pattern
+ }
+ end
+ end
+
+ def target_framework_nuget_id(base_nuget_id, target_framework)
+ target_framework.blank? ? base_nuget_id : "#{base_nuget_id}/#{target_framework.downcase}"
+ end
+
+ def metadatum_for(package)
+ metadatum = package.nuget_metadatum
+ return {} unless metadatum
+
+ metadatum.slice(:project_url, :license_url, :icon_url)
+ .compact
+ end
+
+ def base_path_for(package)
+ api_v4_projects_packages_nuget_path(id: package.project_id)
+ end
+
+ def tags_for(package)
+ package.tag_names.join(::Packages::Tag::NUGET_TAGS_SEPARATOR)
+ end
+ end
+ end
+end
diff --git a/app/presenters/packages/nuget/search_results_presenter.rb b/app/presenters/packages/nuget/search_results_presenter.rb
new file mode 100644
index 00000000000..96c8fe7dd2a
--- /dev/null
+++ b/app/presenters/packages/nuget/search_results_presenter.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+module Packages
+ module Nuget
+ class SearchResultsPresenter
+ include Packages::Nuget::PresenterHelpers
+ include Gitlab::Utils::StrongMemoize
+
+ delegate :total_count, to: :@search
+
+ def initialize(search)
+ @search = search
+ @package_versions = {}
+ end
+
+ def data
+ strong_memoize(:data) do
+ @search.results.group_by(&:name).map do |package_name, packages|
+ latest_version = latest_version(packages)
+ latest_package = packages.find { |pkg| pkg.version == latest_version }
+
+ {
+ type: 'Package',
+ authors: '',
+ name: package_name,
+ version: latest_version,
+ versions: build_package_versions(packages),
+ summary: '',
+ total_downloads: 0,
+ verified: true,
+ tags: tags_for(latest_package),
+ metadatum: metadatum_for(latest_package)
+ }
+ end
+ end
+ end
+
+ private
+
+ def build_package_versions(packages)
+ packages.map do |pkg|
+ {
+ json_url: json_url_for(pkg),
+ downloads: 0,
+ version: pkg.version
+ }
+ end
+ end
+
+ def latest_version(packages)
+ versions = packages.map(&:version).compact
+ VersionSorter.sort(versions).last # rubocop: disable Style/UnneededSort
+ end
+ end
+ end
+end
diff --git a/app/presenters/packages/nuget/service_index_presenter.rb b/app/presenters/packages/nuget/service_index_presenter.rb
new file mode 100644
index 00000000000..ed00b36b362
--- /dev/null
+++ b/app/presenters/packages/nuget/service_index_presenter.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+module Packages
+ module Nuget
+ class ServiceIndexPresenter
+ include API::Helpers::RelatedResourcesHelpers
+
+ SERVICE_VERSIONS = {
+ download: %w[PackageBaseAddress/3.0.0],
+ search: %w[SearchQueryService SearchQueryService/3.0.0-beta SearchQueryService/3.0.0-rc],
+ publish: %w[PackagePublish/2.0.0],
+ metadata: %w[RegistrationsBaseUrl RegistrationsBaseUrl/3.0.0-beta RegistrationsBaseUrl/3.0.0-rc]
+ }.freeze
+
+ SERVICE_COMMENTS = {
+ download: 'Get package content (.nupkg).',
+ search: 'Filter and search for packages by keyword.',
+ publish: 'Push and delete (or unlist) packages.',
+ metadata: 'Get package metadata.'
+ }.freeze
+
+ VERSION = '3.0.0'.freeze
+
+ def initialize(project)
+ @project = project
+ end
+
+ def version
+ VERSION
+ end
+
+ def resources
+ [
+ build_service(:download),
+ build_service(:search),
+ build_service(:publish),
+ build_service(:metadata)
+ ].flatten
+ end
+
+ private
+
+ def build_service(service_type)
+ url = build_service_url(service_type)
+ comment = SERVICE_COMMENTS[service_type]
+
+ SERVICE_VERSIONS[service_type].map do |version|
+ { :@id => url, :@type => version, :comment => comment }
+ end
+ end
+
+ def build_service_url(service_type)
+ base_path = api_v4_projects_packages_nuget_path(id: @project.id)
+
+ full_path = case service_type
+ when :download
+ api_v4_projects_packages_nuget_download_package_name_package_version_package_filename_path(
+ {
+ id: @project.id,
+ package_name: nil,
+ package_version: nil,
+ package_filename: nil
+ },
+ true
+ )
+ when :search
+ "#{base_path}/query"
+ when :metadata
+ api_v4_projects_packages_nuget_metadata_package_name_package_version_path(
+ {
+ id: @project.id,
+ package_name: nil,
+ package_version: nil
+ },
+ true
+ )
+ when :publish
+ base_path
+ end
+
+ expose_url(full_path)
+ end
+ end
+ end
+end
diff --git a/app/presenters/packages/pypi/package_presenter.rb b/app/presenters/packages/pypi/package_presenter.rb
new file mode 100644
index 00000000000..4192e974645
--- /dev/null
+++ b/app/presenters/packages/pypi/package_presenter.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+# Display package version data acording to PyPi
+# Simple API: https://warehouse.pypa.io/api-reference/legacy/#simple-project-api
+module Packages
+ module Pypi
+ class PackagePresenter
+ include API::Helpers::RelatedResourcesHelpers
+
+ def initialize(packages, project)
+ @packages = packages
+ @project = project
+ end
+
+ # Returns the HTML body for PyPi simple API.
+ # Basically a list of package download links for a specific
+ # package
+ def body
+ <<-HTML
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>Links for #{escape(name)}</title>
+ </head>
+ <body>
+ <h1>Links for #{escape(name)}</h1>
+ #{links}
+ </body>
+ </html>
+ HTML
+ end
+
+ private
+
+ def links
+ refs = []
+
+ @packages.map do |package|
+ package.package_files.each do |file|
+ url = build_pypi_package_path(file)
+
+ refs << package_link(url, package.pypi_metadatum.required_python, file.file_name)
+ end
+ end
+
+ refs.join
+ end
+
+ def package_link(url, required_python, filename)
+ "<a href=\"#{url}\" data-requires-python=\"#{escape(required_python)}\">#{filename}</a><br>"
+ end
+
+ def build_pypi_package_path(file)
+ expose_url(
+ api_v4_projects_packages_pypi_files_file_identifier_path(
+ {
+ id: @project.id,
+ sha256: file.file_sha256,
+ file_identifier: file.file_name
+ },
+ true
+ )
+ ) + "#sha256=#{file.file_sha256}"
+ end
+
+ def name
+ @packages.first.name
+ end
+
+ def escape(str)
+ ERB::Util.html_escape(str)
+ end
+ end
+ end
+end