activeentity 0.0.1.beta1

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 (74) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +42 -0
  3. data/README.md +145 -0
  4. data/Rakefile +29 -0
  5. data/lib/active_entity.rb +73 -0
  6. data/lib/active_entity/aggregations.rb +276 -0
  7. data/lib/active_entity/associations.rb +146 -0
  8. data/lib/active_entity/associations/embedded/association.rb +134 -0
  9. data/lib/active_entity/associations/embedded/builder/association.rb +100 -0
  10. data/lib/active_entity/associations/embedded/builder/collection_association.rb +69 -0
  11. data/lib/active_entity/associations/embedded/builder/embedded_in.rb +38 -0
  12. data/lib/active_entity/associations/embedded/builder/embeds_many.rb +13 -0
  13. data/lib/active_entity/associations/embedded/builder/embeds_one.rb +16 -0
  14. data/lib/active_entity/associations/embedded/builder/singular_association.rb +28 -0
  15. data/lib/active_entity/associations/embedded/collection_association.rb +188 -0
  16. data/lib/active_entity/associations/embedded/collection_proxy.rb +310 -0
  17. data/lib/active_entity/associations/embedded/embedded_in_association.rb +31 -0
  18. data/lib/active_entity/associations/embedded/embeds_many_association.rb +15 -0
  19. data/lib/active_entity/associations/embedded/embeds_one_association.rb +19 -0
  20. data/lib/active_entity/associations/embedded/singular_association.rb +35 -0
  21. data/lib/active_entity/attribute_assignment.rb +85 -0
  22. data/lib/active_entity/attribute_decorators.rb +90 -0
  23. data/lib/active_entity/attribute_methods.rb +330 -0
  24. data/lib/active_entity/attribute_methods/before_type_cast.rb +78 -0
  25. data/lib/active_entity/attribute_methods/primary_key.rb +98 -0
  26. data/lib/active_entity/attribute_methods/query.rb +35 -0
  27. data/lib/active_entity/attribute_methods/read.rb +47 -0
  28. data/lib/active_entity/attribute_methods/serialization.rb +90 -0
  29. data/lib/active_entity/attribute_methods/time_zone_conversion.rb +91 -0
  30. data/lib/active_entity/attribute_methods/write.rb +63 -0
  31. data/lib/active_entity/attributes.rb +165 -0
  32. data/lib/active_entity/base.rb +303 -0
  33. data/lib/active_entity/coders/json.rb +15 -0
  34. data/lib/active_entity/coders/yaml_column.rb +50 -0
  35. data/lib/active_entity/core.rb +281 -0
  36. data/lib/active_entity/define_callbacks.rb +17 -0
  37. data/lib/active_entity/enum.rb +234 -0
  38. data/lib/active_entity/errors.rb +80 -0
  39. data/lib/active_entity/gem_version.rb +17 -0
  40. data/lib/active_entity/inheritance.rb +278 -0
  41. data/lib/active_entity/integration.rb +78 -0
  42. data/lib/active_entity/locale/en.yml +45 -0
  43. data/lib/active_entity/model_schema.rb +115 -0
  44. data/lib/active_entity/nested_attributes.rb +592 -0
  45. data/lib/active_entity/readonly_attributes.rb +47 -0
  46. data/lib/active_entity/reflection.rb +441 -0
  47. data/lib/active_entity/serialization.rb +25 -0
  48. data/lib/active_entity/store.rb +242 -0
  49. data/lib/active_entity/translation.rb +24 -0
  50. data/lib/active_entity/type.rb +73 -0
  51. data/lib/active_entity/type/date.rb +9 -0
  52. data/lib/active_entity/type/date_time.rb +9 -0
  53. data/lib/active_entity/type/decimal_without_scale.rb +15 -0
  54. data/lib/active_entity/type/hash_lookup_type_map.rb +25 -0
  55. data/lib/active_entity/type/internal/timezone.rb +17 -0
  56. data/lib/active_entity/type/json.rb +30 -0
  57. data/lib/active_entity/type/modifiers/array.rb +72 -0
  58. data/lib/active_entity/type/registry.rb +92 -0
  59. data/lib/active_entity/type/serialized.rb +71 -0
  60. data/lib/active_entity/type/text.rb +11 -0
  61. data/lib/active_entity/type/time.rb +21 -0
  62. data/lib/active_entity/type/type_map.rb +62 -0
  63. data/lib/active_entity/type/unsigned_integer.rb +17 -0
  64. data/lib/active_entity/validate_embedded_association.rb +305 -0
  65. data/lib/active_entity/validations.rb +50 -0
  66. data/lib/active_entity/validations/absence.rb +25 -0
  67. data/lib/active_entity/validations/associated.rb +60 -0
  68. data/lib/active_entity/validations/length.rb +26 -0
  69. data/lib/active_entity/validations/presence.rb +68 -0
  70. data/lib/active_entity/validations/subset.rb +76 -0
  71. data/lib/active_entity/validations/uniqueness_in_embedding.rb +99 -0
  72. data/lib/active_entity/version.rb +10 -0
  73. data/lib/tasks/active_entity_tasks.rake +6 -0
  74. metadata +155 -0
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module ReadonlyAttributes
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class_attribute :_attr_readonly, instance_accessor: false, default: []
9
+ end
10
+
11
+ def disable_readonly!
12
+ @_readonly_enabled = false
13
+ end
14
+
15
+ def enable_readonly!
16
+ @_readonly_enabled = true
17
+ end
18
+
19
+ def enable_readonly
20
+ return unless block_given?
21
+
22
+ disable_readonly!
23
+ yield self
24
+ enable_readonly!
25
+
26
+ self
27
+ end
28
+
29
+ def _readonly_enabled
30
+ @_readonly_enabled
31
+ end
32
+ alias readonly_enabled? _readonly_enabled
33
+
34
+ module ClassMethods
35
+ # Attributes listed as readonly will be used to create a new record but update operations will
36
+ # ignore these fields.
37
+ def attr_readonly(*attributes)
38
+ self._attr_readonly = Set.new(attributes.map(&:to_s)) + (_attr_readonly || [])
39
+ end
40
+
41
+ # Returns an array of all the attributes that have been specified as readonly.
42
+ def readonly_attributes
43
+ _attr_readonly
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,441 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string/filters"
4
+ require "concurrent/map"
5
+
6
+ module ActiveEntity
7
+ # = Active Entity Reflection
8
+ module Reflection # :nodoc:
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ class_attribute :_reflections, instance_writer: false, default: {}
13
+ class_attribute :aggregate_reflections, instance_writer: false, default: {}
14
+ end
15
+
16
+ class << self
17
+ def create(macro, name, scope, options, ar_or_ae)
18
+ reflection = reflection_class_for(macro).new(name, scope, options, ar_or_ae)
19
+ options[:through] ? ThroughReflection.new(reflection) : reflection
20
+ end
21
+
22
+ def add_reflection(ar_or_ae, name, reflection)
23
+ ar_or_ae.clear_reflections_cache
24
+ name = name.to_s
25
+ ar_or_ae._reflections = ar_or_ae._reflections.except(name).merge!(name => reflection)
26
+ end
27
+
28
+ def add_aggregate_reflection(ae, name, reflection)
29
+ ae.aggregate_reflections = ae.aggregate_reflections.merge(name.to_s => reflection)
30
+ end
31
+
32
+ private
33
+ def reflection_class_for(macro)
34
+ case macro
35
+ when :composed_of
36
+ AggregateReflection
37
+ when :embedded_in
38
+ EmbeddedInReflection
39
+ when :embeds_one
40
+ EmbedsOneReflection
41
+ when :embeds_many
42
+ EmbedsManyReflection
43
+ else
44
+ raise "Unsupported Macro: #{macro}"
45
+ end
46
+ end
47
+ end
48
+
49
+ # \Reflection enables the ability to examine the associations and aggregations of
50
+ # Active Entity classes and objects. This information, for example,
51
+ # can be used in a form builder that takes an Active Entity object
52
+ # and creates input fields for all of the attributes depending on their type
53
+ # and displays the associations to other objects.
54
+ #
55
+ # MacroReflection class has info for AggregateReflection and AssociationReflection
56
+ # classes.
57
+ module ClassMethods
58
+ # Returns an array of AggregateReflection objects for all the aggregations in the class.
59
+ def reflect_on_all_aggregations
60
+ aggregate_reflections.values
61
+ end
62
+
63
+ # Returns the AggregateReflection object for the named +aggregation+ (use the symbol).
64
+ #
65
+ # Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection
66
+ #
67
+ def reflect_on_aggregation(aggregation)
68
+ aggregate_reflections[aggregation.to_s]
69
+ end
70
+
71
+ # Returns a Hash of name of the reflection as the key and an AssociationReflection as the value.
72
+ #
73
+ # Account.reflections # => {"balance" => AggregateReflection}
74
+ #
75
+ def reflections
76
+ @__reflections ||= begin
77
+ ref = {}
78
+
79
+ _reflections.each do |name, reflection|
80
+ parent_reflection = reflection.parent_reflection
81
+
82
+ if parent_reflection
83
+ parent_name = parent_reflection.name
84
+ ref[parent_name.to_s] = parent_reflection
85
+ else
86
+ ref[name] = reflection
87
+ end
88
+ end
89
+
90
+ ref
91
+ end
92
+ end
93
+
94
+ # Returns an array of AssociationReflection objects for all the
95
+ # associations in the class. If you only want to reflect on a certain
96
+ # association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>,
97
+ # <tt>:belongs_to</tt>) as the first parameter.
98
+ #
99
+ # Example:
100
+ #
101
+ # Account.reflect_on_all_associations # returns an array of all associations
102
+ # Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
103
+ #
104
+ def reflect_on_all_associations(macro = nil)
105
+ association_reflections = reflections.values
106
+ association_reflections.select! { |reflection| reflection.macro == macro } if macro
107
+ association_reflections
108
+ end
109
+
110
+ # Returns the AssociationReflection object for the +association+ (use the symbol).
111
+ #
112
+ # Account.reflect_on_association(:owner) # returns the owner AssociationReflection
113
+ # Invoice.reflect_on_association(:line_items).macro # returns :has_many
114
+ #
115
+ def reflect_on_association(association)
116
+ reflections[association.to_s]
117
+ end
118
+
119
+ def _reflect_on_association(association) #:nodoc:
120
+ _reflections[association.to_s]
121
+ end
122
+
123
+ def clear_reflections_cache # :nodoc:
124
+ @__reflections = nil
125
+ end
126
+ end
127
+
128
+ # Holds all the methods that are shared between MacroReflection and ThroughReflection.
129
+ #
130
+ # AbstractReflection
131
+ # MacroReflection
132
+ # AggregateReflection
133
+ # AssociationReflection
134
+ # HasManyReflection
135
+ # HasOneReflection
136
+ # BelongsToReflection
137
+ # HasAndBelongsToManyReflection
138
+ # ThroughReflection
139
+ # PolymorphicReflection
140
+ # RuntimeReflection
141
+
142
+ class AbstractReflection
143
+ def embedded?
144
+ false
145
+ end
146
+ end
147
+
148
+ class AbstractEmbeddedReflection < AbstractReflection # :nodoc:
149
+ def embedded?
150
+ true
151
+ end
152
+
153
+ def through_reflection?
154
+ false
155
+ end
156
+
157
+ # Returns a new, unsaved instance of the associated class. +attributes+ will
158
+ # be passed to the class's constructor.
159
+ def build_association(attributes, &block)
160
+ klass.new(attributes, &block)
161
+ end
162
+
163
+ # Returns the class name for the macro.
164
+ #
165
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt>
166
+ # <tt>has_many :clients</tt> returns <tt>'Client'</tt>
167
+ def class_name
168
+ @class_name ||= (options[:class_name] || derive_class_name).to_s
169
+ end
170
+
171
+ def inverse_of
172
+ return unless inverse_name
173
+
174
+ @inverse_of ||= klass._reflect_on_association inverse_name
175
+ end
176
+
177
+ def check_validity_of_inverse!
178
+ if has_inverse? && inverse_of.nil?
179
+ raise InverseOfAssociationNotFoundError.new(self)
180
+ end
181
+ end
182
+
183
+ protected
184
+ def actual_source_reflection # FIXME: this is a horrible name
185
+ self
186
+ end
187
+ end
188
+
189
+ # Base class for AggregateReflection and AssociationReflection. Objects of
190
+ # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
191
+ class MacroEmbeddedReflection < AbstractEmbeddedReflection
192
+ # Returns the name of the macro.
193
+ #
194
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:balance</tt>
195
+ # <tt>has_many :clients</tt> returns <tt>:clients</tt>
196
+ attr_reader :name
197
+
198
+ # Returns the hash of options used for the macro.
199
+ #
200
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>{ class_name: "Money" }</tt>
201
+ # <tt>has_many :clients</tt> returns <tt>{}</tt>
202
+ attr_reader :options
203
+
204
+ attr_reader :active_entity
205
+
206
+ def initialize(name, scope, options, active_entity)
207
+ @name = name
208
+ @scope = scope
209
+ @options = options
210
+ @active_entity = active_entity
211
+ @klass = options[:anonymous_class]
212
+ end
213
+
214
+ # Returns the class for the macro.
215
+ #
216
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class
217
+ # <tt>has_many :clients</tt> returns the Client class
218
+ #
219
+ # class Company < ActiveEntity::Base
220
+ # has_many :clients
221
+ # end
222
+ #
223
+ # Company.reflect_on_association(:clients).klass
224
+ # # => Client
225
+ #
226
+ # <b>Note:</b> Do not call +klass.new+ or +klass.create+ to instantiate
227
+ # a new association object. Use +build_association+ or +create_association+
228
+ # instead. This allows plugins to hook into association object creation.
229
+ def klass
230
+ @klass ||= compute_class(class_name)
231
+ end
232
+
233
+ def compute_class(name)
234
+ name.constantize
235
+ end
236
+
237
+ # Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_entity+ attribute,
238
+ # and +other_aggregation+ has an options hash assigned to it.
239
+ def ==(other_aggregation)
240
+ super ||
241
+ other_aggregation.kind_of?(self.class) &&
242
+ name == other_aggregation.name &&
243
+ !other_aggregation.options.nil? &&
244
+ active_entity == other_aggregation.active_entity
245
+ end
246
+
247
+ private
248
+ def derive_class_name
249
+ name.to_s.camelize
250
+ end
251
+ end
252
+
253
+ # Holds all the metadata about an aggregation as it was specified in the
254
+ # Active Entity class.
255
+ class AggregateReflection < MacroEmbeddedReflection #:nodoc:
256
+ def mapping
257
+ mapping = options[:mapping] || [name, name]
258
+ mapping.first.is_a?(Array) ? mapping : [mapping]
259
+ end
260
+ end
261
+
262
+ # Holds all the metadata about an association as it was specified in the
263
+ # Active Entity class.
264
+ class EmbeddedAssociationReflection < MacroEmbeddedReflection #:nodoc:
265
+ def compute_class(name)
266
+ active_entity.send(:compute_type, name)
267
+ end
268
+
269
+ attr_reader :type
270
+ attr_accessor :parent_reflection # Reflection
271
+
272
+ def initialize(name, scope, options, active_entity)
273
+ super
274
+ @constructable = calculate_constructable(macro, options)
275
+
276
+ if options[:class_name] && options[:class_name].class == Class
277
+ raise ArgumentError, "A class was passed to `:class_name` but we are expecting a string."
278
+ end
279
+ end
280
+
281
+ def constructable? # :nodoc:
282
+ @constructable
283
+ end
284
+
285
+ def check_validity!
286
+ check_validity_of_inverse!
287
+ end
288
+
289
+ def nested?
290
+ false
291
+ end
292
+
293
+ def has_inverse?
294
+ inverse_name
295
+ end
296
+
297
+ # Returns the macro type.
298
+ #
299
+ # <tt>has_many :clients</tt> returns <tt>:has_many</tt>
300
+ def macro; raise NotImplementedError; end
301
+
302
+ # Returns whether or not this association reflection is for a collection
303
+ # association. Returns +true+ if the +macro+ is either +has_many+ or
304
+ # +has_and_belongs_to_many+, +false+ otherwise.
305
+ def collection?
306
+ false
307
+ end
308
+
309
+ # Returns whether or not the association should be validated as part of
310
+ # the parent's validation.
311
+ #
312
+ # Unless you explicitly disable validation with
313
+ # <tt>validate: false</tt>, validation will take place when:
314
+ #
315
+ # * you explicitly enable validation; <tt>validate: true</tt>
316
+ # * you use autosave; <tt>autosave: true</tt>
317
+ # * the association is a +has_many+ association
318
+ def validate?
319
+ !!options[:validate]
320
+ end
321
+
322
+ # Returns +true+ if +self+ is a +belongs_to+ reflection.
323
+ def embedded_in?; false; end
324
+
325
+ # Returns +true+ if +self+ is a +has_one+ reflection.
326
+ def embeds_one?; false; end
327
+
328
+ def association_class; raise NotImplementedError; end
329
+
330
+ VALID_AUTOMATIC_INVERSE_MACROS = [:embeds_many, :embeds_one, :embedded_in]
331
+
332
+ def extensions
333
+ Array(options[:extend])
334
+ end
335
+
336
+ private
337
+
338
+ def calculate_constructable(_macro, _options)
339
+ true
340
+ end
341
+
342
+ # Attempts to find the inverse association name automatically.
343
+ # If it cannot find a suitable inverse association name, it returns
344
+ # +nil+.
345
+ def inverse_name
346
+ unless defined?(@inverse_name)
347
+ @inverse_name = options.fetch(:inverse_of) { automatic_inverse_of }
348
+ end
349
+
350
+ @inverse_name
351
+ end
352
+
353
+ # returns either +nil+ or the inverse association name that it finds.
354
+ def automatic_inverse_of
355
+ return unless can_find_inverse_of_automatically?(self)
356
+
357
+ inverse_name_candidates =
358
+ begin
359
+ active_entity_name = active_entity.name.demodulize
360
+ [active_entity_name, ActiveSupport::Inflector.pluralize(active_entity_name)]
361
+ end
362
+
363
+ inverse_name_candidates.map! do |candidate|
364
+ ActiveSupport::Inflector.underscore(candidate).to_sym
365
+ end
366
+
367
+ inverse_name_candidates.detect do |inverse_name|
368
+ begin
369
+ reflection = klass._reflect_on_association(inverse_name)
370
+ rescue NameError
371
+ # Give up: we couldn't compute the klass type so we won't be able
372
+ # to find any associations either.
373
+ reflection = false
374
+ end
375
+
376
+ valid_inverse_reflection?(reflection)
377
+ end
378
+ end
379
+
380
+ # Checks if the inverse reflection that is returned from the
381
+ # +automatic_inverse_of+ method is a valid reflection. We must
382
+ # make sure that the reflection's active_entity name matches up
383
+ # with the current reflection's klass name.
384
+ def valid_inverse_reflection?(reflection)
385
+ reflection &&
386
+ klass <= reflection.active_entity &&
387
+ can_find_inverse_of_automatically?(reflection)
388
+ end
389
+
390
+ # Checks to see if the reflection doesn't have any options that prevent
391
+ # us from being able to guess the inverse automatically. First, the
392
+ # <tt>inverse_of</tt> option cannot be set to false. Second, we must
393
+ # have <tt>has_many</tt>, <tt>has_one</tt>, <tt>belongs_to</tt> associations.
394
+ # Third, we must not have options such as <tt>:foreign_key</tt>
395
+ # which prevent us from correctly guessing the inverse association.
396
+ #
397
+ # Anything with a scope can additionally ruin our attempt at finding an
398
+ # inverse, so we exclude reflections with scopes.
399
+ def can_find_inverse_of_automatically?(reflection)
400
+ reflection.options[:inverse_of] != false &&
401
+ VALID_AUTOMATIC_INVERSE_MACROS.include?(reflection.macro)
402
+ end
403
+
404
+ def derive_class_name
405
+ class_name = name.to_s
406
+ class_name = class_name.singularize if collection?
407
+ class_name.camelize
408
+ end
409
+ end
410
+
411
+ class EmbedsManyReflection < EmbeddedAssociationReflection # :nodoc:
412
+ def macro; :embeds_many; end
413
+
414
+ def collection?; true; end
415
+
416
+ def association_class
417
+ Associations::Embedded::EmbedsManyAssociation
418
+ end
419
+ end
420
+
421
+ class EmbedsOneReflection < EmbeddedAssociationReflection # :nodoc:
422
+ def macro; :embeds_one; end
423
+
424
+ def embeds_one?; true; end
425
+
426
+ def association_class
427
+ Associations::Embedded::EmbedsOneAssociation
428
+ end
429
+ end
430
+
431
+ class EmbeddedInReflection < EmbeddedAssociationReflection # :nodoc:
432
+ def macro; :embedded_in; end
433
+
434
+ def embedded_in?; true; end
435
+
436
+ def association_class
437
+ Associations::Embedded::EmbeddedInAssociation
438
+ end
439
+ end
440
+ end
441
+ end