wrest 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/README.rdoc +51 -19
  2. data/Rakefile +8 -4
  3. data/VERSION.yml +1 -1
  4. data/lib/wrest.rb +11 -2
  5. data/lib/wrest/components.rb +1 -2
  6. data/lib/wrest/components/attributes_container.rb +31 -69
  7. data/lib/wrest/components/attributes_container/typecaster.rb +121 -0
  8. data/lib/wrest/components/mutators.rb +18 -1
  9. data/lib/wrest/components/mutators/base.rb +52 -39
  10. data/lib/wrest/components/mutators/camel_to_snake_case.rb +7 -5
  11. data/lib/wrest/components/mutators/xml_mini_type_caster.rb +43 -0
  12. data/lib/wrest/components/mutators/xml_simple_type_caster.rb +22 -20
  13. data/lib/wrest/components/translators.rb +20 -17
  14. data/lib/wrest/components/translators/content_types.rb +2 -2
  15. data/lib/wrest/components/translators/json.rb +11 -8
  16. data/lib/wrest/components/translators/xml.rb +9 -12
  17. data/lib/wrest/core_ext/hash/conversions.rb +1 -1
  18. data/lib/wrest/resource/base.rb +25 -13
  19. data/lib/wrest/resource/state.rb +6 -0
  20. data/lib/wrest/response.rb +4 -0
  21. data/lib/wrest/uri.rb +5 -1
  22. data/lib/wrest/version.rb +1 -1
  23. data/spec/spec.opts +1 -1
  24. data/spec/spec_helper.rb +8 -1
  25. data/spec/wrest/components/attributes_container/typecaster_spec.rb +63 -0
  26. data/spec/wrest/components/attributes_container_spec.rb +6 -61
  27. data/spec/wrest/components/mutators/base_spec.rb +5 -1
  28. data/spec/wrest/components/mutators/xml_mini_type_caster_spec.rb +75 -0
  29. data/spec/wrest/components/mutators_spec.rb +21 -0
  30. data/spec/wrest/components/translators/xml_spec.rb +1 -1
  31. data/spec/wrest/components/translators_spec.rb +9 -0
  32. data/spec/wrest/uri_spec.rb +16 -4
  33. metadata +14 -15
  34. data/lib/wrest/components/typecast_helpers.rb +0 -41
data/README.rdoc CHANGED
@@ -40,30 +40,56 @@ You can launch the interactive Wrest shell by running bin/wrest if you have the
40
40
  )
41
41
  === Basic Http Calls
42
42
 
43
- ==== Get
43
+ ==== GET
44
44
 
45
- A couple of ways to get the Yahoo news as hash map (needs the JSON gem - gem install json).
45
+ A couple of ways to get the Yahoo news as hash map.
46
46
 
47
47
  * This example simply does a get on a uri and figures out the appropriate deserialiser using the content-type (in this case 'text/javascript', which uses Wrest::Translators::Json). See content_types.rb under lib/wrest/mappers/translators.
48
48
  "http://search.yahooapis.com/NewsSearchService/V1/newsSearch?appid=YahooDemo&output=json&query=India&results=3&start=1".to_uri.get.deserialise
49
49
 
50
50
  * This example does a get on a base uri with several parameters passed to it, resulting in a uri essentially the same as the one above. It also shows how you can specify a custom deserialiser to produce a hash-map from the response, as well as a hash mutator to clean up the deserialised hash.
51
- require 'rubygems'
52
- require 'wrest'
51
+ require 'rubygems'
52
+ require 'wrest'
53
+ include Wrest::Components
54
+ y "http://search.yahooapis.com/NewsSearchService/V1/newsSearch".to_uri.get(
55
+ :appid => 'YahooDemo',
56
+ :output => 'xml',
57
+ :query => 'India',
58
+ :results=> '3',
59
+ :start => '1'
60
+ ).deserialise_using(
61
+ Translators::Xml
62
+ ).mutate_using(
63
+ Mutators::XmlSimpleTypeCaster.new
64
+ )
65
+
66
+ ==== OPTIONS
67
+
68
+ To find out what actions are permitted on a URI:
69
+
70
+ 'http://www.yahoo.com'.to_uri.options.headers['allow']
71
+
72
+
73
+ === Attributes Container
74
+
75
+ Allows any class to hold an attributes hash, somewhat like ActiveResource. It also supports several extensions to this base fuctionality such as support for typecasting attribute values.
76
+
77
+ Example:
78
+
79
+ class Demon
80
+ include Wrest::Components::AttributesContainer
81
+ include Wrest::Components::AttributesContainer::Typecaster
53
82
 
54
- include Wrest::Components
55
- p "http://search.yahooapis.com/NewsSearchService/V1/newsSearch".to_uri.get(
56
- :appid => 'YahooDemo',
57
- :output => 'xml',
58
- :query => 'India',
59
- :results=> '3',
60
- :start => '1'
61
- ).deserialise_using(
62
- Translators::Xml
63
- ).mutate_using(
64
- Mutators::XmlSimpleTypeCaster.new
65
- )
83
+ always_has :id
84
+ typecast :age => as_integer,
85
+ :chi => lambda{|chi| Chi.new(chi)}
86
+ end
66
87
 
88
+ kai_wren = Demon.new('id => '1', 'age' => '1500', 'chi' => '1024', 'teacher' => 'Viss')
89
+ kai_wren.id # => '1'
90
+ kai_wren.age # => 1500
91
+ kai_wren.chi # => #<Chi:0x113af8c @count="1024">
92
+ kai_wren.teacher # => 'Viss'
67
93
 
68
94
  === Logging
69
95
 
@@ -80,9 +106,14 @@ Wrest RDocs can be found at http://wrest.rubyforge.org
80
106
 
81
107
  == Wrest::Resource
82
108
 
83
- Wrest::Resource is an alternative to ActiveResource. It targets Rails REST services and is currently under development.
109
+ Wrest::Resource is an alternative to ActiveResource. It targets Rails REST (well, POX - turns out Rails isn't really REST) services and is currently under development.
84
110
 
85
- * No more pretending that REST resources are the same as database records
111
+ * No more pretending that REST resources are the same as records in a database (yeah, no more freaking ActiveResource::Connection)
112
+ * Treat put as 'create or update,' not just 'update'
113
+ * Response codes result in user defined state transitions; favours state transitions based on response code over arbitrary ones
114
+ * Supports moving toward hypermedia links as opposed to client server collusion through URI templates
115
+ * The header is now exposed as metadata, rather being than something you have no control over
116
+ * Out of the box support for If-Unmodified-Since/If-Match+Etag
86
117
  * Out of the box support for collections
87
118
  * Out of the box support for collection pagination (including support for WillPaginate), both header based and xml attribute based
88
119
  * Out of the box support for operations on all the records on the collection
@@ -92,14 +123,15 @@ Wrest::Resource is an alternative to ActiveResource. It targets Rails REST servi
92
123
  * More natural mapping of deserialised entities to existing classes
93
124
  * No communication via exceptions for http error status codes
94
125
  * Better extensibility - allows access to request/response objects, avoids class variables, favours symbols over strings etc.
126
+ * Consider support for OPTIONS and response codes 100/417
95
127
 
96
128
  == Dependencies
97
129
 
98
130
  === Source
99
131
  * gems
100
- * xmlsimple
101
132
  * json (json-jruby on JRuby)
102
133
  * active_support
134
+ * ruby-libxml (Recommended, will fall back to REXML if absent; be aware that Wrest uses ActiveSupport::XmlMini, so if you're using Wrest as a plugin in a Rails application, all xml parsing across the application will switch to using libxml if it's available. You're free to change this by hand by using the ActiveSupport::XmlMini.backend= method.)
103
135
 
104
136
  === Build
105
137
  * rspec
data/Rakefile CHANGED
@@ -10,7 +10,6 @@
10
10
  require 'rubygems'
11
11
  gem 'rspec'
12
12
  require 'rake'
13
- require 'hanna/rdoctask'
14
13
  require 'spec'
15
14
  require 'spec/rake/spectask'
16
15
 
@@ -25,6 +24,12 @@ Spec::Rake::SpecTask.new(:spec) do |task|
25
24
  task.spec_opts = ['--options', 'spec/spec.opts']
26
25
  end
27
26
 
27
+ begin
28
+ require 'hanna/rdoctask'
29
+ rescue LoadError
30
+ puts 'Hanna not available, using standard Rake rdoctask. Fix this by running gem install mislav-hanna.'
31
+ require 'rake/rdoctask'
32
+ end
28
33
  desc 'Generate documentation for Wrest'
29
34
  Rake::RDocTask.new(:rdoc) do |rdoc|
30
35
  rdoc.rdoc_dir = 'rdoc'
@@ -62,12 +67,11 @@ begin
62
67
  gemspec.homepage = "http://github.com/kaiwren/wrest"
63
68
  gemspec.has_rdoc = true
64
69
  gemspec.rubyforge_project = 'wrest'
65
- gemspec.executables = ['wrest']
70
+ gemspec.executables = ['wrest', 'jwrest']
66
71
  gemspec.require_path = "lib"
67
72
  gemspec.files.exclude 'spec/wrest/meh_spec.rb'
68
73
  gemspec.test_files.exclude 'spec/wrest/meh_spec.rb'
69
- gemspec.add_dependency('activesupport', '>= 2.1.0')
70
- gemspec.add_dependency('xml-simple', '>= 1.0.11')
74
+ gemspec.add_dependency('activesupport', '>= 2.3.2')
71
75
  case RUBY_PLATFORM
72
76
  when /java/
73
77
  gemspec.add_dependency('json-jruby', '>= 1.1.3')
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
- :patch: 6
2
+ :patch: 7
3
3
  :minor: 0
4
4
  :major: 0
data/lib/wrest.rb CHANGED
@@ -7,6 +7,8 @@
7
7
  # See the License for the specific language governing permissions and limitations under the License.
8
8
 
9
9
  require 'rubygems'
10
+ gem 'activesupport', '>= 2.3.2'
11
+
10
12
  require 'net/http'
11
13
  require 'net/https'
12
14
  require 'forwardable'
@@ -19,7 +21,7 @@ require 'active_support'
19
21
 
20
22
  WREST_ROOT = File.dirname(__FILE__)
21
23
 
22
- module Wrest
24
+ module Wrest #:nodoc:
23
25
  def self.logger=(logger)
24
26
  @logger = logger
25
27
  end
@@ -29,9 +31,16 @@ module Wrest
29
31
  end
30
32
  end
31
33
 
32
- Wrest.logger = Logger.new(STDOUT)
34
+ Wrest.logger = ActiveSupport::BufferedLogger.new(STDOUT)
33
35
  Wrest.logger.level = Logger::DEBUG
34
36
 
37
+ begin
38
+ gem 'libxml-ruby', '>= 1.1.3'
39
+ ActiveSupport::XmlMini.backend='LibXML'
40
+ rescue Gem::LoadError
41
+ Wrest.logger.warn "LibXML >= 1.1.3 not found, falling back to #{ActiveSupport::XmlMini.backend}. To install LibXML run `sudo gem install ruby-libxml`"
42
+ end
43
+
35
44
  source_dirs = ["/wrest/core_ext/*.rb", "/wrest/*.rb"]
36
45
 
37
46
  source_dirs.each{|directory|
@@ -7,7 +7,7 @@
7
7
  # is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8
8
  # See the License for the specific language governing permissions and limitations under the License.
9
9
 
10
- module Wrest #:nodoc:
10
+ module Wrest
11
11
  # A component is a building block that can
12
12
  # be used while building an object oriented wrapper
13
13
  # around a REST service
@@ -15,7 +15,6 @@ module Wrest #:nodoc:
15
15
  end
16
16
  end
17
17
 
18
- require "#{WREST_ROOT}/wrest/components/typecast_helpers"
19
18
  require "#{WREST_ROOT}/wrest/components/attributes_container"
20
19
  require "#{WREST_ROOT}/wrest/components/mutators"
21
20
  require "#{WREST_ROOT}/wrest/components/translators"
@@ -7,16 +7,23 @@
7
7
  # is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8
8
  # See the License for the specific language governing permissions and limitations under the License.
9
9
 
10
- module Wrest::Components #:nodoc:
10
+ module Wrest
11
+ module Components::AttributesContainer
12
+ end
13
+ end
14
+
15
+ require "#{WREST_ROOT}/wrest/components/attributes_container/typecaster"
16
+
17
+ module Wrest::Components
11
18
 
12
19
  # Adds behaviour allowing a class to
13
20
  # contain attributes and providing support
14
21
  # for dynamic getters, setters and query methods.
15
22
  # These methods are added at runtime, on the first
16
- # invocation and on a per instance basis.
17
- # <tt>respond_to?</tt> however will respond as though
23
+ # invocation and on a per instance basis.
24
+ # <tt>respond_to?</tt> however will respond as though
18
25
  # they are all already present.
19
- # This means that two different instances of the same
26
+ # This means that two different instances of the same
20
27
  # AttributesContainer could well have
21
28
  # different attribute getters/setters/query methods.
22
29
  #
@@ -30,99 +37,60 @@ module Wrest::Components #:nodoc:
30
37
  # In situations where this is a problem, such as a client consuming Rails
31
38
  # REST services where <tt>id</tt> is a common attribute and clashes with
32
39
  # Object#id, it is recommended to create getter/setter/query methods
33
- # on the class (which affects all instances) using the +has_attributes+ macro.
40
+ # on the class (which affects all instances) using the +always_has+ macro.
34
41
  #
35
42
  # If you're implementing your own initialize method
36
- # remember to delegate to the default initialize
43
+ # remember to delegate to the default initialize
37
44
  # of AttributesContainer by invoking <tt>super(attributes)</tt>
38
45
  #
39
46
  # Example:
40
47
  # class ShenCoin
41
48
  # include Wrest::Components::AttributesContainer
49
+ # include Wrest::Components::AttributesContainer::Typecaster
42
50
  #
43
- # has_attributes :id
44
- # typecast :id => as_integer
45
- # end
51
+ # always_has :id
52
+ # typecast :id => as_integer
53
+ # end
46
54
  # coin = ShenCoin.new(:id => '5', :chi_count => 500, :owner => 'Kai Wren')
47
55
  # coin.id # => 5
48
56
  # coin.owner # => 'Kai Wren'
49
57
  module AttributesContainer
50
58
  def self.included(klass) #:nodoc:
51
59
  klass.extend AttributesContainer::ClassMethods
52
- klass.extend TypecastHelpers
53
60
  klass.class_eval{ include AttributesContainer::InstanceMethods }
54
61
  end
55
-
62
+
56
63
  def self.build_attribute_getter(attribute_name) #:nodoc:
57
64
  "def #{attribute_name};@attributes[:#{attribute_name}];end;"
58
65
  end
59
-
66
+
60
67
  def self.build_attribute_setter(attribute_name) #:nodoc:
61
68
  "def #{attribute_name}=(value);@attributes[:#{attribute_name}] = value;end;"
62
69
  end
63
-
70
+
64
71
  def self.build_attribute_queryer(attribute_name) #:nodoc:
65
72
  "def #{attribute_name}?;not @attributes[:#{attribute_name}].nil?;end;"
66
73
  end
67
-
74
+
68
75
  module ClassMethods
69
76
  # This macro explicitly creates getter, setter and query methods on
70
- # a class, overriding any exisiting methods with the same names.
77
+ # a class, overriding any exisiting methods with the same names.
71
78
  # This can be used when attribute names clash with method names;
72
79
  # an example would be Rails REST services which frequently make use
73
80
  # an attribute named <tt>id</tt> which clashes with Object#id. Also,
74
81
  # this can be used as a performance optimisation if the incoming
75
82
  # attributes are known beforehand.
76
- def has_attributes(*attribute_names)
83
+ def always_has(*attribute_names)
77
84
  attribute_names.each do |attribute_name|
78
85
  self.class_eval(
79
- AttributesContainer.build_attribute_getter(attribute_name) +
80
- AttributesContainer.build_attribute_setter(attribute_name) +
81
- AttributesContainer.build_attribute_queryer(attribute_name)
82
- )
86
+ AttributesContainer.build_attribute_getter(attribute_name) +
87
+ AttributesContainer.build_attribute_setter(attribute_name) +
88
+ AttributesContainer.build_attribute_queryer(attribute_name)
89
+ )
83
90
  end
84
91
  end
85
-
86
- # Accepts a set of attribute-name/lambda pairs which are used
87
- # to typecast string values injected through the constructor.
88
- # Typically needed when populating an +AttributesContainer+
89
- # directly from request params. Typecasting kicks in for
90
- # a given value _only_ if it is a string.
91
- #
92
- # Typcast information is inherited by subclasses; however be
93
- # aware that explicitly invoking +typecast+ in a subclass will
94
- # discard inherited typecast information leaving only the casts
95
- # defined in the subclass.
96
- #
97
- # Common typecasts such as integer, float, datetime etc. are
98
- # available through predefined helpers. See TypecastHelpers
99
- # for a full list.
100
- #
101
- # Example:
102
- #
103
- # class Demon
104
- # include Wrest::Components::AttributesContainer
105
- # typecast :age => as_integer,
106
- # :chi => lambda{|chi| Chi.new(chi)}
107
- # end
108
- # kai_wren = Demon.new('age' => '1500', 'chi' => '1024')
109
- # kai_wren.age # => 1500
110
- # kai_wren.chi # => #<Chi:0x113af8c @count="1024">
111
- def typecast(cast_map)
112
- @typecast_map = @typecast_map ? @typecast_map.merge(cast_map.symbolize_keys) : cast_map.symbolize_keys
113
- end
114
-
115
- def typecast_map #:nodoc:
116
- if defined?(@typecast_map)
117
- @typecast_map
118
- elsif superclass != Object && superclass.respond_to?(:typecast_map)
119
- superclass.typecast_map
120
- else
121
- {}
122
- end
123
- end
124
92
  end
125
-
93
+
126
94
  module InstanceMethods
127
95
  # Sets up any class to act like
128
96
  # an attributes container by creating
@@ -132,22 +100,18 @@ module Wrest::Components #:nodoc:
132
100
  # own class.
133
101
  def initialize(attributes = {})
134
102
  @attributes = attributes.symbolize_keys
135
- self.class.typecast_map.each do |key, typecaster|
136
- value = @attributes[key]
137
- @attributes[key] = typecaster.call(value) if value.is_a?(String)
138
- end
139
103
  @interface = Module.new
140
104
  self.extend @interface
141
105
  end
142
-
106
+
143
107
  def [](key)
144
108
  @attributes[key.to_sym]
145
109
  end
146
-
110
+
147
111
  def []=(key, value)
148
112
  @attributes[key.to_sym] = value
149
113
  end
150
-
114
+
151
115
  def respond_to?(method_name, include_private = false)
152
116
  super(method_name, include_private) ? true : @attributes.include?(method_name.to_s.gsub(/(\?$)|(=$)/, '').to_sym)
153
117
  end
@@ -157,7 +121,6 @@ module Wrest::Components #:nodoc:
157
121
  def method_missing(method_sym, *arguments)
158
122
  method_name = method_sym.to_s
159
123
  attribute_name = method_name.gsub(/(\?$)|(=$)/, '')
160
-
161
124
  if @attributes.include?(attribute_name.to_sym) || method_name.last == '='
162
125
  case method_name.last
163
126
  when '='
@@ -172,7 +135,6 @@ module Wrest::Components #:nodoc:
172
135
  super(method_sym, *arguments)
173
136
  end
174
137
  end
175
-
176
138
  end
177
139
  end
178
140
  end
@@ -0,0 +1,121 @@
1
+ # Copyright 2009 Sidu Ponnappa
2
+
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ # Unless required by applicable law or agreed to in writing, software distributed under the License
7
+ # is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8
+ # See the License for the specific language governing permissions and limitations under the License.
9
+
10
+ module Wrest
11
+ module Components::AttributesContainer
12
+ # An extension to AttributesContainer that adds support for
13
+ # specifying how the values associated with certain attribute keys
14
+ # should be typecast.
15
+ #
16
+ # This extension can be used in situations where the attributes
17
+ # hash consists of just strings with no associated tup information.
18
+ # For example, params recieved from a web browser may contain
19
+ # attributes like
20
+ # 'id' => '4', 'dateofbirth' => '1984-04-05'
21
+ # and we'd like to have these cast to an integer and a date
22
+ # respectively, rather than have to deal with them as strings.
23
+ module Typecaster
24
+ def self.included(klass) #:nodoc:
25
+ klass.extend Typecaster::ClassMethods
26
+ klass.class_eval{ include Typecaster::InstanceMethods }
27
+ klass.alias_method_chain :initialize, :typecasting
28
+ end
29
+
30
+ module ClassMethods
31
+ # Accepts a set of attribute-name/lambda pairs which are used
32
+ # to typecast string values injected through the constructor.
33
+ # Typically needed when populating an +AttributesContainer+
34
+ # directly from request params. Typecasting kicks in for
35
+ # a given value _only_ if it is a string.
36
+ #
37
+ # Typecast information is inherited by subclasses; however be
38
+ # aware that explicitly invoking +typecast+ in a subclass will
39
+ # discard inherited typecast information leaving only the casts
40
+ # defined in the subclass.
41
+ #
42
+ # Common typecasts such as integer, float, datetime etc. are
43
+ # available through predefined helpers. See TypecastHelpers
44
+ # for a full list.
45
+ #
46
+ # Example:
47
+ #
48
+ # class Demon
49
+ # include Wrest::Components::AttributesContainer
50
+ # include Wrest::Components::AttributesContainer::Typecaster
51
+ #
52
+ # typecast :age => as_integer,
53
+ # :chi => lambda{|chi| Chi.new(chi)}
54
+ # end
55
+ #
56
+ # kai_wren = Demon.new('age' => '1500', 'chi' => '1024')
57
+ # kai_wren.age # => 1500
58
+ # kai_wren.chi # => #<Chi:0x113af8c @count="1024">
59
+ def typecast(cast_map)
60
+ @typecast_map = @typecast_map ? @typecast_map.merge(cast_map.symbolize_keys) : cast_map.symbolize_keys
61
+ end
62
+
63
+ def typecast_map #:nodoc:
64
+ if defined?(@typecast_map)
65
+ @typecast_map
66
+ elsif superclass != Object && superclass.respond_to?(:typecast_map)
67
+ superclass.typecast_map
68
+ else
69
+ {}
70
+ end
71
+ end
72
+
73
+ def as_base64Binary
74
+ ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING['base64Binary']
75
+ end
76
+
77
+ def as_boolean
78
+ ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING['boolean']
79
+ end
80
+
81
+ def as_decimal
82
+ ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING['decimal']
83
+ end
84
+
85
+ def as_date
86
+ ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING['date']
87
+ end
88
+
89
+ def as_datetime
90
+ ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING['datetime']
91
+ end
92
+
93
+ def as_float
94
+ ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING['float']
95
+ end
96
+
97
+ def as_integer
98
+ ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING['integer']
99
+ end
100
+
101
+ def as_symbol
102
+ ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING['symbol']
103
+ end
104
+
105
+ def as_yaml
106
+ ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING['yaml']
107
+ end
108
+ end
109
+
110
+ module InstanceMethods # :nodoc:
111
+ def initialize_with_typecasting(attributes = {}) # :nodoc:
112
+ initialize_without_typecasting(attributes)
113
+ self.class.typecast_map.each do |key, typecaster|
114
+ value = @attributes[key]
115
+ @attributes[key] = typecaster.call(value) if value.is_a?(String)
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end