all_futures 1.0.3 → 2.0.0

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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +4 -1
  3. data/Gemfile.lock +272 -61
  4. data/LICENSE.txt +1 -1
  5. data/README.md +59 -79
  6. data/Rakefile +13 -1
  7. data/SUMMARY.md +22 -2
  8. data/bin/console +8 -0
  9. data/bin/loc +0 -0
  10. data/bin/setup +0 -0
  11. data/bin/standardize +0 -0
  12. data/lib/all_futures/association.rb +42 -0
  13. data/lib/all_futures/association_index.rb +69 -0
  14. data/lib/all_futures/attributes.rb +28 -0
  15. data/lib/all_futures/base.rb +66 -0
  16. data/lib/all_futures/callbacks.rb +71 -0
  17. data/lib/all_futures/configuration.rb +9 -0
  18. data/lib/all_futures/dirty.rb +116 -0
  19. data/lib/all_futures/embeds.rb +40 -0
  20. data/lib/all_futures/errors.rb +37 -0
  21. data/lib/all_futures/finder.rb +214 -0
  22. data/lib/all_futures/locale/en.yml +49 -0
  23. data/lib/all_futures/persistence.rb +595 -0
  24. data/lib/all_futures/presenter.rb +35 -0
  25. data/lib/all_futures/railtie.rb +26 -0
  26. data/lib/all_futures/translation.rb +22 -0
  27. data/lib/all_futures/validations.rb +10 -0
  28. data/lib/all_futures/version.rb +3 -1
  29. data/lib/all_futures/versions.rb +80 -0
  30. data/lib/all_futures.rb +24 -1
  31. data/lib/vendor/ACTIVE_ENTITY_MIT-LICENSE +42 -0
  32. data/lib/vendor/README.md +8 -0
  33. data/lib/vendor/active_entity/aggregations.rb +277 -0
  34. data/lib/vendor/active_entity/associations/embeds/association.rb +134 -0
  35. data/lib/vendor/active_entity/associations/embeds/builder/association.rb +105 -0
  36. data/lib/vendor/active_entity/associations/embeds/builder/collection_association.rb +71 -0
  37. data/lib/vendor/active_entity/associations/embeds/builder/embedded_in.rb +38 -0
  38. data/lib/vendor/active_entity/associations/embeds/builder/embeds_many.rb +13 -0
  39. data/lib/vendor/active_entity/associations/embeds/builder/embeds_one.rb +16 -0
  40. data/lib/vendor/active_entity/associations/embeds/builder/singular_association.rb +28 -0
  41. data/lib/vendor/active_entity/associations/embeds/collection_association.rb +209 -0
  42. data/lib/vendor/active_entity/associations/embeds/collection_proxy.rb +310 -0
  43. data/lib/vendor/active_entity/associations/embeds/embedded_in_association.rb +31 -0
  44. data/lib/vendor/active_entity/associations/embeds/embeds_many_association.rb +15 -0
  45. data/lib/vendor/active_entity/associations/embeds/embeds_one_association.rb +26 -0
  46. data/lib/vendor/active_entity/associations/embeds/singular_association.rb +35 -0
  47. data/lib/vendor/active_entity/associations.rb +168 -0
  48. data/lib/vendor/active_entity/attribute_assignment.rb +87 -0
  49. data/lib/vendor/active_entity/attribute_decorators.rb +90 -0
  50. data/lib/vendor/active_entity/attribute_methods/before_type_cast.rb +78 -0
  51. data/lib/vendor/active_entity/attribute_methods/dirty.rb +180 -0
  52. data/lib/vendor/active_entity/attribute_methods/primary_key.rb +96 -0
  53. data/lib/vendor/active_entity/attribute_methods/query.rb +38 -0
  54. data/lib/vendor/active_entity/attribute_methods/read.rb +44 -0
  55. data/lib/vendor/active_entity/attribute_methods/serialization.rb +90 -0
  56. data/lib/vendor/active_entity/attribute_methods/time_zone_conversion.rb +93 -0
  57. data/lib/vendor/active_entity/attribute_methods/write.rb +54 -0
  58. data/lib/vendor/active_entity/attribute_methods.rb +833 -0
  59. data/lib/vendor/active_entity/attributes.rb +240 -0
  60. data/lib/vendor/active_entity/base.rb +293 -0
  61. data/lib/vendor/active_entity/callbacks.rb +21 -0
  62. data/lib/vendor/active_entity/coders/json.rb +15 -0
  63. data/lib/vendor/active_entity/coders/yaml_column.rb +50 -0
  64. data/lib/vendor/active_entity/core.rb +348 -0
  65. data/lib/vendor/active_entity/enum.rb +260 -0
  66. data/lib/vendor/active_entity/errors.rb +69 -0
  67. data/lib/vendor/active_entity/gem_version.rb +17 -0
  68. data/lib/vendor/active_entity/inheritance.rb +176 -0
  69. data/lib/vendor/active_entity/integration.rb +78 -0
  70. data/lib/vendor/active_entity/locale/en.yml +45 -0
  71. data/lib/vendor/active_entity/model_schema.rb +118 -0
  72. data/lib/vendor/active_entity/nested_attributes.rb +554 -0
  73. data/lib/vendor/active_entity/persistence.rb +20 -0
  74. data/lib/vendor/active_entity/railtie.rb +76 -0
  75. data/lib/vendor/active_entity/readonly_attributes.rb +55 -0
  76. data/lib/vendor/active_entity/reflection.rb +447 -0
  77. data/lib/vendor/active_entity/serialization.rb +28 -0
  78. data/lib/vendor/active_entity/store.rb +291 -0
  79. data/lib/vendor/active_entity/translation.rb +24 -0
  80. data/lib/vendor/active_entity/type/date.rb +9 -0
  81. data/lib/vendor/active_entity/type/date_time.rb +9 -0
  82. data/lib/vendor/active_entity/type/internal/timezone.rb +17 -0
  83. data/lib/vendor/active_entity/type/json.rb +30 -0
  84. data/lib/vendor/active_entity/type/modifiers/array.rb +72 -0
  85. data/lib/vendor/active_entity/type/modifiers/array_without_blank.rb +19 -0
  86. data/lib/vendor/active_entity/type/registry.rb +112 -0
  87. data/lib/vendor/active_entity/type/serialized.rb +71 -0
  88. data/lib/vendor/active_entity/type/text.rb +11 -0
  89. data/lib/vendor/active_entity/type/time.rb +21 -0
  90. data/lib/vendor/active_entity/type/unsigned_integer.rb +17 -0
  91. data/lib/vendor/active_entity/type.rb +75 -0
  92. data/lib/vendor/active_entity/validate_embeds_association.rb +300 -0
  93. data/lib/vendor/active_entity/validations/absence.rb +25 -0
  94. data/lib/vendor/active_entity/validations/associated.rb +60 -0
  95. data/lib/vendor/active_entity/validations/length.rb +26 -0
  96. data/lib/vendor/active_entity/validations/presence.rb +68 -0
  97. data/lib/vendor/active_entity/validations/subset.rb +76 -0
  98. data/lib/vendor/active_entity/validations/uniqueness_in_embeds.rb +91 -0
  99. data/lib/vendor/active_entity/validations/uniqueness_on_active_record.rb +234 -0
  100. data/lib/vendor/active_entity/validations.rb +47 -0
  101. data/lib/vendor/active_entity/version.rb +10 -0
  102. data/lib/vendor/active_entity.rb +99 -0
  103. data/lib/vendor/core_ext/array_without_blank.rb +48 -0
  104. metadata +175 -21
  105. data/lib/all_futures/all_futures.rb +0 -28
  106. data/lib/all_futures/config.rb +0 -14
@@ -0,0 +1,554 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/except"
4
+ require "active_support/core_ext/module/redefine_method"
5
+ require "active_support/core_ext/hash/indifferent_access"
6
+
7
+ module ActiveEntity
8
+ module NestedAttributes #:nodoc:
9
+ class TooManyRecords < ActiveEntityError
10
+ end
11
+
12
+ extend ActiveSupport::Concern
13
+
14
+ included do
15
+ class_attribute :nested_attributes_options, instance_writer: false, default: {}
16
+ end
17
+
18
+ # = Active Entity Nested Attributes
19
+ #
20
+ # Nested attributes allow you to save attributes on associated records
21
+ # through the parent. By default nested attribute updating is turned off
22
+ # and you can enable it using the accepts_nested_attributes_for class
23
+ # method. When you enable nested attributes an attribute writer is
24
+ # defined on the model.
25
+ #
26
+ # The attribute writer is named after the association, which means that
27
+ # in the following example, two new methods are added to your model:
28
+ #
29
+ # <tt>author_attributes=(attributes)</tt> and
30
+ # <tt>pages_attributes=(attributes)</tt>.
31
+ #
32
+ # class Book < ActiveEntity::Base
33
+ # has_one :author
34
+ # has_many :pages
35
+ #
36
+ # accepts_nested_attributes_for :author, :pages
37
+ # end
38
+ #
39
+ # Note that the <tt>:autosave</tt> option is automatically enabled on every
40
+ # association that accepts_nested_attributes_for is used for.
41
+ #
42
+ # === One-to-one
43
+ #
44
+ # Consider a Member model that has one Avatar:
45
+ #
46
+ # class Member < ActiveEntity::Base
47
+ # has_one :avatar
48
+ # accepts_nested_attributes_for :avatar
49
+ # end
50
+ #
51
+ # Enabling nested attributes on a one-to-one association allows you to
52
+ # create the member and avatar in one go:
53
+ #
54
+ # params = { member: { name: 'Jack', avatar_attributes: { icon: 'smiling' } } }
55
+ # member = Member.create(params[:member])
56
+ # member.avatar.id # => 2
57
+ # member.avatar.icon # => 'smiling'
58
+ #
59
+ # It also allows you to update the avatar through the member:
60
+ #
61
+ # params = { member: { avatar_attributes: { id: '2', icon: 'sad' } } }
62
+ # member.update params[:member]
63
+ # member.avatar.icon # => 'sad'
64
+ #
65
+ # If you want to update the current avatar without providing the id, you must add <tt>:update_only</tt> option.
66
+ #
67
+ # class Member < ActiveEntity::Base
68
+ # has_one :avatar
69
+ # accepts_nested_attributes_for :avatar, update_only: true
70
+ # end
71
+ #
72
+ # params = { member: { avatar_attributes: { icon: 'sad' } } }
73
+ # member.update params[:member]
74
+ # member.avatar.id # => 2
75
+ # member.avatar.icon # => 'sad'
76
+ #
77
+ # By default you will only be able to set and update attributes on the
78
+ # associated model. If you want to destroy the associated model through the
79
+ # attributes hash, you have to enable it first using the
80
+ # <tt>:allow_destroy</tt> option.
81
+ #
82
+ # class Member < ActiveEntity::Base
83
+ # has_one :avatar
84
+ # accepts_nested_attributes_for :avatar, allow_destroy: true
85
+ # end
86
+ #
87
+ # Now, when you add the <tt>_destroy</tt> key to the attributes hash, with a
88
+ # value that evaluates to +true+, you will destroy the associated model:
89
+ #
90
+ # member.avatar_attributes = { id: '2', _destroy: '1' }
91
+ # member.avatar.marked_for_destruction? # => true
92
+ # member.save
93
+ # member.reload.avatar # => nil
94
+ #
95
+ # Note that the model will _not_ be destroyed until the parent is saved.
96
+ #
97
+ # Also note that the model will not be destroyed unless you also specify
98
+ # its id in the updated hash.
99
+ #
100
+ # === One-to-many
101
+ #
102
+ # Consider a member that has a number of posts:
103
+ #
104
+ # class Member < ActiveEntity::Base
105
+ # has_many :posts
106
+ # accepts_nested_attributes_for :posts
107
+ # end
108
+ #
109
+ # You can now set or update attributes on the associated posts through
110
+ # an attribute hash for a member: include the key +:posts_attributes+
111
+ # with an array of hashes of post attributes as a value.
112
+ #
113
+ # For each hash that does _not_ have an <tt>id</tt> key a new record will
114
+ # be instantiated, unless the hash also contains a <tt>_destroy</tt> key
115
+ # that evaluates to +true+.
116
+ #
117
+ # params = { member: {
118
+ # name: 'joe', posts_attributes: [
119
+ # { title: 'Kari, the awesome Ruby documentation browser!' },
120
+ # { title: 'The egalitarian assumption of the modern citizen' },
121
+ # { title: '', _destroy: '1' } # this will be ignored
122
+ # ]
123
+ # }}
124
+ #
125
+ # member = Member.create(params[:member])
126
+ # member.posts.length # => 2
127
+ # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
128
+ # member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
129
+ #
130
+ # You may also set a +:reject_if+ proc to silently ignore any new record
131
+ # hashes if they fail to pass your criteria. For example, the previous
132
+ # example could be rewritten as:
133
+ #
134
+ # class Member < ActiveEntity::Base
135
+ # has_many :posts
136
+ # accepts_nested_attributes_for :posts, reject_if: proc { |attributes| attributes['title'].blank? }
137
+ # end
138
+ #
139
+ # params = { member: {
140
+ # name: 'joe', posts_attributes: [
141
+ # { title: 'Kari, the awesome Ruby documentation browser!' },
142
+ # { title: 'The egalitarian assumption of the modern citizen' },
143
+ # { title: '' } # this will be ignored because of the :reject_if proc
144
+ # ]
145
+ # }}
146
+ #
147
+ # member = Member.create(params[:member])
148
+ # member.posts.length # => 2
149
+ # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
150
+ # member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
151
+ #
152
+ # Alternatively, +:reject_if+ also accepts a symbol for using methods:
153
+ #
154
+ # class Member < ActiveEntity::Base
155
+ # has_many :posts
156
+ # accepts_nested_attributes_for :posts, reject_if: :new_record?
157
+ # end
158
+ #
159
+ # class Member < ActiveEntity::Base
160
+ # has_many :posts
161
+ # accepts_nested_attributes_for :posts, reject_if: :reject_posts
162
+ #
163
+ # def reject_posts(attributes)
164
+ # attributes['title'].blank?
165
+ # end
166
+ # end
167
+ #
168
+ # If the hash contains an <tt>id</tt> key that matches an already
169
+ # associated record, the matching record will be modified:
170
+ #
171
+ # member.attributes = {
172
+ # name: 'Joe',
173
+ # posts_attributes: [
174
+ # { id: 1, title: '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' },
175
+ # { id: 2, title: '[UPDATED] other post' }
176
+ # ]
177
+ # }
178
+ #
179
+ # member.posts.first.title # => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!'
180
+ # member.posts.second.title # => '[UPDATED] other post'
181
+ #
182
+ # However, the above applies if the parent model is being updated as well.
183
+ # For example, If you wanted to create a +member+ named _joe_ and wanted to
184
+ # update the +posts+ at the same time, that would give an
185
+ # ActiveEntity::RecordNotFound error.
186
+ #
187
+ # By default the associated records are protected from being destroyed. If
188
+ # you want to destroy any of the associated records through the attributes
189
+ # hash, you have to enable it first using the <tt>:allow_destroy</tt>
190
+ # option. This will allow you to also use the <tt>_destroy</tt> key to
191
+ # destroy existing records:
192
+ #
193
+ # class Member < ActiveEntity::Base
194
+ # has_many :posts
195
+ # accepts_nested_attributes_for :posts, allow_destroy: true
196
+ # end
197
+ #
198
+ # params = { member: {
199
+ # posts_attributes: [{ id: '2', _destroy: '1' }]
200
+ # }}
201
+ #
202
+ # member.attributes = params[:member]
203
+ # member.posts.detect { |p| p.id == 2 }.marked_for_destruction? # => true
204
+ # member.posts.length # => 2
205
+ # member.save
206
+ # member.reload.posts.length # => 1
207
+ #
208
+ # Nested attributes for an associated collection can also be passed in
209
+ # the form of a hash of hashes instead of an array of hashes:
210
+ #
211
+ # Member.create(
212
+ # name: 'joe',
213
+ # posts_attributes: {
214
+ # first: { title: 'Foo' },
215
+ # second: { title: 'Bar' }
216
+ # }
217
+ # )
218
+ #
219
+ # has the same effect as
220
+ #
221
+ # Member.create(
222
+ # name: 'joe',
223
+ # posts_attributes: [
224
+ # { title: 'Foo' },
225
+ # { title: 'Bar' }
226
+ # ]
227
+ # )
228
+ #
229
+ # The keys of the hash which is the value for +:posts_attributes+ are
230
+ # ignored in this case.
231
+ # However, it is not allowed to use <tt>'id'</tt> or <tt>:id</tt> for one of
232
+ # such keys, otherwise the hash will be wrapped in an array and
233
+ # interpreted as an attribute hash for a single post.
234
+ #
235
+ # Passing attributes for an associated collection in the form of a hash
236
+ # of hashes can be used with hashes generated from HTTP/HTML parameters,
237
+ # where there may be no natural way to submit an array of hashes.
238
+ #
239
+ # === Saving
240
+ #
241
+ # All changes to models, including the destruction of those marked for
242
+ # destruction, are saved and destroyed automatically and atomically when
243
+ # the parent model is saved. This happens inside the transaction initiated
244
+ # by the parent's save method. See ActiveEntity::AutosaveAssociation.
245
+ #
246
+ # === Validating the presence of a parent model
247
+ #
248
+ # If you want to validate that a child record is associated with a parent
249
+ # record, you can use the +validates_presence_of+ method and the +:inverse_of+
250
+ # key as this example illustrates:
251
+ #
252
+ # class Member < ActiveEntity::Base
253
+ # has_many :posts, inverse_of: :member
254
+ # accepts_nested_attributes_for :posts
255
+ # end
256
+ #
257
+ # class Post < ActiveEntity::Base
258
+ # belongs_to :member, inverse_of: :posts
259
+ # validates_presence_of :member
260
+ # end
261
+ #
262
+ # Note that if you do not specify the +:inverse_of+ option, then
263
+ # Active Entity will try to automatically guess the inverse association
264
+ # based on heuristics.
265
+ #
266
+ # For one-to-one nested associations, if you build the new (in-memory)
267
+ # child object yourself before assignment, then this module will not
268
+ # overwrite it, e.g.:
269
+ #
270
+ # class Member < ActiveEntity::Base
271
+ # has_one :avatar
272
+ # accepts_nested_attributes_for :avatar
273
+ #
274
+ # def avatar
275
+ # super || build_avatar(width: 200)
276
+ # end
277
+ # end
278
+ #
279
+ # member = Member.new
280
+ # member.avatar_attributes = {icon: 'sad'}
281
+ # member.avatar.width # => 200
282
+ module ClassMethods
283
+ REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == "_destroy" || value.blank? } }
284
+
285
+ # Defines an attributes writer for the specified association(s).
286
+ #
287
+ # Supported options:
288
+ # [:allow_destroy]
289
+ # If true, destroys any members from the attributes hash with a
290
+ # <tt>_destroy</tt> key and a value that evaluates to +true+
291
+ # (e.g. 1, '1', true, or 'true'). This option is off by default.
292
+ # [:reject_if]
293
+ # Allows you to specify a Proc or a Symbol pointing to a method
294
+ # that checks whether a record should be built for a certain attribute
295
+ # hash. The hash is passed to the supplied Proc or the method
296
+ # and it should return either +true+ or +false+. When no +:reject_if+
297
+ # is specified, a record will be built for all attribute hashes that
298
+ # do not have a <tt>_destroy</tt> value that evaluates to true.
299
+ # Passing <tt>:all_blank</tt> instead of a Proc will create a proc
300
+ # that will reject a record where all the attributes are blank excluding
301
+ # any value for +_destroy+.
302
+ # [:limit]
303
+ # Allows you to specify the maximum number of associated records that
304
+ # can be processed with the nested attributes. Limit also can be specified
305
+ # as a Proc or a Symbol pointing to a method that should return a number.
306
+ # If the size of the nested attributes array exceeds the specified limit,
307
+ # NestedAttributes::TooManyRecords exception is raised. If omitted, any
308
+ # number of associations can be processed.
309
+ # Note that the +:limit+ option is only applicable to one-to-many
310
+ # associations.
311
+ # [:update_only]
312
+ # For a one-to-one association, this option allows you to specify how
313
+ # nested attributes are going to be used when an associated record already
314
+ # exists. In general, an existing record may either be updated with the
315
+ # new set of attribute values or be replaced by a wholly new record
316
+ # containing those values. By default the +:update_only+ option is +false+
317
+ # and the nested attributes are used to update the existing record only
318
+ # if they include the record's <tt>:id</tt> value. Otherwise a new
319
+ # record will be instantiated and used to replace the existing one.
320
+ # However if the +:update_only+ option is +true+, the nested attributes
321
+ # are used to update the record's attributes always, regardless of
322
+ # whether the <tt>:id</tt> is present. The option is ignored for collection
323
+ # associations.
324
+ #
325
+ # Examples:
326
+ # # creates avatar_attributes=
327
+ # accepts_nested_attributes_for :avatar, reject_if: proc { |attributes| attributes['name'].blank? }
328
+ # # creates avatar_attributes=
329
+ # accepts_nested_attributes_for :avatar, reject_if: :all_blank
330
+ # # creates avatar_attributes= and posts_attributes=
331
+ # accepts_nested_attributes_for :avatar, :posts, allow_destroy: true
332
+ def accepts_nested_attributes_for(*attr_names)
333
+ options = { allow_destroy: false, update_only: false }
334
+ options.update(attr_names.extract_options!)
335
+ options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only)
336
+ options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
337
+
338
+ attr_names.each do |association_name|
339
+ if reflection = _reflect_on_association(association_name)
340
+ nested_attributes_options = self.nested_attributes_options.dup
341
+ nested_attributes_options[association_name.to_sym] = options
342
+ self.nested_attributes_options = nested_attributes_options
343
+
344
+ type = (reflection.collection? ? :collection : :one_to_one)
345
+ generate_association_writer(association_name, type)
346
+ else
347
+ raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
348
+ end
349
+ end
350
+ end
351
+
352
+ private
353
+
354
+ # Generates a writer method for this association. Serves as a point for
355
+ # accessing the objects in the association. For example, this method
356
+ # could generate the following:
357
+ #
358
+ # def pirate_attributes=(attributes)
359
+ # assign_nested_attributes_for_one_to_one_association(:pirate, attributes)
360
+ # end
361
+ #
362
+ # This redirects the attempts to write objects in an association through
363
+ # the helper methods defined below. Makes it seem like the nested
364
+ # associations are just regular associations.
365
+ def generate_association_writer(association_name, type)
366
+ generated_association_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
367
+ silence_redefinition_of_method :#{association_name}_attributes=
368
+ def #{association_name}_attributes=(attributes)
369
+ assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
370
+ end
371
+ eoruby
372
+ end
373
+ end
374
+
375
+ # Returns ActiveEntity::AutosaveAssociation::marked_for_destruction? It's
376
+ # used in conjunction with fields_for to build a form element for the
377
+ # destruction of this association.
378
+ #
379
+ # See ActionView::Helpers::FormHelper::fields_for for more info.
380
+ def _destroy
381
+ marked_for_destruction?
382
+ end
383
+
384
+ private
385
+
386
+ # Attribute hash keys that should not be assigned as normal attributes.
387
+ # These hash keys are nested attributes implementation details.
388
+ UNASSIGNABLE_KEYS = %w( id _destroy )
389
+
390
+ # Assigns the given attributes to the association.
391
+ #
392
+ # If an associated record does not yet exist, one will be instantiated. If
393
+ # an associated record already exists, the method's behavior depends on
394
+ # the value of the update_only option. If update_only is +false+ and the
395
+ # given attributes include an <tt>:id</tt> that matches the existing record's
396
+ # id, then the existing record will be modified. If no <tt>:id</tt> is provided
397
+ # it will be replaced with a new record. If update_only is +true+ the existing
398
+ # record will be modified regardless of whether an <tt>:id</tt> is provided.
399
+ #
400
+ # If the given attributes include a matching <tt>:id</tt> attribute, or
401
+ # update_only is true, and a <tt>:_destroy</tt> key set to a truthy value,
402
+ # then the existing record will be marked for destruction.
403
+ def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
404
+ if attributes.respond_to?(:permitted?)
405
+ attributes = attributes.to_h
406
+ end
407
+ attributes = attributes.with_indifferent_access
408
+ existing_record = send(association_name)
409
+
410
+ assignable_attributes = attributes.except(*UNASSIGNABLE_KEYS)
411
+
412
+ if existing_record
413
+ existing_record.assign_attributes(assignable_attributes)
414
+ association(association_name).initialize_attributes(existing_record)
415
+ else
416
+ method = :"build_#{association_name}"
417
+ if respond_to?(method)
418
+ send(method, assignable_attributes)
419
+ else
420
+ raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?"
421
+ end
422
+ end
423
+ end
424
+
425
+ # Assigns the given attributes to the collection association.
426
+ #
427
+ # Hashes with an <tt>:id</tt> value matching an existing associated record
428
+ # will update that record. Hashes without an <tt>:id</tt> value will build
429
+ # a new record for the association. Hashes with a matching <tt>:id</tt>
430
+ # value and a <tt>:_destroy</tt> key set to a truthy value will mark the
431
+ # matched record for destruction.
432
+ #
433
+ # For example:
434
+ #
435
+ # assign_nested_attributes_for_collection_association(:people, {
436
+ # '1' => { id: '1', name: 'Peter' },
437
+ # '2' => { name: 'John' },
438
+ # '3' => { id: '2', _destroy: true }
439
+ # })
440
+ #
441
+ # Will update the name of the Person with ID 1, build a new associated
442
+ # person with the name 'John', and mark the associated Person with ID 2
443
+ # for destruction.
444
+ #
445
+ # Also accepts an Array of attribute hashes:
446
+ #
447
+ # assign_nested_attributes_for_collection_association(:people, [
448
+ # { id: '1', name: 'Peter' },
449
+ # { name: 'John' },
450
+ # { id: '2', _destroy: true }
451
+ # ])
452
+ def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
453
+ options = nested_attributes_options[association_name]
454
+ if attributes_collection.respond_to?(:permitted?)
455
+ attributes_collection = attributes_collection.to_h
456
+ end
457
+
458
+ unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
459
+ raise ArgumentError, "Hash or Array expected for attribute `#{association_name}`, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
460
+ end
461
+
462
+ check_record_limit!(options[:limit], attributes_collection)
463
+
464
+ if attributes_collection.is_a? Hash
465
+ keys = attributes_collection.keys
466
+ attributes_collection = if keys.include?("id") || keys.include?(:id)
467
+ [attributes_collection]
468
+ else
469
+ attributes_collection.values
470
+ end
471
+ end
472
+
473
+ association = association(association_name)
474
+
475
+ association.target.clear
476
+
477
+ attributes_collection.each do |attributes|
478
+ if attributes.respond_to?(:permitted?)
479
+ attributes = attributes.to_h
480
+ end
481
+ attributes = attributes.with_indifferent_access
482
+ association.reader.build(attributes.except(*UNASSIGNABLE_KEYS))
483
+ end
484
+ end
485
+
486
+ # Takes in a limit and checks if the attributes_collection has too many
487
+ # records. It accepts limit in the form of symbol, proc, or
488
+ # number-like object (anything that can be compared with an integer).
489
+ #
490
+ # Raises TooManyRecords error if the attributes_collection is
491
+ # larger than the limit.
492
+ def check_record_limit!(limit, attributes_collection)
493
+ if limit
494
+ limit = \
495
+ case limit
496
+ when Symbol
497
+ send(limit)
498
+ when Proc
499
+ limit.call
500
+ else
501
+ limit
502
+ end
503
+
504
+ if limit && attributes_collection.size > limit
505
+ raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead."
506
+ end
507
+ end
508
+ end
509
+
510
+ # Updates a record with the +attributes+ or marks it for destruction if
511
+ # +allow_destroy+ is +true+ and has_destroy_flag? returns +true+.
512
+ def assign_to_or_mark_for_destruction(record, attributes, allow_destroy)
513
+ record.assign_attributes(attributes.except(*UNASSIGNABLE_KEYS))
514
+ record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy
515
+ end
516
+
517
+ # Determines if a hash contains a truthy _destroy key.
518
+ def has_destroy_flag?(hash)
519
+ Type::Boolean.new.cast(hash["_destroy"])
520
+ end
521
+
522
+ # Determines if a new record should be rejected by checking
523
+ # has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this
524
+ # association and evaluates to +true+.
525
+ def reject_new_record?(association_name, attributes)
526
+ will_be_destroyed?(association_name, attributes) || call_reject_if(association_name, attributes)
527
+ end
528
+
529
+ # Determines if a record with the particular +attributes+ should be
530
+ # rejected by calling the reject_if Symbol or Proc (if defined).
531
+ # The reject_if option is defined by +accepts_nested_attributes_for+.
532
+ #
533
+ # Returns false if there is a +destroy_flag+ on the attributes.
534
+ def call_reject_if(association_name, attributes)
535
+ return false if will_be_destroyed?(association_name, attributes)
536
+
537
+ case callback = nested_attributes_options[association_name][:reject_if]
538
+ when Symbol
539
+ method(callback).arity == 0 ? send(callback) : send(callback, attributes)
540
+ when Proc
541
+ callback.call(attributes)
542
+ end
543
+ end
544
+
545
+ # Only take into account the destroy flag if <tt>:allow_destroy</tt> is true
546
+ def will_be_destroyed?(association_name, attributes)
547
+ allow_destroy?(association_name) && has_destroy_flag?(attributes)
548
+ end
549
+
550
+ def allow_destroy?(association_name)
551
+ nested_attributes_options[association_name][:allow_destroy]
552
+ end
553
+ end
554
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ # = Active Entity \Persistence
5
+ module Persistence
6
+ extend ActiveSupport::Concern
7
+
8
+ def new_record?
9
+ true
10
+ end
11
+
12
+ def destroyed?
13
+ false
14
+ end
15
+
16
+ def persisted?
17
+ false
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_entity"
4
+ require "rails"
5
+ require "active_support/core_ext/object/try"
6
+ require "active_model/railtie"
7
+
8
+ # For now, action_controller must always be present with
9
+ # Rails, so let's make sure that it gets required before
10
+ # here. This is needed for correctly setting up the middleware.
11
+ # In the future, this might become an optional require.
12
+ require "action_controller/railtie"
13
+
14
+ module ActiveEntity
15
+ # = Active Entity Railtie
16
+ class Railtie < Rails::Railtie # :nodoc:
17
+ config.active_entity = ActiveSupport::OrderedOptions.new
18
+
19
+ config.eager_load_namespaces << ActiveEntity
20
+
21
+ # When loading console, force ActiveEntity::Base to be loaded
22
+ # to avoid cross references when loading a constant for the
23
+ # first time. Also, make it output to STDERR.
24
+ console do |_app|
25
+ require "active_entity/base"
26
+ unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDERR, STDOUT)
27
+ console = ActiveSupport::Logger.new(STDERR)
28
+ Rails.logger.extend ActiveSupport::Logger.broadcast console
29
+ end
30
+ end
31
+
32
+ runner do
33
+ require "active_entity/base"
34
+ end
35
+
36
+ initializer "active_entity.initialize_timezone" do
37
+ ActiveSupport.on_load(:active_entity) do
38
+ self.time_zone_aware_attributes = true
39
+ self.default_timezone = :utc
40
+ end
41
+ end
42
+
43
+ initializer "active_entity.logger" do
44
+ ActiveSupport.on_load(:active_entity) { self.logger ||= ::Rails.logger }
45
+ end
46
+
47
+
48
+ initializer "active_entity.define_attribute_methods" do |app|
49
+ config.after_initialize do
50
+ ActiveSupport.on_load(:active_entity) do
51
+ if app.config.eager_load
52
+ descendants.each do |model|
53
+ model.define_attribute_methods
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ initializer "active_entity.set_configs" do |app|
61
+ ActiveSupport.on_load(:active_entity) do
62
+ configs = app.config.active_entity
63
+
64
+ configs.each do |k, v|
65
+ send "#{k}=", v
66
+ end
67
+ end
68
+ end
69
+
70
+ initializer "active_entity.set_filter_attributes" do
71
+ ActiveSupport.on_load(:active_entity) do
72
+ self.filter_attributes += Rails.application.config.filter_parameters
73
+ end
74
+ end
75
+ end
76
+ end