geo_combine 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f7557bdb34860f4f6a38081e658c51ebec60d804
4
- data.tar.gz: 8d1fddf86f46fa134c5070a4c813cfdf362d1ea7
3
+ metadata.gz: 0370f82160756d74fe36f4c4a5e9639b8e06d461
4
+ data.tar.gz: e06d158e0770862c7033935c91c82bfe6b9d9f21
5
5
  SHA512:
6
- metadata.gz: eaa908a3ff9e0d181236bab6a1cb3af8464e6d8912e10dc1fe6fcbb56d787ac59356777965469a8d773714566180e1657354199050b033902f2524bfb310890f
7
- data.tar.gz: 5898290a91a655751dbd0bf23f5230075eeadb87546338f84c49a16c8a2fb2dd1f40bfa505a992fbb208870839ba34213e41c18da20daca98e78d8b22e5be881
6
+ metadata.gz: f5072b677855334cbe0716a3639394ead6c10bce24cdec3485daf5764d36df1466938247075cbf5737070a7b08fb5f1926aa618ac0c9679783c9edf8e5ea7b86
7
+ data.tar.gz: 26098a941f59b4c36fa0c4f8331d2c31d8575c2978e4bcba1804a9711b98a22e34d78e16dfa1a5d04da4a130f887bb91ff5bc4f0de794bb2dc2e07ffa72b93d2
data/.travis.yml CHANGED
@@ -2,6 +2,6 @@ sudo: false
2
2
  language: ruby
3
3
  cache: bundler
4
4
  rvm:
5
- - 2.1.10
6
- - 2.2.5
7
- - 2.3.1
5
+ - 2.2.7
6
+ - 2.3.4
7
+ - 2.4.1
data/Gemfile CHANGED
@@ -3,4 +3,5 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in geo_combine.gemspec
4
4
  gemspec
5
5
 
6
- gem 'coveralls', require: false
6
+ gem 'coveralls', require: false
7
+ gem 'byebug'
data/README.md CHANGED
@@ -3,12 +3,13 @@
3
3
  [![Build Status](https://travis-ci.org/OpenGeoMetadata/GeoCombine.svg?branch=master)](https://travis-ci.org/OpenGeoMetadata/GeoCombine) | [![Coverage Status](https://coveralls.io/repos/OpenGeoMetadata/GeoCombine/badge.svg?branch=master)](https://coveralls.io/r/OpenGeoMetadata/GeoCombine?branch=master)
4
4
 
5
5
 
6
-
7
- A Ruby toolkit for managing geospatial metadata
6
+ A Ruby toolkit for managing geospatial metadata, including:
7
+ - tasks for cloning, updating, and indexing OpenGeoMetdata metadata
8
+ - library for converting metadata between standards
8
9
 
9
10
  ## Installation
10
11
 
11
- Add this line to your application's Gemfile:
12
+ Add this line to your application's `Gemfile`:
12
13
 
13
14
  ```ruby
14
15
  gem 'geo_combine'
@@ -16,81 +17,96 @@ gem 'geo_combine'
16
17
 
17
18
  And then execute:
18
19
 
19
- $ bundle
20
+ $ bundle install
20
21
 
21
22
  Or install it yourself as:
22
23
 
23
24
  $ gem install geo_combine
24
25
 
25
26
  ## Usage
26
- GeoCombine can be used as a set of rake tasks for cloning, updating, and indexing OpenGeoMetdata metdata. It can also be used as a Ruby library for converting metdata.
27
27
 
28
- ### Transforming metadata
28
+ ### Converting metadata
29
29
 
30
30
  ```ruby
31
31
  # Create a new ISO19139 object
32
32
  > iso_metadata = GeoCombine::Iso19139.new('./tmp/opengeometadata/edu.stanford.purl/bb/338/jh/0716/iso19139.xml')
33
33
 
34
- # Convert it to GeoBlacklight
34
+ # Convert ISO to GeoBlacklight
35
35
  > iso_metadata.to_geoblacklight
36
36
 
37
37
  # Convert that to JSON
38
38
  > iso_metadata.to_geoblacklight.to_json
39
39
 
40
- # Convert ISO or FGDC to HTML
40
+ # Convert ISO (or FGDC) to HTML
41
41
  > iso_metadata.to_html
42
42
  ```
43
43
 
44
- ## Command line ##
45
-
46
- GeoCombine's tasks can be run either as rake tasks or as standalone executables.
44
+ ### OpenGeoMetadata
47
45
 
48
- ### Clone all OpenGeoMetadata repositories
46
+ #### Clone OpenGeoMetadata repositories locally
49
47
 
50
48
  ```sh
51
- $ rake geocombine:clone
49
+ $ bundle exec rake geocombine:clone
52
50
  ```
53
51
 
52
+ Will clone all `edu.*`,` org.*`, and `uk.*` OpenGeoMetadata repositories into `./tmp/opengeometadata`. Location of the OpenGeoMetadata repositories can be configured using the `OGM_PATH` environment variable.
53
+
54
54
  ```sh
55
- $ bundle exec geocombine clone
55
+ $ OGM_PATH='my/custom/location' bundle exec rake geocombine:clone
56
56
  ```
57
57
 
58
- Will clone all edu.* OpenGeoMetadata repositories into `./tmp/opengeometadata`. Location of the OpenGeoMetadata repositories can be configured using the `OGM_PATH` environment variable.
58
+ You can also specify a single repository:
59
59
 
60
60
  ```sh
61
- $ OGM_PATH='my/custom/location' rake geocombine:clone
61
+ $ bundle exec rake geocombine:clone[edu.stanford.purl]
62
62
  ```
63
63
 
64
- ### Pull all OpenGeoMetadata repositories
64
+ #### Update local OpenGeoMetadata repositories
65
65
 
66
66
  ```sh
67
- $ rake geocombine:pull
67
+ $ bundle exec rake geocombine:pull
68
68
  ```
69
69
 
70
+ Runs `git pull origin master` on all cloned repositories in `./tmp/opengeometadata` (or custom path with configured environment variable `OGM_PATH`).
71
+
72
+ You can also specify a single repository:
73
+
70
74
  ```sh
71
- $ bundle exec geocombine pull
75
+ $ bundle exec rake geocombine:pull[edu.stanford.purl]
72
76
  ```
73
77
 
74
- Runs `git pull origin master` on all cloned repositories in `./tmp/opengeometadata` (or custom path with configured environment variable `OGM_PATH`)
78
+ #### Index GeoBlacklight documents
75
79
 
76
- ### Index all of the GeoBlacklight documents
80
+ To index into Solr, GeoCombine requires a Solr instance that is running the
81
+ [GeoBlacklight schema](https://github.com/geoblacklight/geoblacklight):
77
82
 
78
83
  ```sh
79
- $ rake geocombine:index
84
+ $ bundle exec rake geocombine:index
80
85
  ```
81
86
 
87
+ Indexes the `geoblacklight.json` files in cloned repositories to a Solr index running at http://127.0.0.1:8983/solr
88
+
89
+ ##### Custom Solr location
90
+
91
+ Solr location can also be specified by an environment variable `SOLR_URL`.
92
+
82
93
  ```sh
83
- $ bundle exec geocombine index
94
+ $ SOLR_URL=http://www.example.com:1234/solr/collection bundle exec rake geocombine:index
84
95
  ```
85
96
 
86
- Indexes all of the `geoblacklight.json` files in cloned repositories to a Solr index running at http://127.0.0.1:8983/solr
97
+ Depending on your Solr instance's performance characteristics, you may want to
98
+ change the [`commitWithin` parameter](https://lucene.apache.org/solr/guide/6_6/updatehandlers-in-solrconfig.html) (in milliseconds):
99
+
100
+ ```sh
101
+ $ SOLR_COMMIT_WITHIN=100 bundle exec rake geocombine:index
102
+ ```
87
103
 
88
- #### Custom Solr location
104
+ ## Tests
89
105
 
90
- Solr location can also be specified by an environment variable `SOLR_URL`.
106
+ To run the tests, use:
91
107
 
92
108
  ```sh
93
- $ SOLR_URL=http://www.example.com:1234/solr/collection rake geocombine:index
109
+ $ bundle exec rake spec
94
110
  ```
95
111
 
96
112
  ## Contributing
data/geo_combine.gemspec CHANGED
@@ -18,14 +18,16 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
+ spec.add_dependency 'activesupport'
21
22
  spec.add_dependency 'rsolr'
23
+ spec.add_dependency 'net-http-persistent', '~> 2.0' # pin since faraday (rsolr) doesn't work correctly with 3.x
22
24
  spec.add_dependency 'nokogiri'
23
25
  spec.add_dependency 'json-schema'
24
26
  spec.add_dependency 'sanitize'
25
27
  spec.add_dependency 'thor'
26
28
 
27
- spec.add_development_dependency "bundler", "~> 1.7"
28
- spec.add_development_dependency "rake", "~> 10.0"
29
+ spec.add_development_dependency 'bundler'
30
+ spec.add_development_dependency 'rake'
29
31
  spec.add_development_dependency 'rspec'
30
32
  spec.add_development_dependency 'rspec-html-matchers'
31
33
  end
data/lib/geo_combine.rb CHANGED
@@ -20,7 +20,7 @@ module GeoCombine
20
20
  ##
21
21
  # Creates a new GeoCombine::Metadata object, where metadata parameter is can
22
22
  # be a File path or String of XML
23
- # @param [String] metadata can be a File path
23
+ # @param [String] metadata can be a File path
24
24
  # "./tmp/edu.stanford.purl/bb/338/jh/0716/iso19139.xml" or a String of XML
25
25
  # metadata
26
26
  def initialize metadata
@@ -66,6 +66,8 @@ require 'geo_combine/geoblacklight'
66
66
  require 'geo_combine/iso19139'
67
67
  require 'geo_combine/esri_open_data'
68
68
  require 'geo_combine/ckan_metadata'
69
+ require 'geo_combine/ogp'
69
70
 
70
71
  # Require gem files
71
72
  require 'geo_combine/version'
73
+ require 'geo_combine/railtie' if defined?(Rails)
@@ -15,7 +15,7 @@ module GeoCombine
15
15
  # @param [String] text
16
16
  # @return [String]
17
17
  def remove_lines(text)
18
- text.gsub(/\n/, '')
18
+ text.delete("\n")
19
19
  end
20
20
 
21
21
  ##
@@ -25,5 +25,10 @@ module GeoCombine
25
25
  def sanitize_and_remove_lines(text)
26
26
  remove_lines(sanitize(text))
27
27
  end
28
+
29
+ # slugs should be lowercase and only have a-z, A-Z, 0-9, and -
30
+ def sluggify(slug)
31
+ slug.gsub(/[^a-zA-Z0-9\-]/, '-').gsub(/[\-]+/, '-').downcase
32
+ end
28
33
  end
29
34
  end
@@ -1,3 +1,5 @@
1
+ require 'active_support/core_ext/object/blank'
2
+ require 'active_support/core_ext/hash/except'
1
3
  require 'open-uri'
2
4
 
3
5
  module GeoCombine
@@ -9,6 +11,16 @@ module GeoCombine
9
11
  attr_reader :metadata
10
12
 
11
13
  GEOBLACKLIGHT_VERSION = 'v1.1.0'
14
+ SCHEMA_JSON_URL = "https://raw.githubusercontent.com/geoblacklight/geoblacklight/#{GEOBLACKLIGHT_VERSION}/schema/geoblacklight-schema.json".freeze
15
+ DEPRECATED_KEYS_V1 = %w[
16
+ uuid
17
+ georss_polygon_s
18
+ georss_point_s
19
+ georss_box_s
20
+ dc_relation_sm
21
+ solr_issued_i
22
+ solr_bbox
23
+ ].freeze
12
24
 
13
25
  ##
14
26
  # Initializes a GeoBlacklight object
@@ -24,7 +36,9 @@ module GeoCombine
24
36
  # Calls metadata enhancement methods for each key, value pair in the
25
37
  # metadata hash
26
38
  def enhance_metadata
27
- @metadata.each do |key, value|
39
+ upgrade_to_v1 if metadata['geoblacklight_version'].blank?
40
+
41
+ metadata.each do |key, value|
28
42
  translate_formats(key, value)
29
43
  enhance_subjects(key, value)
30
44
  format_proper_date(key, value)
@@ -36,15 +50,15 @@ module GeoCombine
36
50
  ##
37
51
  # Returns a string of JSON from a GeoBlacklight hash
38
52
  # @return (String)
39
- def to_json
40
- @metadata.to_json
53
+ def to_json(options = {})
54
+ metadata.to_json(options)
41
55
  end
42
56
 
43
57
  ##
44
58
  # Validates a GeoBlacklight-Schema json document
45
59
  # @return [Boolean]
46
60
  def valid?
47
- @schema ||= JSON.parse(open("https://raw.githubusercontent.com/geoblacklight/geoblacklight/#{GEOBLACKLIGHT_VERSION}/schema/geoblacklight-schema.json").read)
61
+ @schema ||= JSON.parse(open(SCHEMA_JSON_URL).read)
48
62
  JSON::Validator.validate!(@schema, to_json, fragment: '#/properties/layer') &&
49
63
  dct_references_validate! &&
50
64
  spatial_validate!
@@ -54,7 +68,7 @@ module GeoCombine
54
68
  # Validate dct_references_s
55
69
  # @return [Boolean]
56
70
  def dct_references_validate!
57
- return true unless metadata.key?('dct_references_s')
71
+ return true unless metadata.key?('dct_references_s') # TODO: shouldn't we require this field?
58
72
  begin
59
73
  ref = JSON.parse(metadata['dct_references_s'])
60
74
  raise GeoCombine::Exceptions::InvalidDCTReferences, 'dct_references must be parsed to a Hash' unless ref.is_a?(Hash)
@@ -74,43 +88,72 @@ module GeoCombine
74
88
  # Enhances the 'dc_format_s' field by translating a format type to a valid
75
89
  # GeoBlacklight-Schema format
76
90
  def translate_formats(key, value)
77
- @metadata[key] = formats[value] if key == 'dc_format_s' && formats.include?(value)
91
+ return unless key == 'dc_format_s' && formats.include?(value)
92
+ metadata[key] = formats[value]
78
93
  end
79
94
 
80
95
  ##
81
96
  # Enhances the 'layer_geom_type_s' field by translating from known types
82
97
  def translate_geometry_type(key, value)
83
- @metadata[key] = geometry_types[value] if key == 'layer_geom_type_s' && geometry_types.include?(value)
98
+ return unless key == 'layer_geom_type_s' && geometry_types.include?(value)
99
+ metadata[key] = geometry_types[value]
84
100
  end
85
101
 
86
102
  ##
87
103
  # Enhances the 'dc_subject_sm' field by translating subjects to ISO topic
88
104
  # categories
89
105
  def enhance_subjects(key, value)
90
- @metadata[key] = value.map do |val|
106
+ return unless key == 'dc_subject_sm'
107
+ metadata[key] = value.map do |val|
91
108
  if subjects.include?(val)
92
109
  subjects[val]
93
110
  else
94
111
  val
95
112
  end
96
- end if key == 'dc_subject_sm'
113
+ end
97
114
  end
98
115
 
99
116
  ##
100
117
  # Formats the 'layer_modified_dt' to a valid valid RFC3339 date/time string
101
118
  # and ISO8601 (for indexing into Solr)
102
119
  def format_proper_date(key, value)
103
- @metadata[key] = Time.parse(value).utc.iso8601 if key == 'layer_modified_dt'
120
+ return unless key == 'layer_modified_dt'
121
+ metadata[key] = Time.parse(value).utc.iso8601
104
122
  end
105
123
 
106
124
  def fields_should_be_array(key, value)
107
- @metadata[key] = [value] if should_be_array.include?(key) && !value.kind_of?(Array)
125
+ return unless should_be_array.include?(key) && !value.is_a?(Array)
126
+ metadata[key] = [value]
108
127
  end
109
128
 
110
129
  ##
111
130
  # GeoBlacklight-Schema fields that should be type Array
112
131
  def should_be_array
113
- ['dc_creator_sm', 'dc_subject_sm', 'dct_spatial_sm', 'dct_temporal_sm', 'dct_isPartOf_sm']
132
+ %w[
133
+ dc_creator_sm
134
+ dc_subject_sm
135
+ dct_spatial_sm
136
+ dct_temporal_sm
137
+ dct_isPartOf_sm
138
+ ].freeze
139
+ end
140
+
141
+ ##
142
+ # Converts a pre-v1.0 schema into a compliant v1.0 schema
143
+ def upgrade_to_v1
144
+ metadata['geoblacklight_version'] = '1.0'
145
+
146
+ # ensure required fields
147
+ metadata['dc_identifier_s'] = metadata['uuid'] if metadata['dc_identifier_s'].blank?
148
+
149
+ # normalize to alphanum and - only
150
+ metadata['layer_slug_s'].gsub!(/[^[[:alnum:]]]+/, '-') if metadata['layer_slug_s'].present?
151
+
152
+ # remove deprecated fields
153
+ metadata.except!(*DEPRECATED_KEYS_V1)
154
+
155
+ # ensure we have a proper v1 record
156
+ valid?
114
157
  end
115
158
  end
116
159
  end
@@ -0,0 +1,229 @@
1
+ require 'active_support/core_ext/object/blank'
2
+ require 'cgi'
3
+
4
+ module GeoCombine
5
+ # Data model for OpenGeoPortal metadata
6
+ class OGP
7
+ class InvalidMetadata < RuntimeError; end
8
+ include GeoCombine::Formatting
9
+ attr_reader :metadata
10
+
11
+ ##
12
+ # Initializes an OGP object for parsing
13
+ # @param [String] metadata a valid serialized JSON string from OGP instance
14
+ # @raise [InvalidMetadata]
15
+ def initialize(metadata)
16
+ @metadata = JSON.parse(metadata)
17
+ raise InvalidMetadata unless valid?
18
+ end
19
+
20
+ OGP_REQUIRED_FIELDS = %w[
21
+ Access
22
+ Institution
23
+ LayerDisplayName
24
+ LayerId
25
+ MaxX
26
+ MaxY
27
+ MinX
28
+ MinY
29
+ Name
30
+ ].freeze
31
+
32
+ ##
33
+ # Runs validity checks on OGP metadata to ensure fields are present
34
+ def valid?
35
+ OGP_REQUIRED_FIELDS.all? { |k| metadata[k].present? }
36
+ end
37
+
38
+ ##
39
+ # Creates and returns a Geoblacklight schema object from this metadata
40
+ # @return [GeoCombine::Geoblacklight]
41
+ def to_geoblacklight
42
+ GeoCombine::Geoblacklight.new(geoblacklight_terms.to_json)
43
+ end
44
+
45
+ ##
46
+ # Builds a Geoblacklight Schema type hash from Esri Open Data portal
47
+ # metadata
48
+ # @return [Hash]
49
+ def geoblacklight_terms
50
+ {
51
+ # Required fields
52
+ dc_identifier_s: identifier,
53
+ layer_slug_s: slug,
54
+ dc_title_s: metadata['LayerDisplayName'],
55
+ solr_geom: envelope,
56
+ dct_provenance_s: institution,
57
+ dc_rights_s: metadata['Access'],
58
+ geoblacklight_version: '1.0',
59
+
60
+ # Recommended fields
61
+ dc_description_s: metadata['Abstract'],
62
+ layer_geom_type_s: ogp_geom,
63
+ dct_references_s: references,
64
+ layer_id_s: "#{metadata['WorkspaceName']}:#{metadata['Name']}",
65
+
66
+ # Optional
67
+ dct_temporal_sm: [metadata['ContentDate']],
68
+ dc_format_s: ogp_formats,
69
+ # dct_issued_dt
70
+ # dc_language_s
71
+ dct_spatial_sm: placenames,
72
+ solr_year_i: year,
73
+ dc_publisher_s: metadata['Publisher'],
74
+ dc_subject_sm: subjects,
75
+ dc_type_s: 'Dataset'
76
+ }.delete_if { |_k, v| v.nil? }
77
+ end
78
+
79
+ def date
80
+ begin
81
+ DateTime.rfc3339(metadata['ContentDate'])
82
+ rescue
83
+ nil
84
+ end
85
+ end
86
+
87
+ def year
88
+ date.year unless date.nil?
89
+ end
90
+
91
+ ##
92
+ # Convert "Paper Map" to Raster, assumes all OGP "Paper Maps" have WMS
93
+ def ogp_geom
94
+ case metadata['DataType']
95
+ when 'Paper Map'
96
+ 'Raster'
97
+ else
98
+ metadata['DataType']
99
+ end
100
+ end
101
+
102
+ ##
103
+ # OGP doesn't ship format types, so we just try and be clever here.
104
+ def ogp_formats
105
+ case metadata['DataType']
106
+ when 'Paper Map', 'Raster'
107
+ return 'GeoTIFF'
108
+ when 'Polygon', 'Point', 'Line'
109
+ return 'Shapefile'
110
+ else
111
+ raise ArgumentError, metadata['DataType']
112
+ end
113
+ end
114
+
115
+ ##
116
+ # Converts references to json
117
+ # @return [String]
118
+ def references
119
+ references_hash.to_json
120
+ end
121
+
122
+ ##
123
+ # Builds a Solr Envelope using CQL syntax
124
+ # @return [String]
125
+ def envelope
126
+ raise ArgumentError unless west >= -180 && west <= 180 &&
127
+ east >= -180 && east <= 180 &&
128
+ north >= -90 && north <= 90 &&
129
+ south >= -90 && south <= 90 &&
130
+ west <= east && south <= north
131
+ "ENVELOPE(#{west}, #{east}, #{north}, #{south})"
132
+ end
133
+
134
+ def subjects
135
+ fgdc.metadata.xpath('//themekey').map(&:text) if fgdc
136
+ end
137
+
138
+ def placenames
139
+ fgdc.metadata.xpath('//placekey').map(&:text) if fgdc
140
+ end
141
+
142
+ def fgdc
143
+ GeoCombine::Fgdc.new(metadata['FgdcText']) if metadata['FgdcText']
144
+ end
145
+
146
+ private
147
+
148
+ ##
149
+ # Builds references used for dct_references
150
+ # @return [Hash]
151
+ def references_hash
152
+ results = {
153
+ 'http://www.opengis.net/def/serviceType/ogc/wfs' => location['wfs'],
154
+ 'http://www.opengis.net/def/serviceType/ogc/wms' => location['wms'],
155
+ 'http://schema.org/url' => location['url'],
156
+ download_uri => location['download']
157
+ }
158
+
159
+ # Handle null, "", and [""]
160
+ results.map { |k, v| { k => ([] << v).flatten.first } if v }
161
+ .flatten
162
+ .compact
163
+ .reduce({}, :merge)
164
+ end
165
+
166
+ def download_uri
167
+ return 'http://schema.org/DownloadAction' if institution == 'Harvard'
168
+ 'http://schema.org/downloadUrl'
169
+ end
170
+
171
+ ##
172
+ # OGP "Location" field parsed
173
+ def location
174
+ JSON.parse(metadata['Location'])
175
+ end
176
+
177
+ def north
178
+ metadata['MaxY'].to_f
179
+ end
180
+
181
+ def south
182
+ metadata['MinY'].to_f
183
+ end
184
+
185
+ def east
186
+ metadata['MaxX'].to_f
187
+ end
188
+
189
+ def west
190
+ metadata['MinX'].to_f
191
+ end
192
+
193
+ def institution
194
+ metadata['Institution']
195
+ end
196
+
197
+ def identifier
198
+ CGI.escape(metadata['LayerId']) # TODO: why are we using CGI.escape?
199
+ end
200
+
201
+ def slug
202
+ name = metadata['LayerId'] || metadata['Name'] || ''
203
+ name = [institution, name].join('-') if institution.present? &&
204
+ !name.downcase.start_with?(institution.downcase)
205
+ sluggify(filter_name(name))
206
+ end
207
+
208
+ SLUG_BLACKLIST = %w[
209
+ SDE_DATA.
210
+ SDE.
211
+ SDE2.
212
+ GISPORTAL.GISOWNER01.
213
+ GISDATA.
214
+ MORIS.
215
+ ].freeze
216
+
217
+ def filter_name(name)
218
+ # strip out schema and usernames
219
+ SLUG_BLACKLIST.each do |blacklisted|
220
+ name.sub!(blacklisted, '')
221
+ end
222
+ unless name.size > 1
223
+ # use first word of title is empty name
224
+ name = metadata['LayerDisplayName'].split.first
225
+ end
226
+ name
227
+ end
228
+ end
229
+ end