riakrest 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. data/History.txt +4 -0
  2. data/Manifest.txt +41 -0
  3. data/PostInstall.txt +2 -0
  4. data/README.rdoc +51 -0
  5. data/Rakefile +24 -0
  6. data/examples/auto_update_data.rb +50 -0
  7. data/examples/auto_update_links.rb +48 -0
  8. data/examples/basic_client.rb +33 -0
  9. data/examples/basic_resource.rb +34 -0
  10. data/examples/json_data_resource.rb +32 -0
  11. data/examples/linked_resource.rb +113 -0
  12. data/examples/multiple_resources.rb +43 -0
  13. data/lib/riakrest/core/exceptions.rb +73 -0
  14. data/lib/riakrest/core/jiak_bucket.rb +146 -0
  15. data/lib/riakrest/core/jiak_client.rb +316 -0
  16. data/lib/riakrest/core/jiak_data.rb +265 -0
  17. data/lib/riakrest/core/jiak_link.rb +131 -0
  18. data/lib/riakrest/core/jiak_object.rb +233 -0
  19. data/lib/riakrest/core/jiak_schema.rb +242 -0
  20. data/lib/riakrest/core/query_link.rb +156 -0
  21. data/lib/riakrest/data/jiak_data_hash.rb +182 -0
  22. data/lib/riakrest/resource/jiak_resource.rb +628 -0
  23. data/lib/riakrest/version.rb +7 -0
  24. data/lib/riakrest.rb +164 -0
  25. data/riakrest.gemspec +38 -0
  26. data/script/console +10 -0
  27. data/script/destroy +14 -0
  28. data/script/generate +14 -0
  29. data/spec/core/exceptions_spec.rb +18 -0
  30. data/spec/core/jiak_bucket_spec.rb +103 -0
  31. data/spec/core/jiak_client_spec.rb +358 -0
  32. data/spec/core/jiak_link_spec.rb +77 -0
  33. data/spec/core/jiak_object_spec.rb +210 -0
  34. data/spec/core/jiak_schema_spec.rb +184 -0
  35. data/spec/core/query_link_spec.rb +128 -0
  36. data/spec/data/jiak_data_hash_spec.rb +14 -0
  37. data/spec/resource/jiak_resource_spec.rb +128 -0
  38. data/spec/riakrest_spec.rb +17 -0
  39. data/spec/spec.opts +5 -0
  40. data/spec/spec_helper.rb +12 -0
  41. data/tasks/rspec.rake +21 -0
  42. metadata +113 -0
@@ -0,0 +1,265 @@
1
+ module RiakRest
2
+
3
+ # All end-user data stored via RiakRest is contained in user-defined data
4
+ # objects. To make a user-defined data object, include module JiakData into
5
+ # your class definition. This allows creating a class that can be used to
6
+ # create instances of your user-defined data. Note JiakData does create
7
+ # user-data instances, rather it facilitates creating the class you use to
8
+ # create user-data instances.
9
+ #
10
+ # The four class methods <code>allowed, required, readable, writable</code>
11
+ # defined in JiakData#ClassMethods are used to declare the schema for
12
+ # structured Jiak interaction with user-defined data. The <code>allowed</code>
13
+ # method is mandatory; the other methods take the defaults as described in
14
+ # JiakSchema. See JiakSchema for discussion on structured data interaction.
15
+ #
16
+ # User-defined data classes must override JiakData#ClassMethods#jiak_create
17
+ # (creating your user-defined data from the information returned by Jiak) and
18
+ # JiakData#for_jiak (providing the information to be sent to Jiak). The
19
+ # default implementations of these methods throw JiakDataException to
20
+ # enforce this override.
21
+ #
22
+ # JiakData provides a default data key generator that returns nil, which
23
+ # instructs the Jiak server to generate a random key on first data
24
+ # storage. To explicitly set the key override JiakData#keygen to return
25
+ # whatever string you want to use for the key. Keys need to be unique within
26
+ # each bucket on the Jiak server but can be the same across distinct buckets.
27
+ #
28
+ # ===Example
29
+ # <code>
30
+ # class FooBarBaz
31
+ # include JiakData
32
+ #
33
+ # allowed :foo, :bar, :baz
34
+ # required :foo
35
+ # readable :foo, :bar
36
+ # writable :foo, :baz
37
+ #
38
+ # def initialize(foo,bar,baz)
39
+ # @foo = foo
40
+ # @bar = bar
41
+ # @baz = baz
42
+ # end
43
+ #
44
+ # def self.jiak_create(jiak)
45
+ # new(jiak['foo'],jiak['bar'])
46
+ # end
47
+ #
48
+ # def for_jiak
49
+ # { :foo => @foo,
50
+ # :baz => @baz
51
+ # }
52
+ # end
53
+ #
54
+ # def keygen
55
+ # "#{foo}"
56
+ # end
57
+ # end
58
+ # </code>
59
+ #
60
+ # Note that FooBarBaz <code>bar</code> is readable but not writable and
61
+ # <code>baz</code> is writable but not readable. Also note
62
+ # <code>for_jiak</code> only provides the <code>writable</code> fields for
63
+ # writing to the Jiak server and <code>jiak_create</code> only initializes
64
+ # from the <code>readable</code> fields returned by the Jiak server. The
65
+ # above definition means a user of FooBarBaz could change <code>baz</code>
66
+ # but not see that change and could access <code>bar</code> but not change
67
+ # it. This could be useful if either another JiakData class (with a different
68
+ # schema) created access into the same data, allowing <code>bar</code> writes
69
+ # and <code>baz</code> reads, or if Riak server-side manipulation affected
70
+ # those fields. The constraints declared in FooBarBaz simply provide
71
+ # a particular structured interaction of data on a Jiak server.
72
+ #
73
+ # If only one JiakData will be used for a particular type of data on the Jiak
74
+ # server it is desirable to have the <code>readable</code> and
75
+ # <code>writable</code> fields be the same as <code>allowed</code>. Setting
76
+ # only <code>allowed</code> fields provide this reasonable default, hence only
77
+ # that call is mandatory.
78
+ module JiakData
79
+
80
+ # ----------------------------------------------------------------------
81
+ # Class methods
82
+ # ----------------------------------------------------------------------
83
+
84
+ # Class methods for use in creating a user-defined JiakData. The methods
85
+ # <code>allowed, required, readable, writable</code> define the JiakSchema
86
+ # for this JiakData. See JiakSchema for discussion on the use of schemas in
87
+ # Riak.
88
+ module ClassMethods
89
+
90
+ # :call-seq:
91
+ # allowed :f1, ..., :fn -> array
92
+ #
93
+ # Fields allowed in Jiak interactions. Returns an array of the allowed
94
+ # fields.
95
+ #
96
+ def allowed(*fields)
97
+ arr_fields = create_array(fields)
98
+ fields.each {|field| attr_accessor field}
99
+ @schema = JiakSchema.new(arr_fields)
100
+ arr_fields
101
+ end
102
+
103
+ # :call-seq:
104
+ # required :f1, ..., :fn -> array
105
+ #
106
+ # Fields required during in Jiak interactions. Returns an array of the
107
+ # required fields.
108
+ #
109
+ def required(*fields)
110
+ set_fields('required_fields',*fields)
111
+ end
112
+
113
+ # :call-seq:
114
+ # readable :f1, ..., :fn -> array
115
+ #
116
+ # Fields returned by Jiak on retrieval. Returns an array of the fields in
117
+ # the read mask.
118
+ #
119
+ def readable(*fields)
120
+ set_fields('read_mask',*fields)
121
+ end
122
+
123
+ # :call-seq:
124
+ # writable :f1, ..., :fn -> arry
125
+ #
126
+ # Fields that can be written during Jiak interaction. Returns an array of
127
+ # the fields in the write mask.
128
+ #
129
+ def writable(*fields)
130
+ set_fields('write_mask',*fields)
131
+ end
132
+
133
+ # :call-seq:
134
+ # readwrite :f1, ..., :fn -> array
135
+ #
136
+ # Set the read and write masks to the same fields. Returns an array of
137
+ # the fields in the masks.
138
+ def readwrite(*fields)
139
+ arr_fields = set_fields('readwrite',*fields)
140
+ arr_fields
141
+ end
142
+
143
+ def set_fields(which,*fields)
144
+ arr_fields = create_array(fields)
145
+ check_allowed(arr_fields)
146
+ @schema.send("#{which}=",arr_fields)
147
+ arr_fields
148
+ end
149
+ private :set_fields
150
+
151
+ # :call-seq:
152
+ # JiakData.schema -> JiakSchema
153
+ #
154
+ # Get a JiakSchema representation determined by
155
+ # <code>allowed, required, readable, writable</code>.
156
+ #
157
+ def schema
158
+ @schema
159
+ end
160
+
161
+ # :call-seq:
162
+ # JiakData.jiak_create(jiak) -> JiakData
163
+ #
164
+ # Create an instance of user-defined data object from the fields read
165
+ # by Jiak. These fields are determined by the read mask of the
166
+ # structured Jiak interaction. See JiakSchema for read mask discussion.
167
+ #
168
+ # User-defined data classes must override this method. The method is
169
+ # called during the creation of a JiakObject from information returned by
170
+ # Jiak. The JiakObject contains the user-defined data itself. You do not
171
+ # call this method explicitly.
172
+ #
173
+ # ====Example
174
+ # <code>
175
+ # def initialize(f1,f2)
176
+ # @f1 = f1
177
+ # @f2 = f2
178
+ # end
179
+ # def jiak_create(jiak)
180
+ # new(jiak['f1'], jiak['f2'])
181
+ # end
182
+ # </code>
183
+ #
184
+ # Raise JiakDataException if not explicitly defined by user-defined data class.
185
+ def jiak_create(json)
186
+ raise JiakDataException, "#{self} must define jiak_create"
187
+ end
188
+
189
+ def create_array(*fields)
190
+ if(fields.size == 1 && fields[0].is_a?(Array))
191
+ array = fields[0]
192
+ else
193
+ array = fields
194
+ end
195
+ array.map {|field| field}
196
+ array
197
+ end
198
+ private :create_array
199
+
200
+ def check_allowed(fields)
201
+ allowed_fields = @schema.allowed_fields
202
+ fields.each do |field|
203
+ unless allowed_fields.include?(field)
204
+ raise JiakDataException, "field '#{field}' not allowed"
205
+ end
206
+ end
207
+ end
208
+ private :check_allowed
209
+ end
210
+
211
+ def self.included(including_class) # :nodoc:
212
+ including_class.extend(ClassMethods)
213
+ end
214
+
215
+ # ----------------------------------------------------------------------
216
+ # Instance methods
217
+ # ----------------------------------------------------------------------
218
+
219
+ # :call-seq:
220
+ # for_jiak -> hash
221
+ #
222
+ # Provide a hash structure of the data to write to Jiak. The fields for
223
+ # this structure should come from the write mask of the structured Jiak
224
+ # interaction. See JiakSchema for write mask discussion.
225
+ #
226
+ # User-defined data classes must override this method. The method is called
227
+ # during the creation of a JiakObject to send information to Jiak. The
228
+ # JiakObject contains the user-defined data itself. You do not call this
229
+ # method explicitly.
230
+ #
231
+ # ====Example
232
+ # <code>
233
+ # def for_jiak
234
+ # { :writable_f1 => @writable_f1,
235
+ # :writable_f2 => @writable_f2
236
+ # }
237
+ # end
238
+ # </code>
239
+ #
240
+ # Raise JiakDataException if not explicitly defined by user-defined data class.
241
+ def for_jiak
242
+ raise JiakDataException, "#{self} must define for_jiak"
243
+ end
244
+
245
+ # :call-seq:
246
+ # keygen -> string
247
+ #
248
+ # Generate Jiak key for data. Default implementation returns
249
+ # <code>nil</code> which instructs the Jiak server to generate a random
250
+ # key. Override for user-defined data behaviour.
251
+ #
252
+ # ====Example
253
+ # A simple implementation would look like:
254
+ # <code>
255
+ # def keygen
256
+ # f1.to_s
257
+ # end
258
+ # </code>
259
+ def keygen
260
+ nil
261
+ end
262
+
263
+ end
264
+ end
265
+
@@ -0,0 +1,131 @@
1
+ module RiakRest
2
+
3
+ # Represents a link between object in Jiak. JiakLinks are used to link a
4
+ # JiakObject to another JiakObject at a bucket/key. The tag allows the link
5
+ # to be traversed later using a QueryLink.
6
+ #
7
+ # ===Usage
8
+ # <code>
9
+ # link = JiakLink.new('people','callie','sister')
10
+ # </code>
11
+ # If the above link were added to a JiakObject in the same bucket and keyed
12
+ # by the string 'remy', the link from remy to the sister callie would be
13
+ # retrieve using JiakClient#walk and the query link
14
+ # <code>QueryLink.new('people','sister')</code>
15
+ #
16
+ class JiakLink
17
+
18
+ attr_accessor :bucket, :key, :tag
19
+
20
+ # :call-seq:
21
+ # JiakLink.new(bucket,key,tag) -> JiakLink
22
+ # JiakLink.new([bucket,key,tag]) -> JiakLink
23
+ #
24
+ # Create a link (designated by tag) to an object at bucket/key. Bucket can
25
+ # be either a JiakBucket or a string bucket name; key and tag must both be
26
+ # strings.
27
+ #
28
+ def initialize(*args)
29
+ case args.size
30
+ when 1
31
+ if args[0].is_a? Array
32
+ bucket, key, tag = args[0][0], args[0][1], args[0][2]
33
+ elsif args[0].is_a? JiakLink
34
+ bucket, key, tag = args[0].bucket, args[0].key, args[0].tag
35
+ else
36
+ raise JiakLinkException, "argument error"
37
+ end
38
+ when 3
39
+ bucket, key, tag = args[0], args[1], args[2]
40
+ else
41
+ raise JiakLinkException, "argument error"
42
+ end
43
+
44
+ @bucket, @key, @tag = transform_args(bucket,key,tag)
45
+ end
46
+
47
+ # :call-seq:
48
+ # link.bucket = bucket
49
+ #
50
+ # Set the bucket field.
51
+ def bucket=(bucket)
52
+ bucket = bucket.name if bucket.is_a? JiakBucket
53
+ @bucket = transform_arg(bucket)
54
+ end
55
+
56
+ # :call-seq:
57
+ # link.key = key
58
+ #
59
+ # Set the key field.
60
+ def key=(key)
61
+ @key = transform_arg(key)
62
+ end
63
+
64
+ # :call-seq:
65
+ # link.tag = tag
66
+ #
67
+ # Set the tag field.
68
+ def tag=(tag)
69
+ @tag = transform_arg(tag)
70
+ end
71
+
72
+ # :call-seq:
73
+ # link.for_jiak -> JSON
74
+ #
75
+ # Representation of this JiakLink for transport to Jiak.
76
+ def for_jiak
77
+ [@bucket, @key, @tag]
78
+ end
79
+
80
+ # :call-seq:
81
+ # link == other -> true or false
82
+ #
83
+ # Equality -- JiakLinks are equal if they contain the same attribute values.
84
+ def ==(other)
85
+ (@bucket == other.bucket &&
86
+ @key == other.key &&
87
+ @tag == other.tag) rescue false
88
+ end
89
+
90
+ # :call-seq:
91
+ # eql?(other) -> true or false
92
+ #
93
+ # Returns <code>true</code> if <code>other</code> is a JiakLink with the
94
+ # same attribute values.
95
+ def eql?(other)
96
+ other.is_a?(JiakLink) &&
97
+ @bucket.eql?(other.bucket) &&
98
+ @key.eql?(other.key) &&
99
+ @tag.eql?(other.tag)
100
+ end
101
+
102
+ def hash # :nodoc:
103
+ @bucket.hash + @key.hash + @tag.hash
104
+ end
105
+
106
+ # String representation of this JiakLink.
107
+ def to_s
108
+ "[#{bucket},#{key},#{tag}]"
109
+ end
110
+
111
+ def transform_args(b,k,t)
112
+ b = b.name if b.is_a? JiakBucket
113
+ [transform_arg(b),transform_arg(k),transform_arg(t)]
114
+ end
115
+ private :transform_args
116
+
117
+ def transform_arg(arg)
118
+ unless arg.is_a? String
119
+ raise JiakLinkException, "Link elements must be Strings."
120
+ end
121
+ value = arg.dup
122
+ value.strip!
123
+ if value.empty?
124
+ raise JiakLinkException, "Link elements can't be empty."
125
+ end
126
+ value
127
+ end
128
+ private :transform_arg
129
+
130
+ end
131
+ end
@@ -0,0 +1,233 @@
1
+ module RiakRest
2
+
3
+ # Wrapper for JiakData.
4
+ class JiakObject
5
+
6
+ attr_accessor :bucket, :key, :data, :links, :riak
7
+
8
+ # :call-seq:
9
+ # JiakObject.new(opts) -> JiakObject
10
+ #
11
+ # Create a object for Jiak storage. Valid options:
12
+ # <code>:bucket</code> :: JiakBucket for storage.
13
+ # <code>:data</code> :: Object JiakData to be stored.
14
+ # <code>:key</code> :: Object key.
15
+ # <code>:links</code> :: Object JiakLink array.
16
+ #
17
+ # The bucket and data options are required.
18
+ #
19
+ # The key and links options are optional. If no key is provided, the
20
+ # <code>keygen</code> method of <code>data</code> is used to provide the
21
+ # key. The default implementation of JiakData#keygen is an empty string,
22
+ # which instructs the Jiak server to generate a random key. If no links are
23
+ # provided, the default uses an empty array.
24
+ #
25
+ # There are other options used by the system to maintain the context of the
26
+ # JiakObject on the Riak cluster. These options should not be manually
27
+ # altered and are purposely not described here.
28
+ def initialize(opts)
29
+ opts[:links] ||= []
30
+ check_opts(opts)
31
+
32
+ @bucket = check_bucket(opts[:bucket])
33
+ @data = check_data(opts[:data])
34
+ @key = transform_key(opts[:key] || @data.keygen)
35
+ @links = check_links(opts[:links])
36
+
37
+ # The Riak context for the object if provided.
38
+ if opts[:vclock]
39
+ @riak = opts.select {|k,v| [:vclock,:vtag,:lastmod].include?(k)}
40
+ end
41
+ end
42
+
43
+ # :call-seq:
44
+ # JiakObject.from_jiak(jiak) -> JiakObject
45
+ #
46
+ # Create a JiakObject from parsed JSON returned by the Jiak server. Calls
47
+ # the <code>jiak_create</code> of the JiakData class passed as the second
48
+ # argument to inflate the data into the user-defined data class.
49
+ def self.from_jiak(jiak,klass)
50
+ jiak[:bucket] = JiakBucket.new(jiak.delete('bucket'),klass)
51
+ jiak[:data] = klass.jiak_create(jiak.delete('object'))
52
+ jiak[:links] = jiak.delete('links').map {|link| JiakLink.new(link)}
53
+
54
+ new(jiak.inject({}) do |build, (key, value)|
55
+ build[key.to_sym] = value
56
+ build
57
+ end)
58
+ end
59
+
60
+ # :call-seq:
61
+ # jiak_object.to_jiak -> JSON
62
+ #
63
+ # Create a representation suitable for sending to a Jiak server. Calls the
64
+ # <code>for_jiak</code> method of the wrapped JiakData. Called by
65
+ # JiakClient when transporting an object to Jiak.
66
+ def to_jiak
67
+ jiak = {
68
+ :bucket => @bucket.name,
69
+ :key => @key,
70
+ :object => @data.for_jiak,
71
+ :links => @links.map {|link| link.for_jiak}
72
+ }
73
+ if(@riak)
74
+ jiak[:vclock] = @riak[:vclock]
75
+ jiak[:vtag] = @riak[:vtag]
76
+ jiak[:lastmod] = @riak[:lastmod]
77
+ end
78
+ jiak.to_json
79
+ end
80
+
81
+ # :call-seq:
82
+ # jiak_object.bucket = bucket
83
+ #
84
+ # Set the bucket for a JiakObject. Bucket must be a JiakBucket.
85
+ def bucket=(bucket)
86
+ @bucket = check_bucket(bucket)
87
+ end
88
+
89
+ # :call-seq:
90
+ # jiak_object.key = string or nil
91
+ #
92
+ # Set the key for a JiakObject. Key string is stripped of leading and
93
+ # trailing blanks. A nil key is interpreted as an empty string.
94
+ def key=(key)
95
+ @key = transform_key(key)
96
+ end
97
+
98
+ # :call-seq:
99
+ # jiak_object.data = {}
100
+ #
101
+ # Set the data wrapped by a JiakObject. The data must be a JiakData object.
102
+ def data=(data)
103
+ @data = check_data(data)
104
+ end
105
+
106
+ # :call-seq:
107
+ # jiak_object.links = []
108
+ #
109
+ # Set the links array for JiakObject. Each array element must be a
110
+ # JiakLink.
111
+ def links=(links)
112
+ @links = check_links(links)
113
+ end
114
+
115
+ # :call-seq:
116
+ # jiak_object << JiakLink -> JiakObject
117
+ #
118
+ # Convenience method for adding a JiakLink to the links for a
119
+ # JiakObject. Duplicate links are ignored. Returns the JiakObject for
120
+ # chaining.
121
+ def <<(link)
122
+ link = check_link(link)
123
+ @links << link unless @links.include?(link)
124
+ self
125
+ end
126
+
127
+ # :call-seq:
128
+ # jiak_object.riak = riak
129
+ #
130
+ # Set the Riak context for a JiakObject.
131
+ def riak=(riak)
132
+ @riak = check_riak(riak)
133
+ end
134
+
135
+ # :call-seq:
136
+ # jiak_object == other -> true or false
137
+ #
138
+ # Equality -- Two JiakObjects are equal if they contain the same values
139
+ # for all attributes.
140
+ def ==(other)
141
+ (@bucket == other.bucket &&
142
+ @key == other.key &&
143
+ @data == other.data &&
144
+ @links == other.links
145
+ ) rescue false
146
+ end
147
+
148
+ # :call-seq:
149
+ # jiak_object.eql?(other) -> true or false
150
+ #
151
+ # Returns <code>true</code> if <code>other</code> is a JiakObject with the
152
+ # same the same attribute values.
153
+ def eql?(other)
154
+ other.is_a?(JiakObject) &&
155
+ @bucket.eql?(other.bucket) &&
156
+ @key.eql?(other.key) &&
157
+ @data.eql?(other.data) &&
158
+ @links.eql?(other.links)
159
+ end
160
+
161
+ def hash # :nodoc:
162
+ @bucket.name.hash + @key.hash + @data.hash + @links.hash + @riak.hash
163
+ end
164
+
165
+ def check_opts(opts)
166
+ err = opts.select {|k,v| !VALID_OPTS.include?(k)}
167
+ unless err.empty?
168
+ raise JiakObjectException, "unrecognized options: #{err.keys}"
169
+ end
170
+ opts
171
+ end
172
+ private :check_opts
173
+
174
+ def check_bucket(bucket)
175
+ unless bucket.is_a?(JiakBucket)
176
+ raise JiakObjectException, "Bucket must be a JiakBucket."
177
+ end
178
+ bucket
179
+ end
180
+ private :check_bucket
181
+
182
+ def transform_key(key)
183
+ # Change nil key to empty
184
+ o_key = key.nil? ? '' : key.dup
185
+ unless o_key.is_a?(String)
186
+ raise JiakObjectException, "Key must be a String"
187
+ end
188
+ o_key.strip!
189
+ o_key
190
+ end
191
+ private :transform_key
192
+
193
+ def check_data(data)
194
+ unless data.is_a?(JiakData)
195
+ raise JiakObjectException, "Data must be a JiakData."
196
+ end
197
+ data
198
+ end
199
+ private :check_data
200
+
201
+ def check_links(links)
202
+ unless links.is_a? Array
203
+ raise JiakObjectException, "Links must be an array"
204
+ end
205
+ links.each do |link|
206
+ check_link(link)
207
+ end
208
+ links
209
+ end
210
+ private :check_links
211
+
212
+ def check_link(link)
213
+ unless link.is_a? JiakLink
214
+ raise JiakObjectException, "Each link must be a JiakLink"
215
+ end
216
+ link
217
+ end
218
+ private :check_link
219
+
220
+ def check_riak(riak)
221
+ err = riak.select {|k,v| !VALID_RIAK.include?(k)}
222
+ unless err.empty?
223
+ raise JiakObjectException, "unrecognized options: #{err.keys}"
224
+ end
225
+ riak
226
+ end
227
+
228
+ private
229
+ VALID_RIAK = [:vclock,:vtag,:lastmod]
230
+ VALID_OPTS = [:bucket,:key,:data,:links] + VALID_RIAK
231
+ end
232
+
233
+ end