mongo_doc_rails2 0.6.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 (142) hide show
  1. data/.document +5 -0
  2. data/.gitignore +8 -0
  3. data/HISTORY.md +11 -0
  4. data/LICENSE +20 -0
  5. data/README.textile +185 -0
  6. data/Rakefile +188 -0
  7. data/TODO +40 -0
  8. data/VERSION +1 -0
  9. data/data/.gitignore +2 -0
  10. data/examples/simple_document.rb +46 -0
  11. data/examples/simple_object.rb +34 -0
  12. data/features/collections.feature +9 -0
  13. data/features/embed_hash.feature +16 -0
  14. data/features/finders.feature +76 -0
  15. data/features/indexes.feature +28 -0
  16. data/features/mongodb.yml +7 -0
  17. data/features/mongodoc_base.feature +128 -0
  18. data/features/new_record.feature +36 -0
  19. data/features/partial_updates.feature +95 -0
  20. data/features/removing_documents.feature +68 -0
  21. data/features/saving_an_object.feature +15 -0
  22. data/features/scopes.feature +66 -0
  23. data/features/step_definitions/collection_steps.rb +17 -0
  24. data/features/step_definitions/document_steps.rb +149 -0
  25. data/features/step_definitions/documents.rb +40 -0
  26. data/features/step_definitions/embed_hash_steps.rb +6 -0
  27. data/features/step_definitions/finder_steps.rb +15 -0
  28. data/features/step_definitions/index_steps.rb +10 -0
  29. data/features/step_definitions/json_steps.rb +9 -0
  30. data/features/step_definitions/object_steps.rb +50 -0
  31. data/features/step_definitions/objects.rb +24 -0
  32. data/features/step_definitions/partial_update_steps.rb +31 -0
  33. data/features/step_definitions/query_steps.rb +66 -0
  34. data/features/step_definitions/removing_documents_steps.rb +14 -0
  35. data/features/step_definitions/scope_steps.rb +18 -0
  36. data/features/step_definitions/string_casting_steps.rb +29 -0
  37. data/features/step_definitions/util_steps.rb +7 -0
  38. data/features/string_casting.feature +10 -0
  39. data/features/support/support.rb +10 -0
  40. data/features/using_criteria.feature +142 -0
  41. data/lib/mongo_doc.rb +12 -0
  42. data/lib/mongo_doc/associations.rb +109 -0
  43. data/lib/mongo_doc/associations/collection_proxy.rb +121 -0
  44. data/lib/mongo_doc/associations/document_proxy.rb +65 -0
  45. data/lib/mongo_doc/associations/hash_proxy.rb +102 -0
  46. data/lib/mongo_doc/associations/proxy_base.rb +48 -0
  47. data/lib/mongo_doc/attributes.rb +84 -0
  48. data/lib/mongo_doc/bson.rb +31 -0
  49. data/lib/mongo_doc/collection.rb +82 -0
  50. data/lib/mongo_doc/connection.rb +88 -0
  51. data/lib/mongo_doc/contexts.rb +31 -0
  52. data/lib/mongo_doc/contexts/ids.rb +41 -0
  53. data/lib/mongo_doc/contexts/mongo.rb +272 -0
  54. data/lib/mongo_doc/criteria.rb +70 -0
  55. data/lib/mongo_doc/cursor.rb +32 -0
  56. data/lib/mongo_doc/document.rb +205 -0
  57. data/lib/mongo_doc/ext.rb +16 -0
  58. data/lib/mongo_doc/ext/array.rb +5 -0
  59. data/lib/mongo_doc/ext/binary.rb +7 -0
  60. data/lib/mongo_doc/ext/boolean_class.rb +17 -0
  61. data/lib/mongo_doc/ext/date.rb +19 -0
  62. data/lib/mongo_doc/ext/date_time.rb +17 -0
  63. data/lib/mongo_doc/ext/dbref.rb +7 -0
  64. data/lib/mongo_doc/ext/hash.rb +7 -0
  65. data/lib/mongo_doc/ext/min_max_keys.rb +13 -0
  66. data/lib/mongo_doc/ext/nil_class.rb +5 -0
  67. data/lib/mongo_doc/ext/numeric.rb +17 -0
  68. data/lib/mongo_doc/ext/object.rb +19 -0
  69. data/lib/mongo_doc/ext/object_id.rb +7 -0
  70. data/lib/mongo_doc/ext/regexp.rb +5 -0
  71. data/lib/mongo_doc/ext/string.rb +5 -0
  72. data/lib/mongo_doc/ext/symbol.rb +5 -0
  73. data/lib/mongo_doc/ext/time.rb +9 -0
  74. data/lib/mongo_doc/finders.rb +38 -0
  75. data/lib/mongo_doc/index.rb +46 -0
  76. data/lib/mongo_doc/matchers.rb +35 -0
  77. data/lib/mongo_doc/root.rb +26 -0
  78. data/lib/mongo_doc/scope.rb +64 -0
  79. data/lib/mongo_doc/validations.rb +12 -0
  80. data/lib/mongo_doc/validations/macros.rb +11 -0
  81. data/lib/mongo_doc/validations/validates_embedded.rb +13 -0
  82. data/lib/mongoid/contexts/enumerable.rb +151 -0
  83. data/lib/mongoid/contexts/paging.rb +42 -0
  84. data/lib/mongoid/criteria.rb +239 -0
  85. data/lib/mongoid/criterion/complex.rb +21 -0
  86. data/lib/mongoid/criterion/exclusion.rb +65 -0
  87. data/lib/mongoid/criterion/inclusion.rb +93 -0
  88. data/lib/mongoid/criterion/optional.rb +136 -0
  89. data/lib/mongoid/extensions/hash/criteria_helpers.rb +20 -0
  90. data/lib/mongoid/extensions/symbol/inflections.rb +36 -0
  91. data/lib/mongoid/matchers/all.rb +11 -0
  92. data/lib/mongoid/matchers/default.rb +26 -0
  93. data/lib/mongoid/matchers/exists.rb +13 -0
  94. data/lib/mongoid/matchers/gt.rb +11 -0
  95. data/lib/mongoid/matchers/gte.rb +11 -0
  96. data/lib/mongoid/matchers/in.rb +11 -0
  97. data/lib/mongoid/matchers/lt.rb +11 -0
  98. data/lib/mongoid/matchers/lte.rb +11 -0
  99. data/lib/mongoid/matchers/ne.rb +11 -0
  100. data/lib/mongoid/matchers/nin.rb +11 -0
  101. data/lib/mongoid/matchers/size.rb +11 -0
  102. data/mongo_doc_rails2.gemspec +237 -0
  103. data/mongod.example.yml +2 -0
  104. data/mongodb.example.yml +14 -0
  105. data/perf/mongo_doc_object.rb +83 -0
  106. data/perf/mongo_document.rb +84 -0
  107. data/perf/ruby_driver.rb +49 -0
  108. data/script/console +8 -0
  109. data/spec/array_including_argument_matcher.rb +62 -0
  110. data/spec/associations/collection_proxy_spec.rb +233 -0
  111. data/spec/associations/document_proxy_spec.rb +45 -0
  112. data/spec/associations/hash_proxy_spec.rb +181 -0
  113. data/spec/associations/proxy_base_spec.rb +92 -0
  114. data/spec/associations_spec.rb +218 -0
  115. data/spec/attributes_accessor_spec.rb +33 -0
  116. data/spec/attributes_spec.rb +145 -0
  117. data/spec/bson_matchers.rb +54 -0
  118. data/spec/bson_spec.rb +196 -0
  119. data/spec/collection_spec.rb +169 -0
  120. data/spec/connection_spec.rb +147 -0
  121. data/spec/contexts/ids_spec.rb +49 -0
  122. data/spec/contexts/mongo_spec.rb +235 -0
  123. data/spec/contexts_spec.rb +56 -0
  124. data/spec/criteria_spec.rb +69 -0
  125. data/spec/cursor_spec.rb +91 -0
  126. data/spec/document_ext.rb +9 -0
  127. data/spec/document_spec.rb +553 -0
  128. data/spec/embedded_save_spec.rb +73 -0
  129. data/spec/ext_spec.rb +89 -0
  130. data/spec/finders_spec.rb +61 -0
  131. data/spec/hash_matchers.rb +27 -0
  132. data/spec/index_spec.rb +79 -0
  133. data/spec/matchers_spec.rb +342 -0
  134. data/spec/mongodb.yml +6 -0
  135. data/spec/mongodb_pairs.yml +8 -0
  136. data/spec/new_record_spec.rb +128 -0
  137. data/spec/root_spec.rb +41 -0
  138. data/spec/scope_spec.rb +79 -0
  139. data/spec/spec.opts +2 -0
  140. data/spec/spec_helper.rb +14 -0
  141. data/spec/validations_spec.rb +30 -0
  142. metadata +346 -0
@@ -0,0 +1,82 @@
1
+ require 'mongo_doc/cursor'
2
+ require 'mongo_doc/criteria'
3
+
4
+ module MongoDoc
5
+ class Collection
6
+ attr_accessor :_collection
7
+
8
+ include MongoDoc::Criteria
9
+
10
+ delegate \
11
+ :[],
12
+ :clear,
13
+ :count,
14
+ :create_index,
15
+ :db,
16
+ :distinct,
17
+ :drop,
18
+ :drop_index,
19
+ :drop_indexes,
20
+ :group,
21
+ :hint,
22
+ :index_information,
23
+ :map_reduce,
24
+ :mapreduce,
25
+ :name,
26
+ :options,
27
+ :pk_factory,
28
+ :remove,
29
+ :rename,
30
+ :size, :to => :_collection
31
+
32
+ def initialize(name)
33
+ self._collection = self.class.mongo_collection(name)
34
+ end
35
+
36
+ def find(query = {}, options = {})
37
+ cursor = wrapped_cursor(query, options)
38
+ if block_given?
39
+ yield cursor
40
+ cursor.close
41
+ else
42
+ cursor
43
+ end
44
+ end
45
+
46
+ def find_one(spec_or_object_id = nil, options = {})
47
+ MongoDoc::BSON.decode(_collection.find_one(spec_or_object_id, options))
48
+ end
49
+
50
+ def insert(doc_or_docs, options = {})
51
+ _collection.insert(doc_or_docs.to_bson, options)
52
+ end
53
+ alias << insert
54
+
55
+ def save(doc, options = {})
56
+ _collection.save(doc.to_bson, options)
57
+ end
58
+
59
+ def update(spec, doc, options = {})
60
+ _collection.update(spec, doc.to_bson, options)
61
+ (last_error || {})['updatedExisting'] || false
62
+ end
63
+
64
+ protected
65
+
66
+ def collection
67
+ self
68
+ end
69
+
70
+ def last_error
71
+ MongoDoc::Connection.database.command({'getlasterror' => 1})
72
+ end
73
+
74
+ def wrapped_cursor(query = {}, options = {})
75
+ MongoDoc::Cursor.new(self, _collection.find(query, options))
76
+ end
77
+
78
+ def self.mongo_collection(name)
79
+ MongoDoc::Connection.database.collection(name)
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,88 @@
1
+ module MongoDoc
2
+ class NoConnectionError < RuntimeError; end
3
+ class UnsupportedServerVersionError < RuntimeError; end
4
+
5
+ module Connection
6
+
7
+ extend self
8
+
9
+ attr_writer :config_path, :env, :host, :name, :options, :port, :strict
10
+
11
+ def config_path
12
+ @config_path || default_path
13
+ end
14
+
15
+ def configuration
16
+ @configuration ||= File.exists?(config_path) ? YAML.load_file(config_path)[env] : {}
17
+ end
18
+
19
+ def connection
20
+ @connection ||= connect
21
+ end
22
+
23
+ def database
24
+ @database ||= connection.db(name, :strict => strict)
25
+ end
26
+
27
+ def env
28
+ if rails?
29
+ Rails.env
30
+ else
31
+ @env ||= 'development'
32
+ end
33
+ end
34
+
35
+ def host
36
+ @host ||= configuration['host']
37
+ end
38
+
39
+ def name
40
+ @name ||= configuration['name'] || default_name
41
+ end
42
+
43
+ def options
44
+ @options ||= configuration['options'] || {}
45
+ end
46
+
47
+ def port
48
+ @port ||= configuration['port']
49
+ end
50
+
51
+ def strict
52
+ @strict ||= configuration['strict'] || false
53
+ end
54
+
55
+ private
56
+
57
+ def connect
58
+ connection = Mongo::Connection.new(host, port, options)
59
+ raise NoConnectionError unless connection
60
+ verify_server_version(connection)
61
+ connection
62
+ end
63
+
64
+ def default_name
65
+ if rails?
66
+ "#{Rails.root.basename}_#{Rails.env}"
67
+ else
68
+ "mongo_doc"
69
+ end
70
+ end
71
+
72
+ def default_path
73
+ if rails?
74
+ Rails.root + 'config/mongodb.yml'
75
+ else
76
+ './mongodb.yml'
77
+ end
78
+ end
79
+
80
+ def rails?
81
+ Object.const_defined?("Rails")
82
+ end
83
+
84
+ def verify_server_version(connection)
85
+ raise UnsupportedServerVersionError.new('MongoDoc requires at least mongoDB version 1.4.0') unless connection.server_version >= "1.4.0"
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+ require "mongoid/contexts/paging"
3
+ require "mongo_doc/contexts/ids"
4
+ require "mongoid/contexts/enumerable"
5
+ require "mongo_doc/contexts/mongo"
6
+
7
+ module Mongoid
8
+ module Contexts
9
+
10
+ class UnknownContext < RuntimeError; end
11
+
12
+ # Determines the context to be used for this criteria. If the class is an
13
+ # embedded document, then the context will be the array in the embed_many
14
+ # association it is in. If the class is a root, then the database itself
15
+ # will be the context.
16
+ #
17
+ # Example:
18
+ #
19
+ # <tt>Contexts.context_for(criteria)</tt>
20
+ def self.context_for(criteria)
21
+ if criteria.klass.respond_to?(:_append)
22
+ return Mongoid::Contexts::Enumerable.new(criteria)
23
+ elsif criteria.klass.respond_to?(:collection)
24
+ return MongoDoc::Contexts::Mongo.new(criteria)
25
+ else
26
+ raise UnknownContext.new("Context not found for: #{criteria.klass}")
27
+ end
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,41 @@
1
+ module Mongoid
2
+ module Contexts
3
+ module Ids
4
+ # Return documents based on an id search. Will handle if a single id has
5
+ # been passed or mulitple ids.
6
+ #
7
+ # Example:
8
+ #
9
+ # context.id_criteria([1, 2, 3])
10
+ #
11
+ # Returns:
12
+ #
13
+ # The single or multiple documents.
14
+ def id_criteria(params)
15
+ criteria.id(strings_to_object_ids(params))
16
+ params.is_a?(Array) ? criteria.entries : one
17
+ end
18
+
19
+ protected
20
+
21
+ # Convert ids from strings to +BSON::ObjectID+s
22
+ def strings_to_object_ids(ids)
23
+ if Array === ids
24
+ ids.map {|id| string_to_object_id(id) }
25
+ else
26
+ string_to_object_id(ids)
27
+ end
28
+
29
+ end
30
+
31
+ # Convert ids from strings to +BSON::ObjectID+s
32
+ def string_to_object_id(id)
33
+ if String === id
34
+ ::BSON::ObjectID.from_string(id)
35
+ else
36
+ id
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,272 @@
1
+ module MongoDoc
2
+ module Contexts
3
+ class Mongo
4
+ include Mongoid::Contexts::Paging
5
+ include Mongoid::Contexts::Ids
6
+
7
+ attr_reader :criteria, :cache
8
+
9
+ delegate :klass, :options, :selector, :to => :criteria
10
+ delegate :collection, :to => :klass
11
+
12
+ AGGREGATE_REDUCE = "function(obj, prev) { prev.count++; }"
13
+ # Aggregate the context. This will take the internally built selector and options
14
+ # and pass them on to the Ruby driver's +group()+ method on the collection. The
15
+ # collection itself will be retrieved from the class provided, and once the
16
+ # query has returned it will provided a grouping of keys with counts.
17
+ #
18
+ # Example:
19
+ #
20
+ # <tt>context.aggregate</tt>
21
+ #
22
+ # Returns:
23
+ #
24
+ # A +Hash+ with field values as keys, counts as values
25
+ def aggregate
26
+ collection.group(options[:fields], selector, { :count => 0 }, AGGREGATE_REDUCE, true)
27
+ end
28
+
29
+ # Get the average value for the supplied field.
30
+ #
31
+ # This will take the internally built selector and options
32
+ # and pass them on to the Ruby driver's +group()+ method on the collection. The
33
+ # collection itself will be retrieved from the class provided, and once the
34
+ # query has returned it will provided a grouping of keys with averages.
35
+ #
36
+ # Example:
37
+ #
38
+ # <tt>context.avg(:age)</tt>
39
+ #
40
+ # Returns:
41
+ #
42
+ # A numeric value that is the average.
43
+ def avg(field)
44
+ total = sum(field)
45
+ total ? (total / count) : nil
46
+ end
47
+
48
+ # Get the count of matching documents in the database for the context.
49
+ #
50
+ # Example:
51
+ #
52
+ # <tt>context.count</tt>
53
+ #
54
+ # Returns:
55
+ #
56
+ # An +Integer+ count of documents.
57
+ def count
58
+ @count ||= collection.find(selector, options).count
59
+ end
60
+
61
+ # Gets an array of distinct values for the supplied field across the
62
+ # entire collection or the susbset given the criteria.
63
+ #
64
+ # Example:
65
+ #
66
+ # <tt>context.distinct(:title)</tt>
67
+ def distinct(field)
68
+ collection.distinct(field, selector)
69
+ end
70
+
71
+ # Determine if the context is empty or blank given the criteria. Will
72
+ # perform a quick find_one asking only for the id.
73
+ #
74
+ # Example:
75
+ #
76
+ # <tt>context.blank?</tt>
77
+ def empty?
78
+ collection.find_one(selector, options).nil?
79
+ end
80
+ alias blank? empty?
81
+
82
+ # Execute the context. This will take the selector and options
83
+ # and pass them on to the Ruby driver's +find()+ method on the collection. The
84
+ # collection itself will be retrieved from the class provided, and once the
85
+ # query has returned new documents of the type of class provided will be instantiated.
86
+ #
87
+ # Example:
88
+ #
89
+ # <tt>mongo.execute</tt>
90
+ #
91
+ # Returns:
92
+ #
93
+ # An enumerable +Cursor+.
94
+ def execute(paginating = false)
95
+ cursor = collection.find(selector, options)
96
+ if cursor
97
+ @count = cursor.count if paginating
98
+ cursor
99
+ else
100
+ []
101
+ end
102
+ end
103
+
104
+ GROUP_REDUCE = "function(obj, prev) { prev.group.push(obj); }"
105
+ # Groups the context. This will take the internally built selector and options
106
+ # and pass them on to the Ruby driver's +group()+ method on the collection. The
107
+ # collection itself will be retrieved from the class provided, and once the
108
+ # query has returned it will provided a grouping of keys with objects.
109
+ #
110
+ # Example:
111
+ #
112
+ # <tt>context.group</tt>
113
+ #
114
+ # Returns:
115
+ #
116
+ # A +Hash+ with field values as keys, arrays of documents as values.
117
+ def group
118
+ collection.group(
119
+ options[:fields],
120
+ selector,
121
+ { :group => [] },
122
+ GROUP_REDUCE,
123
+ true
124
+ ).collect {|docs| docs["group"] = MongoDoc::BSON.decode(docs["group"]); docs }
125
+ end
126
+
127
+ # Create the new mongo context. This will execute the queries given the
128
+ # selector and options against the database.
129
+ #
130
+ # Example:
131
+ #
132
+ # <tt>Mongoid::Contexts::Mongo.new(criteria)</tt>
133
+ def initialize(criteria)
134
+ @criteria = criteria
135
+ end
136
+
137
+ # Iterate over each +Document+ in the results. This can take an optional
138
+ # block to pass to each argument in the results.
139
+ #
140
+ # Example:
141
+ #
142
+ # <tt>context.iterate { |doc| p doc }</tt>
143
+ def iterate(&block)
144
+ return caching(&block) if criteria.cached?
145
+ if block_given?
146
+ execute.each do |doc|
147
+ yield doc
148
+ end
149
+ end
150
+ end
151
+
152
+ # Return the last result for the +Context+. Essentially does a find_one on
153
+ # the collection with the sorting reversed. If no sorting parameters have
154
+ # been provided it will default to ids.
155
+ #
156
+ # Example:
157
+ #
158
+ # <tt>context.last</tt>
159
+ #
160
+ # Returns:
161
+ #
162
+ # The last document in the collection.
163
+ def last
164
+ sorting = options[:sort] || [[:_id, :asc]]
165
+ options[:sort] = sorting.collect { |option| [ option[0], option[1].invert ] }
166
+ collection.find_one(selector, options)
167
+ end
168
+
169
+ MAX_REDUCE = "function(obj, prev) { if (prev.max == 'start') { prev.max = obj.[field]; } " +
170
+ "if (prev.max < obj.[field]) { prev.max = obj.[field]; } }"
171
+ # Return the max value for a field.
172
+ #
173
+ # This will take the internally built selector and options
174
+ # and pass them on to the Ruby driver's +group()+ method on the collection. The
175
+ # collection itself will be retrieved from the class provided, and once the
176
+ # query has returned it will provided a grouping of keys with sums.
177
+ #
178
+ # Example:
179
+ #
180
+ # <tt>context.max(:age)</tt>
181
+ #
182
+ # Returns:
183
+ #
184
+ # A numeric max value.
185
+ def max(field)
186
+ grouped(:max, field.to_s, MAX_REDUCE)
187
+ end
188
+
189
+ MIN_REDUCE = "function(obj, prev) { if (prev.min == 'start') { prev.min = obj.[field]; } " +
190
+ "if (prev.min > obj.[field]) { prev.min = obj.[field]; } }"
191
+ # Return the min value for a field.
192
+ #
193
+ # This will take the internally built selector and options
194
+ # and pass them on to the Ruby driver's +group()+ method on the collection. The
195
+ # collection itself will be retrieved from the class provided, and once the
196
+ # query has returned it will provided a grouping of keys with sums.
197
+ #
198
+ # Example:
199
+ #
200
+ # <tt>context.min(:age)</tt>
201
+ #
202
+ # Returns:
203
+ #
204
+ # A numeric minimum value.
205
+ def min(field)
206
+ grouped(:min, field.to_s, MIN_REDUCE)
207
+ end
208
+
209
+ # Return the first result for the +Context+.
210
+ #
211
+ # Example:
212
+ #
213
+ # <tt>context.one</tt>
214
+ #
215
+ # Return:
216
+ #
217
+ # The first document in the collection.
218
+ def one
219
+ collection.find_one(selector, options)
220
+ end
221
+
222
+ alias first one
223
+
224
+ SUM_REDUCE = "function(obj, prev) { if (prev.sum == 'start') { prev.sum = 0; } prev.sum += obj.[field]; }"
225
+ # Sum the context.
226
+ #
227
+ # This will take the internally built selector and options
228
+ # and pass them on to the Ruby driver's +group()+ method on the collection. The
229
+ # collection itself will be retrieved from the class provided, and once the
230
+ # query has returned it will provided a grouping of keys with sums.
231
+ #
232
+ # Example:
233
+ #
234
+ # <tt>context.sum(:age)</tt>
235
+ #
236
+ # Returns:
237
+ #
238
+ # A numeric value that is the sum.
239
+ def sum(field)
240
+ grouped(:sum, field.to_s, SUM_REDUCE)
241
+ end
242
+
243
+ # Common functionality for grouping operations. Currently used by min, max
244
+ # and sum. Will gsub the field name in the supplied reduce function.
245
+ def grouped(start, field, reduce)
246
+ result = collection.group(
247
+ nil,
248
+ selector,
249
+ { start => "start" },
250
+ reduce.gsub("[field]", field),
251
+ true
252
+ )
253
+ result.empty? ? nil : result.first[start.to_s]
254
+ end
255
+
256
+ protected
257
+
258
+ # Iterate and cache results from execute
259
+ def caching(&block)
260
+ if cache
261
+ cache.each(&block)
262
+ else
263
+ @cache = []
264
+ execute.each do |doc|
265
+ @cache << doc
266
+ yield doc if block_given?
267
+ end
268
+ end
269
+ end
270
+ end
271
+ end
272
+ end