activeentity 0.0.1.beta1

Sign up to get free protection for your applications and to get access to all the features.
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