omnijack 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.travis.yml +6 -0
  4. data/CHANGELOG.md +11 -0
  5. data/Gemfile +6 -0
  6. data/LICENSE.txt +15 -0
  7. data/NOTICE +5 -0
  8. data/README.md +119 -0
  9. data/Rakefile +23 -0
  10. data/features/list.feature +19 -0
  11. data/features/metadata.feature +43 -0
  12. data/features/platforms.feature +23 -0
  13. data/features/step_definitions/list.rb +12 -0
  14. data/features/step_definitions/metadata.rb +20 -0
  15. data/features/step_definitions/platforms.rb +8 -0
  16. data/features/step_definitions/project.rb +20 -0
  17. data/features/support/env.rb +4 -0
  18. data/lib/omnijack/config.rb +67 -0
  19. data/lib/omnijack/list.rb +94 -0
  20. data/lib/omnijack/metadata.rb +244 -0
  21. data/lib/omnijack/platforms.rb +96 -0
  22. data/lib/omnijack/project/metaprojects.rb +38 -0
  23. data/lib/omnijack/project.rb +63 -0
  24. data/lib/omnijack/version.rb +24 -0
  25. data/lib/omnijack.rb +54 -0
  26. data/omnijack.gemspec +37 -0
  27. data/spec/omnijack/config_spec.rb +55 -0
  28. data/spec/omnijack/list_spec.rb +133 -0
  29. data/spec/omnijack/metadata_spec.rb +577 -0
  30. data/spec/omnijack/platforms_spec.rb +132 -0
  31. data/spec/omnijack/project/angry_chef_spec.rb +55 -0
  32. data/spec/omnijack/project/chef_container_spec.rb +55 -0
  33. data/spec/omnijack/project/chef_dk_spec.rb +55 -0
  34. data/spec/omnijack/project/chef_server_spec.rb +55 -0
  35. data/spec/omnijack/project/chef_spec.rb +55 -0
  36. data/spec/omnijack/project_spec.rb +52 -0
  37. data/spec/omnijack_spec.rb +109 -0
  38. data/spec/spec_helper.rb +38 -0
  39. data/spec/support/real_test_data.json +131 -0
  40. data/vendor/chef/LICENSE +201 -0
  41. data/vendor/chef/NOTICE +21 -0
  42. data/vendor/chef/lib/chef/exceptions.rb +353 -0
  43. data/vendor/chef/lib/chef/mixin/params_validate.rb +242 -0
  44. metadata +276 -0
@@ -0,0 +1,244 @@
1
+ # Encoding: UTF-8
2
+ #
3
+ # Author:: Jonathan Hartman (<j@p4nt5.com>)
4
+ #
5
+ # Copyright (C) 2014, Jonathan Hartman
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require 'ohai'
20
+ require 'open-uri'
21
+ require_relative 'config'
22
+ require_relative '../omnijack'
23
+ require_relative '../../vendor/chef/lib/chef/exceptions'
24
+ require_relative '../../vendor/chef/lib/chef/mixin/params_validate'
25
+
26
+ class Omnijack
27
+ # A class for representing an Omnitruck metadata object
28
+ #
29
+ # @author Jonathan Hartman <j@p4nt5.com>
30
+ class Metadata < Omnijack
31
+ include ::Chef::Mixin::ParamsValidate
32
+ include Config
33
+
34
+ def initialize(name, args = {})
35
+ super
36
+ args.each { |k, v| send(k, v) unless v.nil? } unless args.nil?
37
+ end
38
+
39
+ #
40
+ # Set up an accessor method for each piece of metadata
41
+ #
42
+ METADATA_ATTRIBUTES.each do |a|
43
+ define_method(a) { to_h[a] }
44
+ end
45
+ define_method(:filename) { to_h[:filename] }
46
+
47
+ #
48
+ # Make metadata accessible via hash keys
49
+ #
50
+ # @param [Symbol] key
51
+ # @return [String, NilClass]
52
+ #
53
+ def [](key)
54
+ to_h[key]
55
+ end
56
+
57
+ #
58
+ # Offer a hash representation of the metadata
59
+ #
60
+ # @return [Hash]
61
+ #
62
+ def to_h
63
+ raw_metadata.split("\n").each_with_object({}) do |line, hsh|
64
+ key = line.split[0].to_sym
65
+ val = case line.split[1]
66
+ when 'true' then true
67
+ when 'false' then false
68
+ else line.split[1]
69
+ end
70
+ hsh[key] = val
71
+ hsh[:filename] = val.split('/')[-1] if key == :url
72
+ end
73
+ end
74
+
75
+ #
76
+ # Use the raw metadata string as a string representation
77
+ #
78
+ define_method(:to_s) { raw_metadata }
79
+
80
+ #
81
+ # The version of the project
82
+ #
83
+ # @param [String, NilClass] arg
84
+ # @return [String]
85
+ #
86
+ def version(arg = nil)
87
+ set_or_return(:version,
88
+ arg,
89
+ kind_of: String,
90
+ default: 'latest',
91
+ callbacks: {
92
+ 'Invalid version string' => ->(a) { valid_version?(a) }
93
+ })
94
+ end
95
+
96
+ #
97
+ # Whether to enable prerelease and/or nightly packages
98
+ #
99
+ # @param [TrueClass, FalseClass, NilClass] arg
100
+ # @return [TrueClass, FalseClass]
101
+ #
102
+ [:prerelease, :nightlies].each do |m|
103
+ define_method(m) do |arg = nil|
104
+ set_or_return(m, arg, kind_of: [TrueClass, FalseClass], default: false)
105
+ end
106
+ end
107
+
108
+ #
109
+ # The name of the desired platform
110
+ #
111
+ # @param [String, NilClass]
112
+ # @return [String]
113
+ #
114
+ def platform(arg = nil)
115
+ set_or_return(:platform, arg, kind_of: String, default: node[:platform])
116
+ end
117
+
118
+ #
119
+ # The version of the desired platform
120
+ #
121
+ # @param [String, NilClass] arg
122
+ # @return [String]
123
+ #
124
+ def platform_version(arg = nil)
125
+ # TODO: The platform version parser living in `node` means passing e.g.
126
+ # '10.9.2' here won't result in it being shortened to '10.9'
127
+ set_or_return(:platform_version,
128
+ arg,
129
+ kind_of: String,
130
+ default: node[:platform_version])
131
+ end
132
+
133
+ #
134
+ # The machine architecture of the desired platform
135
+ #
136
+ # @param [String, NilClass]
137
+ # @return [String]
138
+ #
139
+ def machine_arch(arg = nil)
140
+ set_or_return(:machine_arch,
141
+ arg,
142
+ kind_of: String,
143
+ default: node[:kernel][:machine])
144
+ end
145
+
146
+ private
147
+
148
+ #
149
+ # Fetch the raw metadata from the configured URI
150
+ #
151
+ # @return [String]
152
+ #
153
+ def raw_metadata
154
+ @raw_metadata ||= api_url.open.read
155
+ end
156
+
157
+ #
158
+ # Construct the full API query URL from base + endpoint + params
159
+ #
160
+ # @return [URI::HTTP, URI::HTTPS]
161
+ #
162
+ def api_url
163
+ @api_url ||= URI.parse(
164
+ File.join(base_url, "#{endpoint}?#{URI.encode_www_form(query_params)}")
165
+ )
166
+ end
167
+
168
+ #
169
+ # Convert all the metadata attrs into params Omnitruck understands
170
+ #
171
+ # @return [Hash]
172
+ #
173
+ def query_params
174
+ { v: version,
175
+ prerelease: prerelease,
176
+ nightlies: nightlies,
177
+ p: platform,
178
+ pv: platform_version,
179
+ m: machine_arch }
180
+ end
181
+
182
+ #
183
+ # Return the API endpoint for the metadata of this project
184
+ #
185
+ # @return [String]
186
+ #
187
+ def endpoint
188
+ OMNITRUCK_PROJECTS[name][:endpoints][:metadata]
189
+ end
190
+
191
+ #
192
+ # Fetch and return node data from Ohai
193
+ #
194
+ # @return [Mash]
195
+ #
196
+ def node
197
+ unless @node
198
+ @node = Ohai::System.new.all_plugins('platform')[0].data
199
+ case @node[:platform]
200
+ when 'mac_os_x'
201
+ @node[:platform_version] = platform_version_mac_os_x
202
+ when 'windows'
203
+ @node[:platform_version] = platform_version_windows
204
+ end
205
+ end
206
+ @node
207
+ end
208
+
209
+ #
210
+ # Apply special logic for the version of an OS X platform
211
+ #
212
+ # @return [String]
213
+ #
214
+ def platform_version_mac_os_x
215
+ node[:platform_version].match(/^[0-9]+\.[0-9]+/).to_s
216
+ end
217
+
218
+ #
219
+ # Apply special logic for the version of a Windows platform
220
+ #
221
+ # @return [String]
222
+ #
223
+ def platform_version_windows
224
+ # Make a best guess and assume a server OS
225
+ # See: http://msdn.microsoft.com/en-us/library/windows/
226
+ # desktop/ms724832(v=vs.85).aspx
227
+ {
228
+ '6.3' => '2012r2', '6.2' => '2012', '6.1' => '2008r2', '6.0' => '2008',
229
+ '5.2' => '2003r2', '5.1' => 'xp', '5.0' => '2000'
230
+ }[node[:platform_version].match(/^[0-9]+\.[0-9]+/).to_s]
231
+ end
232
+
233
+ #
234
+ # Determine whether a string is a valid version string
235
+ #
236
+ # @param [String] arg
237
+ # @return [TrueClass, FalseClass]
238
+ #
239
+ def valid_version?(arg)
240
+ return true if arg == 'latest'
241
+ arg.match(/^[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$/) ? true : false
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,96 @@
1
+ # Encoding: UTF-8
2
+ #
3
+ # Author:: Jonathan Hartman (<j@p4nt5.com>)
4
+ #
5
+ # Copyright (C) 2014, Jonathan Hartman
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require 'json'
20
+ require 'open-uri'
21
+ require_relative 'config'
22
+ require_relative '../omnijack'
23
+
24
+ class Omnijack
25
+ # A class for representing an Omnitruck list of platform names
26
+ #
27
+ # @author Jonathan Hartman <j@p4nt5.com>
28
+ class Platforms < Omnijack
29
+ include Config
30
+
31
+ # TODO: Lots of duplicate code in platforms and list
32
+
33
+ #
34
+ # Make list items accessible via methods
35
+ #
36
+ # @param [Symbol] method_id
37
+ #
38
+ def method_missing(method_id, args = nil)
39
+ args.nil? && to_h[method_id] || super
40
+ end
41
+
42
+ #
43
+ # Make list items accessible via hash keys
44
+ #
45
+ # @param [Symbol] key
46
+ # @return [String, NilClass]
47
+ #
48
+ def [](key)
49
+ to_h[key]
50
+ end
51
+
52
+ #
53
+ # Offer a hash representation of the list
54
+ #
55
+ # @return [Hash]
56
+ #
57
+ def to_h
58
+ # TODO: Use a Mash -- some keys are better off addressed as strings
59
+ JSON.parse(raw_data, symbolize_names: true)
60
+ end
61
+
62
+ #
63
+ # Use the raw data string as a string representation
64
+ #
65
+ define_method(:to_s) { raw_data }
66
+
67
+ private
68
+
69
+ #
70
+ # Fetch the raw list from the configured URI
71
+ #
72
+ # @return [String]
73
+ #
74
+ def raw_data
75
+ @raw_data ||= api_url.open.read
76
+ end
77
+
78
+ #
79
+ # Construct the full API query URL from base + endpoint
80
+ #
81
+ # @return [URI::HTTP, URI::HTTPS]
82
+ #
83
+ def api_url
84
+ @api_url ||= URI.parse(::File.join(base_url, endpoint))
85
+ end
86
+
87
+ #
88
+ # Return the API endpoint for the package list of this project
89
+ #
90
+ # @return [String]
91
+ #
92
+ def endpoint
93
+ OMNITRUCK_PROJECTS[name][:endpoints][:platform_names]
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,38 @@
1
+ # Encoding: UTF-8
2
+ #
3
+ # Author:: Jonathan Hartman (<j@p4nt5.com>)
4
+ #
5
+ # Copyright (C) 2014, Jonathan Hartman
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require_relative '../config'
20
+ require_relative '../project'
21
+
22
+ class Omnijack
23
+ class Project < Omnijack
24
+ end
25
+ end
26
+
27
+ # Dynamically define classes for each configured project
28
+ #
29
+ # @author Jonathan Hartman <j@p4nt5.com>
30
+ Omnijack::Config::OMNITRUCK_PROJECTS.each do |project_name, _|
31
+ klass = Class.new(Omnijack::Project) do
32
+ define_method(:initialize) do |args = {}|
33
+ super(project_name, args)
34
+ end
35
+ end
36
+ class_name = project_name.to_s.split('_').map(&:capitalize).join
37
+ Omnijack::Project.const_set(class_name, klass)
38
+ end
@@ -0,0 +1,63 @@
1
+ # Encoding: UTF-8
2
+ #
3
+ # Author:: Jonathan Hartman (<j@p4nt5.com>)
4
+ #
5
+ # Copyright (C) 2014, Jonathan Hartman
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require_relative '../omnijack'
20
+ require_relative 'config'
21
+ require_relative 'list'
22
+ require_relative 'metadata'
23
+ require_relative 'platforms'
24
+ require_relative 'project/metaprojects'
25
+ require_relative '../../vendor/chef/lib/chef/exceptions'
26
+ require_relative '../../vendor/chef/lib/chef/mixin/params_validate'
27
+
28
+ class Omnijack
29
+ # A parent project that can contain metadata, a pkg list, and platforms
30
+ #
31
+ # @author Jonathan Hartman <j@p4nt5.com>
32
+ class Project < Omnijack
33
+ include Config
34
+
35
+ #
36
+ # The Metadata instance for the project
37
+ #
38
+ # @return [Omnijack::Metadata]
39
+ #
40
+ def metadata
41
+ # TODO: This requires too much knowledge of the Metadata class
42
+ @metadata ||= Metadata.new(name, args)
43
+ end
44
+
45
+ #
46
+ # The full list instance for the project
47
+ #
48
+ # @return [Omnijack::List]
49
+ #
50
+ def list
51
+ @list ||= List.new(name, args)
52
+ end
53
+
54
+ #
55
+ # The platform names instance for the project
56
+ #
57
+ # @return [Omnijack::Platforms]
58
+ #
59
+ def platforms
60
+ @platforms ||= Platforms.new(name, args)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,24 @@
1
+ # Encoding: UTF-8
2
+ #
3
+ # Author:: Jonathan Hartman (<j@p4nt5.com>)
4
+ #
5
+ # Copyright (C) 2014, Jonathan Hartman
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ # Version string for Chef::Package::Metadata
20
+ #
21
+ # @author Jonathan Hartman <j@p4nt5.com>
22
+ class Omnijack
23
+ VERSION = '0.1.0'
24
+ end
data/lib/omnijack.rb ADDED
@@ -0,0 +1,54 @@
1
+ # Encoding: UTF-8
2
+ #
3
+ # Author:: Jonathan Hartman (<j@p4nt5.com>)
4
+ #
5
+ # Copyright (C) 2014, Jonathan Hartman
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require_relative '../vendor/chef/lib/chef/exceptions'
20
+ require_relative '../vendor/chef/lib/chef/mixin/params_validate'
21
+ require_relative 'omnijack/config'
22
+ require_relative 'omnijack/project'
23
+ require_relative 'omnijack/list'
24
+ require_relative 'omnijack/metadata'
25
+ require_relative 'omnijack/platforms'
26
+ require_relative 'omnijack/version'
27
+
28
+ # Provide a base class with some commons everyone can inherit
29
+ #
30
+ # @author Jonathan Hartman <j@p4nt5.com>
31
+ class Omnijack
32
+ include ::Chef::Mixin::ParamsValidate
33
+ include Config
34
+
35
+ def initialize(name, args = {})
36
+ @name = name.to_sym
37
+ @args = args
38
+ base_url(args[:base_url]) if args && args[:base_url]
39
+ end
40
+ attr_reader :name, :args
41
+
42
+ #
43
+ # The base URL of the Omnitruck API
44
+ #
45
+ # @param [String, NilClass] arg
46
+ # @return [String]
47
+ #
48
+ def base_url(arg = nil)
49
+ # TODO: Better URL validation
50
+ set_or_return(:base_url, arg, kind_of: String, default: DEFAULT_BASE_URL)
51
+ end
52
+
53
+ # TODO: Every class' `endpoint` method is similar enough they could go here
54
+ end
data/omnijack.gemspec ADDED
@@ -0,0 +1,37 @@
1
+ # Encoding: UTF-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'omnijack/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'omnijack'
9
+ spec.version = Omnijack::VERSION
10
+ spec.authors = ['Jonathan Hartman']
11
+ spec.email = %w(j@p4nt5.com)
12
+ spec.summary = 'A pallet jack to unload data from the Omnitruck'
13
+ spec.description = 'A pallet jack to unload data from the Omnitruck'
14
+ spec.homepage = 'https://rubygems.org/gems/omnijack'
15
+ spec.license = 'Apache v2.0'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0")
18
+ spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(/^(test|spec|features)\//)
20
+ spec.require_paths = %w(lib)
21
+
22
+ spec.required_ruby_version = '>= 1.9.3'
23
+
24
+ spec.add_dependency 'ohai'
25
+ spec.add_dependency 'json'
26
+
27
+ spec.add_development_dependency 'bundler'
28
+ spec.add_development_dependency 'rake'
29
+ spec.add_development_dependency 'rubocop'
30
+ spec.add_development_dependency 'cane'
31
+ spec.add_development_dependency 'countloc'
32
+ spec.add_development_dependency 'rspec'
33
+ spec.add_development_dependency 'simplecov'
34
+ spec.add_development_dependency 'simplecov-console'
35
+ spec.add_development_dependency 'coveralls'
36
+ spec.add_development_dependency 'cucumber'
37
+ end
@@ -0,0 +1,55 @@
1
+ # Encoding: UTF-8
2
+ #
3
+ # Author:: Jonathan Hartman (<j@p4nt5.com>)
4
+ #
5
+ # Copyright (C) 2014, Jonathan Hartman
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require_relative '../spec_helper'
20
+ require_relative '../../lib/omnijack/config'
21
+
22
+ describe Omnijack::Config do
23
+ describe 'DEFAULT_BASE_URL' do
24
+ it 'uses the official Chef API' do
25
+ expected = 'https://www.getchef.com/chef'
26
+ expect(described_class::DEFAULT_BASE_URL).to eq(expected)
27
+ end
28
+ end
29
+
30
+ describe 'OMNITRUCK_PROJECTS' do
31
+ it 'recognizes all the valid Omnitruck projects' do
32
+ expected = [:angry_chef, :chef, :chef_dk, :chef_container, :chef_server]
33
+ expect(described_class::OMNITRUCK_PROJECTS.keys).to eq(expected)
34
+ end
35
+
36
+ described_class::OMNITRUCK_PROJECTS.each do |project, attrs|
37
+ attrs[:endpoints].each do |name, endpoint|
38
+ it "uses a valid #{project}::#{name} endpoint" do
39
+ url = "http://www.getchef.com/chef#{endpoint}"
40
+ url << '?v=latest&p=ubuntu&pv=12.04&m=x86_64' if name == :metadata
41
+
42
+ # Some endpoints aren't available on Chef's public Omnitruck API
43
+ if [:angry_chef, :chef_dk, :chef_container].include?(project) && \
44
+ [:package_list, :platform_names].include?(name)
45
+ expected = 404
46
+ else
47
+ expected = 200
48
+ end
49
+
50
+ expect(Net::HTTP.get_response(URI(url)).code.to_i).to eq(expected)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end