tpitale-mongo_mapper 0.6.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. data/.gitignore +10 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +53 -0
  4. data/Rakefile +55 -0
  5. data/VERSION +1 -0
  6. data/bin/mmconsole +60 -0
  7. data/lib/mongo_mapper/associations/base.rb +110 -0
  8. data/lib/mongo_mapper/associations/belongs_to_polymorphic_proxy.rb +26 -0
  9. data/lib/mongo_mapper/associations/belongs_to_proxy.rb +21 -0
  10. data/lib/mongo_mapper/associations/collection.rb +19 -0
  11. data/lib/mongo_mapper/associations/many_documents_as_proxy.rb +26 -0
  12. data/lib/mongo_mapper/associations/many_documents_proxy.rb +115 -0
  13. data/lib/mongo_mapper/associations/many_embedded_polymorphic_proxy.rb +31 -0
  14. data/lib/mongo_mapper/associations/many_embedded_proxy.rb +54 -0
  15. data/lib/mongo_mapper/associations/many_polymorphic_proxy.rb +11 -0
  16. data/lib/mongo_mapper/associations/proxy.rb +113 -0
  17. data/lib/mongo_mapper/associations.rb +70 -0
  18. data/lib/mongo_mapper/callbacks.rb +109 -0
  19. data/lib/mongo_mapper/dirty.rb +136 -0
  20. data/lib/mongo_mapper/document.rb +472 -0
  21. data/lib/mongo_mapper/dynamic_finder.rb +74 -0
  22. data/lib/mongo_mapper/embedded_document.rb +384 -0
  23. data/lib/mongo_mapper/finder_options.rb +133 -0
  24. data/lib/mongo_mapper/key.rb +36 -0
  25. data/lib/mongo_mapper/observing.rb +50 -0
  26. data/lib/mongo_mapper/pagination.rb +55 -0
  27. data/lib/mongo_mapper/rails_compatibility/document.rb +15 -0
  28. data/lib/mongo_mapper/rails_compatibility/embedded_document.rb +27 -0
  29. data/lib/mongo_mapper/serialization.rb +54 -0
  30. data/lib/mongo_mapper/serializers/json_serializer.rb +92 -0
  31. data/lib/mongo_mapper/support.rb +206 -0
  32. data/lib/mongo_mapper/validations.rb +41 -0
  33. data/lib/mongo_mapper.rb +120 -0
  34. data/mongo_mapper.gemspec +173 -0
  35. data/specs.watchr +32 -0
  36. data/test/NOTE_ON_TESTING +1 -0
  37. data/test/functional/associations/test_belongs_to_polymorphic_proxy.rb +55 -0
  38. data/test/functional/associations/test_belongs_to_proxy.rb +48 -0
  39. data/test/functional/associations/test_many_documents_as_proxy.rb +246 -0
  40. data/test/functional/associations/test_many_documents_proxy.rb +387 -0
  41. data/test/functional/associations/test_many_embedded_polymorphic_proxy.rb +156 -0
  42. data/test/functional/associations/test_many_embedded_proxy.rb +192 -0
  43. data/test/functional/associations/test_many_polymorphic_proxy.rb +339 -0
  44. data/test/functional/test_associations.rb +44 -0
  45. data/test/functional/test_binary.rb +18 -0
  46. data/test/functional/test_callbacks.rb +85 -0
  47. data/test/functional/test_dirty.rb +159 -0
  48. data/test/functional/test_document.rb +1235 -0
  49. data/test/functional/test_embedded_document.rb +135 -0
  50. data/test/functional/test_logger.rb +20 -0
  51. data/test/functional/test_pagination.rb +95 -0
  52. data/test/functional/test_rails_compatibility.rb +25 -0
  53. data/test/functional/test_string_id_compatibility.rb +72 -0
  54. data/test/functional/test_validations.rb +378 -0
  55. data/test/models.rb +271 -0
  56. data/test/support/custom_matchers.rb +55 -0
  57. data/test/support/timing.rb +16 -0
  58. data/test/test_helper.rb +27 -0
  59. data/test/unit/associations/test_base.rb +166 -0
  60. data/test/unit/associations/test_proxy.rb +91 -0
  61. data/test/unit/serializers/test_json_serializer.rb +189 -0
  62. data/test/unit/test_document.rb +204 -0
  63. data/test/unit/test_dynamic_finder.rb +125 -0
  64. data/test/unit/test_embedded_document.rb +718 -0
  65. data/test/unit/test_finder_options.rb +296 -0
  66. data/test/unit/test_key.rb +172 -0
  67. data/test/unit/test_mongo_mapper.rb +65 -0
  68. data/test/unit/test_observing.rb +101 -0
  69. data/test/unit/test_pagination.rb +113 -0
  70. data/test/unit/test_rails_compatibility.rb +49 -0
  71. data/test/unit/test_serializations.rb +52 -0
  72. data/test/unit/test_support.rb +342 -0
  73. data/test/unit/test_time_zones.rb +40 -0
  74. data/test/unit/test_validations.rb +503 -0
  75. metadata +235 -0
@@ -0,0 +1,384 @@
1
+ module MongoMapper
2
+ module EmbeddedDocument
3
+ def self.included(model)
4
+ model.class_eval do
5
+ extend ClassMethods
6
+ include InstanceMethods
7
+
8
+ extend Associations::ClassMethods
9
+ include Associations::InstanceMethods
10
+
11
+ include RailsCompatibility::EmbeddedDocument
12
+ include Validatable
13
+ include Serialization
14
+
15
+ extend Validations::Macros
16
+
17
+ key :_id, ObjectId
18
+ attr_accessor :_root_document
19
+ end
20
+ end
21
+
22
+ module ClassMethods
23
+ def logger
24
+ MongoMapper.logger
25
+ end
26
+
27
+ def inherited(subclass)
28
+ unless subclass.embeddable?
29
+ subclass.set_collection_name(collection_name)
30
+ end
31
+
32
+ (@subclasses ||= []) << subclass
33
+ end
34
+
35
+ def subclasses
36
+ @subclasses
37
+ end
38
+
39
+ def keys
40
+ @keys ||= if parent = parent_model
41
+ parent.keys.dup
42
+ else
43
+ HashWithIndifferentAccess.new
44
+ end
45
+ end
46
+
47
+ def key(*args)
48
+ key = Key.new(*args)
49
+ keys[key.name] = key
50
+
51
+ create_accessors_for(key)
52
+ create_key_in_subclasses(*args)
53
+ create_validations_for(key)
54
+
55
+ key
56
+ end
57
+
58
+ def using_object_id?
59
+ object_id_key?(:_id)
60
+ end
61
+
62
+ def object_id_key?(name)
63
+ key = keys[name.to_s]
64
+ key && key.type == ObjectId
65
+ end
66
+
67
+ def embeddable?
68
+ !self.ancestors.include?(Document)
69
+ end
70
+
71
+ def parent_model
72
+ (ancestors - [self,EmbeddedDocument]).find do |parent_class|
73
+ parent_class.ancestors.include?(EmbeddedDocument)
74
+ end
75
+ end
76
+
77
+ def to_mongo(instance)
78
+ return nil if instance.nil?
79
+ instance.to_mongo
80
+ end
81
+
82
+ def from_mongo(value)
83
+ return nil if value.nil?
84
+ value.is_a?(self) ? value : initialize_doc(value)
85
+ end
86
+
87
+ private
88
+ def initialize_doc(doc)
89
+ begin
90
+ klass = doc['_type'].present? ? doc['_type'].constantize : self
91
+ klass.new(doc)
92
+ rescue NameError
93
+ new(doc)
94
+ end
95
+ end
96
+
97
+ def accessors_module
98
+ module_defined = if method(:const_defined?).arity == 1 # Ruby 1.9 compat check
99
+ const_defined?('MongoMapperKeys')
100
+ else
101
+ const_defined?('MongoMapperKeys', false)
102
+ end
103
+
104
+ if module_defined
105
+ const_get 'MongoMapperKeys'
106
+ else
107
+ const_set 'MongoMapperKeys', Module.new
108
+ end
109
+ end
110
+
111
+ def create_accessors_for(key)
112
+ accessors_module.module_eval <<-end_eval
113
+ def #{key.name}
114
+ read_attribute(:'#{key.name}')
115
+ end
116
+
117
+ def #{key.name}_before_typecast
118
+ read_attribute_before_typecast(:'#{key.name}')
119
+ end
120
+
121
+ def #{key.name}=(value)
122
+ write_attribute(:'#{key.name}', value)
123
+ end
124
+
125
+ def #{key.name}?
126
+ read_attribute(:#{key.name}).present?
127
+ end
128
+ end_eval
129
+ include accessors_module
130
+ end
131
+
132
+ def create_key_in_subclasses(*args)
133
+ return if subclasses.blank?
134
+
135
+ subclasses.each do |subclass|
136
+ subclass.key(*args)
137
+ end
138
+ end
139
+
140
+ def create_validations_for(key)
141
+ attribute = key.name.to_sym
142
+
143
+ if key.options[:required]
144
+ validates_presence_of(attribute)
145
+ end
146
+
147
+ if key.options[:unique]
148
+ validates_uniqueness_of(attribute)
149
+ end
150
+
151
+ if key.options[:numeric]
152
+ number_options = key.type == Integer ? {:only_integer => true} : {}
153
+ validates_numericality_of(attribute, number_options)
154
+ end
155
+
156
+ if key.options[:format]
157
+ validates_format_of(attribute, :with => key.options[:format])
158
+ end
159
+
160
+ if key.options[:length]
161
+ length_options = case key.options[:length]
162
+ when Integer
163
+ {:minimum => 0, :maximum => key.options[:length]}
164
+ when Range
165
+ {:within => key.options[:length]}
166
+ when Hash
167
+ key.options[:length]
168
+ end
169
+ validates_length_of(attribute, length_options)
170
+ end
171
+ end
172
+ end
173
+
174
+ module InstanceMethods
175
+ def initialize(attrs={})
176
+ unless attrs.nil?
177
+ associations.each do |name, association|
178
+ if collection = attrs.delete(name)
179
+ if association.many? && association.klass.embeddable?
180
+ root_document = attrs[:_root_document] || self
181
+ collection.each do |doc|
182
+ doc[:_root_document] = root_document
183
+ end
184
+ end
185
+ send("#{association.name}=", collection)
186
+ end
187
+ end
188
+
189
+ self.attributes = attrs
190
+
191
+ if respond_to?(:_type=) && self['_type'].blank?
192
+ self._type = self.class.name
193
+ end
194
+ end
195
+
196
+ if self.class.embeddable?
197
+ if read_attribute(:_id).blank?
198
+ write_attribute :_id, Mongo::ObjectID.new
199
+ @new_document = true
200
+ else
201
+ @new_document = false
202
+ end
203
+ end
204
+ end
205
+
206
+ def new?
207
+ !!@new_document
208
+ end
209
+
210
+ def to_param
211
+ id.to_s
212
+ end
213
+
214
+ def attributes=(attrs)
215
+ return if attrs.blank?
216
+ attrs.each_pair do |name, value|
217
+ writer_method = "#{name}="
218
+
219
+ if respond_to?(writer_method)
220
+ self.send(writer_method, value)
221
+ else
222
+ self[name.to_s] = value
223
+ end
224
+ end
225
+ end
226
+
227
+ def attributes
228
+ attrs = HashWithIndifferentAccess.new
229
+
230
+ embedded_keys.each do |key|
231
+ attrs[key.name] = read_attribute(key.name).try(:attributes)
232
+ end
233
+
234
+ non_embedded_keys.each do |key|
235
+ attrs[key.name] = read_attribute(key.name)
236
+ end
237
+
238
+ embedded_associations.each do |association|
239
+ documents = instance_variable_get(association.ivar)
240
+ next if documents.nil?
241
+ attrs[association.name] = documents.collect { |doc| doc.attributes }
242
+ end
243
+
244
+ attrs
245
+ end
246
+
247
+ def to_mongo
248
+ attrs = HashWithIndifferentAccess.new
249
+
250
+ _keys.each_pair do |name, key|
251
+ value = key.set(read_attribute(key.name))
252
+ attrs[name] = value unless value.nil?
253
+ end
254
+
255
+ embedded_associations.each do |association|
256
+ if documents = instance_variable_get(association.ivar)
257
+ attrs[association.name] = documents.map { |document| document.to_mongo }
258
+ end
259
+ end
260
+
261
+ attrs
262
+ end
263
+
264
+ def clone
265
+ clone_attributes = self.attributes
266
+ clone_attributes.delete("_id")
267
+ self.class.new(clone_attributes)
268
+ end
269
+
270
+ def [](name)
271
+ read_attribute(name)
272
+ end
273
+
274
+ def []=(name, value)
275
+ ensure_key_exists(name)
276
+ write_attribute(name, value)
277
+ end
278
+
279
+ def ==(other)
280
+ other.is_a?(self.class) && _id == other._id
281
+ end
282
+
283
+ def id
284
+ read_attribute(:_id)
285
+ end
286
+
287
+ def id=(value)
288
+ if self.class.using_object_id?
289
+ value = MongoMapper.normalize_object_id(value)
290
+ else
291
+ @using_custom_id = true
292
+ end
293
+
294
+ write_attribute :_id, value
295
+ end
296
+
297
+ def using_custom_id?
298
+ !!@using_custom_id
299
+ end
300
+
301
+ def inspect
302
+ attributes_as_nice_string = key_names.collect do |name|
303
+ "#{name}: #{read_attribute(name).inspect}"
304
+ end.join(", ")
305
+ "#<#{self.class} #{attributes_as_nice_string}>"
306
+ end
307
+
308
+ def save
309
+ if _root_document
310
+ _root_document.save
311
+ end
312
+ end
313
+
314
+ def save!
315
+ if _root_document
316
+ _root_document.save!
317
+ end
318
+ end
319
+
320
+ def update_attributes(attrs={})
321
+ self.attributes = attrs
322
+ save
323
+ end
324
+
325
+ def update_attributes!(attrs={})
326
+ self.attributes = attrs
327
+ save!
328
+ end
329
+
330
+ def logger
331
+ self.class.logger
332
+ end
333
+
334
+ private
335
+ def _keys
336
+ self.metaclass.keys
337
+ end
338
+
339
+ def key_names
340
+ _keys.keys
341
+ end
342
+
343
+ def non_embedded_keys
344
+ _keys.values.select { |key| !key.embeddable? }
345
+ end
346
+
347
+ def embedded_keys
348
+ _keys.values.select { |key| key.embeddable? }
349
+ end
350
+
351
+ def ensure_key_exists(name)
352
+ self.metaclass.key(name) unless respond_to?("#{name}=")
353
+ end
354
+
355
+ def read_attribute(name)
356
+ if key = _keys[name]
357
+ value = key.get(instance_variable_get("@#{name}"))
358
+ instance_variable_set "@#{name}", value if !frozen?
359
+ value
360
+ else
361
+ raise KeyNotFound, "Could not find key: #{name.inspect}"
362
+ end
363
+ end
364
+
365
+ def read_attribute_before_typecast(name)
366
+ instance_variable_get("@#{name}_before_typecast")
367
+ end
368
+
369
+ def write_attribute(name, value)
370
+ key = _keys[name]
371
+ instance_variable_set "@#{name}_before_typecast", value
372
+ instance_variable_set "@#{name}", key.set(value)
373
+ end
374
+
375
+ def embedded_associations
376
+ associations.select do |name, association|
377
+ association.embeddable?
378
+ end.map do |name, association|
379
+ association
380
+ end
381
+ end
382
+ end # InstanceMethods
383
+ end # EmbeddedDocument
384
+ end # MongoMapper
@@ -0,0 +1,133 @@
1
+ module MongoMapper
2
+ # Controls the parsing and handling of options used by finders.
3
+ #
4
+ # == Important Note
5
+ #
6
+ # This class is private to MongoMapper and should not be considered part of
7
+ # MongoMapper's public API. Some documentation herein, however, may prove
8
+ # useful for understanding how MongoMapper handles the parsing of finder
9
+ # conditions and options.
10
+ #
11
+ # @private
12
+ class FinderOptions
13
+ OptionKeys = [:fields, :select, :skip, :offset, :limit, :sort, :order]
14
+
15
+ def initialize(model, options)
16
+ raise ArgumentError, "Options must be a hash" unless options.is_a?(Hash)
17
+ options = options.symbolize_keys
18
+
19
+ @model = model
20
+ @options = {}
21
+ @conditions = options.delete(:conditions) || {}
22
+
23
+ options.each_pair do |key, value|
24
+ if OptionKeys.include?(key)
25
+ @options[key] = value
26
+ else
27
+ @conditions[key] = value
28
+ end
29
+ end
30
+
31
+ add_sci_scope
32
+ end
33
+
34
+ # @return [Hash] Mongo compatible criteria options
35
+ #
36
+ # @see FinderOptions#to_mongo_criteria
37
+ def criteria
38
+ to_mongo_criteria(@conditions)
39
+ end
40
+
41
+ # @return [Hash] Mongo compatible options
42
+ def options
43
+ fields = @options.delete(:fields) || @options.delete(:select)
44
+ skip = @options.delete(:skip) || @options.delete(:offset) || 0
45
+ limit = @options.delete(:limit) || 0
46
+ sort = @options.delete(:sort) || convert_order_to_sort(@options.delete(:order))
47
+
48
+ {:fields => to_mongo_fields(fields), :skip => skip.to_i, :limit => limit.to_i, :sort => sort}
49
+ end
50
+
51
+ # @return [Array<Hash>] Mongo criteria and options enclosed in an Array
52
+ def to_a
53
+ [criteria, options]
54
+ end
55
+
56
+ private
57
+ def to_mongo_criteria(conditions, parent_key=nil)
58
+ criteria = {}
59
+
60
+ conditions.each_pair do |field, value|
61
+ field = normalized_field(field)
62
+
63
+ if @model.object_id_key?(field) && value.is_a?(String)
64
+ value = Mongo::ObjectID.from_string(value)
65
+ end
66
+
67
+ if field.is_a?(FinderOperator)
68
+ criteria.merge!(field.to_criteria(value))
69
+ next
70
+ end
71
+
72
+ case value
73
+ when Array
74
+ criteria[field] = operator?(field) ? value : {'$in' => value}
75
+ when Hash
76
+ criteria[field] = to_mongo_criteria(value, field)
77
+ else
78
+ criteria[field] = value
79
+ end
80
+ end
81
+
82
+ criteria
83
+ end
84
+
85
+ def operator?(field)
86
+ field.to_s =~ /^\$/
87
+ end
88
+
89
+ def normalized_field(field)
90
+ field.to_s == 'id' ? :_id : field
91
+ end
92
+
93
+ # adds _type single collection inheritance scope for models that need it
94
+ def add_sci_scope
95
+ if @model.single_collection_inherited?
96
+ @conditions[:_type] = @model.to_s
97
+ end
98
+ end
99
+
100
+ def to_mongo_fields(fields)
101
+ return if fields.blank?
102
+
103
+ if fields.is_a?(String)
104
+ fields.split(',').map { |field| field.strip }
105
+ else
106
+ fields.flatten.compact
107
+ end
108
+ end
109
+
110
+ def convert_order_to_sort(sort)
111
+ return if sort.blank?
112
+ pieces = sort.split(',')
113
+ pieces.map { |s| to_mongo_sort_piece(s) }
114
+ end
115
+
116
+ def to_mongo_sort_piece(str)
117
+ field, direction = str.strip.split(' ')
118
+ direction ||= 'ASC'
119
+ direction = direction.upcase == 'ASC' ? 1 : -1
120
+ [field, direction]
121
+ end
122
+ end
123
+
124
+ class FinderOperator
125
+ def initialize(field, operator)
126
+ @field, @operator = field, operator
127
+ end
128
+
129
+ def to_criteria(value)
130
+ {@field => {@operator => value}}
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,36 @@
1
+ module MongoMapper
2
+ class Key
3
+ attr_accessor :name, :type, :options, :default_value
4
+
5
+ def initialize(*args)
6
+ options = args.extract_options!
7
+ @name, @type = args.shift.to_s, args.shift
8
+ self.options = (options || {}).symbolize_keys
9
+ self.default_value = self.options.delete(:default)
10
+ end
11
+
12
+ def ==(other)
13
+ @name == other.name && @type == other.type
14
+ end
15
+
16
+ def set(value)
17
+ type.to_mongo(value)
18
+ end
19
+
20
+ def embeddable?
21
+ type.respond_to?(:embeddable?) && type.embeddable? ? true : false
22
+ end
23
+
24
+ def number?
25
+ [Integer, Float].include?(type)
26
+ end
27
+
28
+ def get(value)
29
+ if value.nil? && !default_value.nil?
30
+ return default_value
31
+ end
32
+
33
+ type.from_mongo(value)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,50 @@
1
+ require 'observer'
2
+ require 'singleton'
3
+ require 'set'
4
+
5
+ module MongoMapper
6
+ module Observing #:nodoc:
7
+ def self.included(model)
8
+ model.class_eval do
9
+ extend Observable
10
+ end
11
+ end
12
+ end
13
+
14
+ class Observer
15
+ include Singleton
16
+
17
+ class << self
18
+ def observe(*models)
19
+ models.flatten!
20
+ models.collect! { |model| model.is_a?(Symbol) ? model.to_s.camelize.constantize : model }
21
+ define_method(:observed_classes) { Set.new(models) }
22
+ end
23
+
24
+ def observed_class
25
+ if observed_class_name = name[/(.*)Observer/, 1]
26
+ observed_class_name.constantize
27
+ else
28
+ nil
29
+ end
30
+ end
31
+ end
32
+
33
+ def initialize
34
+ Set.new(observed_classes).each { |klass| add_observer! klass }
35
+ end
36
+
37
+ def update(observed_method, object) #:nodoc:
38
+ send(observed_method, object) if respond_to?(observed_method)
39
+ end
40
+
41
+ protected
42
+ def observed_classes
43
+ Set.new([self.class.observed_class].compact.flatten)
44
+ end
45
+
46
+ def add_observer!(klass)
47
+ klass.add_observer(self)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,55 @@
1
+ module MongoMapper
2
+ module Pagination
3
+ class PaginationProxy
4
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|proxy_|^object_id$)/ }
5
+
6
+ attr_accessor :subject
7
+ attr_reader :total_entries, :per_page, :current_page
8
+ alias limit per_page
9
+
10
+ def initialize(total_entries, current_page, per_page=nil)
11
+ @total_entries = total_entries.to_i
12
+ self.per_page = per_page
13
+ self.current_page = current_page
14
+ end
15
+
16
+ def total_pages
17
+ (total_entries / per_page.to_f).ceil
18
+ end
19
+
20
+ def out_of_bounds?
21
+ current_page > total_pages
22
+ end
23
+
24
+ def previous_page
25
+ current_page > 1 ? (current_page - 1) : nil
26
+ end
27
+
28
+ def next_page
29
+ current_page < total_pages ? (current_page + 1) : nil
30
+ end
31
+
32
+ def skip
33
+ (current_page - 1) * per_page
34
+ end
35
+ alias offset skip # for will paginate support
36
+
37
+
38
+ def method_missing(name, *args, &block)
39
+ @subject.send(name, *args, &block)
40
+ end
41
+
42
+ private
43
+ def per_page=(value)
44
+ value = 25 if value.blank?
45
+ @per_page = value.to_i
46
+ end
47
+
48
+ def current_page=(value)
49
+ value = value.to_i
50
+ value = 1 if value < 1
51
+ @current_page = value
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,15 @@
1
+ module MongoMapper
2
+ module RailsCompatibility
3
+ module Document
4
+ def self.included(model)
5
+ model.class_eval do
6
+ alias_method :new_record?, :new?
7
+
8
+ def human_name
9
+ self.name.demodulize.titleize
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ module MongoMapper
2
+ module RailsCompatibility
3
+ module EmbeddedDocument
4
+ def self.included(model)
5
+ model.class_eval do
6
+ extend ClassMethods
7
+
8
+ alias_method :new_record?, :new?
9
+ end
10
+
11
+ class << model
12
+ alias has_many many
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ def column_names
18
+ keys.keys
19
+ end
20
+
21
+ def human_name
22
+ self.name.demodulize.titleize
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end