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,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/enumerable"
4
+ require "active_support/core_ext/string/conversions"
5
+ require "active_support/core_ext/module/remove_method"
6
+ require "active_entity/errors"
7
+
8
+ module ActiveEntity
9
+ class AssociationNotFoundError < ConfigurationError #:nodoc:
10
+ def initialize(record = nil, association_name = nil)
11
+ if record && association_name
12
+ super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?")
13
+ else
14
+ super("Association was not found.")
15
+ end
16
+ end
17
+ end
18
+
19
+ class InverseOfAssociationNotFoundError < ActiveEntityError #:nodoc:
20
+ def initialize(reflection = nil, associated_class = nil)
21
+ if reflection
22
+ super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})")
23
+ else
24
+ super("Could not find the inverse association.")
25
+ end
26
+ end
27
+ end
28
+
29
+ # See ActiveEntity::Associations::ClassMethods for documentation.
30
+ module Associations # :nodoc:
31
+ extend ActiveSupport::Autoload
32
+ extend ActiveSupport::Concern
33
+
34
+ # These classes will be loaded when associations are created.
35
+ # So there is no need to eager load them.
36
+ module Embedded
37
+ extend ActiveSupport::Autoload
38
+
39
+ autoload :Association, "active_entity/associations/embedded/association"
40
+ autoload :SingularAssociation, "active_entity/associations/embedded/singular_association"
41
+ autoload :CollectionAssociation, "active_entity/associations/embedded/collection_association"
42
+ autoload :CollectionProxy, "active_entity/associations/embedded/collection_proxy"
43
+
44
+ module Builder #:nodoc:
45
+ autoload :Association, "active_entity/associations/embedded/builder/association"
46
+ autoload :SingularAssociation, "active_entity/associations/embedded/builder/singular_association"
47
+ autoload :CollectionAssociation, "active_entity/associations/embedded/builder/collection_association"
48
+
49
+ autoload :EmbeddedIn, "active_entity/associations/embedded/builder/embedded_in"
50
+ autoload :EmbedsOne, "active_entity/associations/embedded/builder/embeds_one"
51
+ autoload :EmbedsMany, "active_entity/associations/embedded/builder/embeds_many"
52
+ end
53
+
54
+ eager_autoload do
55
+ autoload :EmbeddedInAssociation
56
+ autoload :EmbedsOneAssociation
57
+ autoload :EmbedsManyAssociation
58
+ end
59
+ end
60
+
61
+ def self.eager_load!
62
+ super
63
+ Embedded.eager_load!
64
+ end
65
+
66
+ # Returns the association instance for the given name, instantiating it if it doesn't already exist
67
+ def association(name) #:nodoc:
68
+ association = association_instance_get(name)
69
+
70
+ if association.nil?
71
+ unless reflection = self.class._reflect_on_association(name)
72
+ raise AssociationNotFoundError.new(self, name)
73
+ end
74
+ association = reflection.association_class.new(self, reflection)
75
+ association_instance_set(name, association)
76
+ end
77
+
78
+ association
79
+ end
80
+
81
+ def association_cached?(name) # :nodoc:
82
+ @association_cache.key?(name)
83
+ end
84
+
85
+ def initialize_dup(*) # :nodoc:
86
+ @association_cache = {}
87
+ super
88
+ end
89
+
90
+ private
91
+ # Clears out the association cache.
92
+ def clear_association_cache
93
+ @association_cache.clear if persisted?
94
+ end
95
+
96
+ def init_internals
97
+ @association_cache = {}
98
+ super
99
+ end
100
+
101
+ # Returns the specified association instance if it exists, +nil+ otherwise.
102
+ def association_instance_get(name)
103
+ @association_cache[name]
104
+ end
105
+
106
+ # Set the specified association instance.
107
+ def association_instance_set(name, association)
108
+ @association_cache[name] = association
109
+ end
110
+
111
+ module ClassMethods
112
+ def embedded_in(name, **options)
113
+ reflection = Embedded::Builder::EmbeddedIn.build(self, name, options)
114
+ Reflection.add_reflection self, name, reflection
115
+ end
116
+
117
+ def embeds_one(name, **options)
118
+ reflection = Embedded::Builder::EmbedsOne.build(self, name, options)
119
+ Reflection.add_reflection self, name, reflection
120
+ end
121
+
122
+ def embeds_many(name, **options)
123
+ reflection = Embedded::Builder::EmbedsMany.build(self, name, options)
124
+ Reflection.add_reflection self, name, reflection
125
+ end
126
+
127
+ def association_names
128
+ @association_names ||=
129
+ if !abstract_class?
130
+ reflections.keys.map(&:to_sym)
131
+ else
132
+ []
133
+ end
134
+ end
135
+
136
+ def embedded_association_names
137
+ @association_names ||=
138
+ if !abstract_class?
139
+ reflections.select { |_, r| r.embedded? }.keys.map(&:to_sym)
140
+ else
141
+ []
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/array/wrap"
4
+
5
+ module ActiveEntity
6
+ module Associations
7
+ module Embedded
8
+ # = Active Entity Associations
9
+ #
10
+ # This is the root class of all associations ('+ Foo' signifies an included module Foo):
11
+ #
12
+ # Association
13
+ # SingularAssociation
14
+ # HasOneAssociation + ForeignAssociation
15
+ # HasOneThroughAssociation + ThroughAssociation
16
+ # BelongsToAssociation
17
+ # BelongsToPolymorphicAssociation
18
+ # CollectionAssociation
19
+ # HasManyAssociation + ForeignAssociation
20
+ # HasManyThroughAssociation + ThroughAssociation
21
+ class Association #:nodoc:
22
+ attr_reader :owner, :target, :reflection
23
+
24
+ delegate :options, to: :reflection
25
+
26
+ def initialize(owner, reflection)
27
+ reflection.check_validity!
28
+
29
+ @owner, @reflection = owner, reflection
30
+
31
+ @target = nil
32
+ @inversed = false
33
+ end
34
+
35
+ # Has the \target been already \loaded?
36
+ def loaded?
37
+ true
38
+ end
39
+
40
+ # Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+.
41
+ attr_writer :target
42
+
43
+ # Set the inverse association, if possible
44
+ def set_inverse_instance(record)
45
+ if inverse = inverse_association_for(record)
46
+ inverse.inversed_from(owner)
47
+ end
48
+ record
49
+ end
50
+
51
+ # Remove the inverse association, if possible
52
+ def remove_inverse_instance(record)
53
+ if inverse = inverse_association_for(record)
54
+ inverse.inversed_from(nil)
55
+ end
56
+ end
57
+
58
+ def inversed_from(record)
59
+ self.target = record
60
+ @inversed = !!record
61
+ end
62
+
63
+ # Returns the class of the target. belongs_to polymorphic overrides this to look at the
64
+ # polymorphic_type field on the owner.
65
+ def klass
66
+ reflection.klass
67
+ end
68
+
69
+ def extensions
70
+ reflection.extensions
71
+ end
72
+
73
+ # We can't dump @reflection and @through_reflection since it contains the scope proc
74
+ def marshal_dump
75
+ ivars = (instance_variables - [:@reflection, :@through_reflection]).map { |name| [name, instance_variable_get(name)] }
76
+ [@reflection.name, ivars]
77
+ end
78
+
79
+ def marshal_load(data)
80
+ reflection_name, ivars = data
81
+ ivars.each { |name, val| instance_variable_set(name, val) }
82
+ @reflection = @owner.class._reflect_on_association(reflection_name)
83
+ end
84
+
85
+ def initialize_attributes(record, attributes = {}) #:nodoc:
86
+ record.send(:_assign_attributes, attributes) if attributes.any?
87
+ set_inverse_instance(record)
88
+ end
89
+
90
+ private
91
+
92
+ # Raises ActiveEntity::AssociationTypeMismatch unless +record+ is of
93
+ # the kind of the class of the associated objects. Meant to be used as
94
+ # a sanity check when you are about to assign an associated record.
95
+ def raise_on_type_mismatch!(record)
96
+ unless record.is_a?(reflection.klass)
97
+ fresh_class = reflection.class_name.safe_constantize
98
+ unless fresh_class && record.is_a?(fresh_class)
99
+ message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, "\
100
+ "got #{record.inspect} which is an instance of #{record.class}(##{record.class.object_id})"
101
+ raise ActiveEntity::AssociationTypeMismatch, message
102
+ end
103
+ end
104
+ end
105
+
106
+ def inverse_association_for(record)
107
+ if invertible_for?(record)
108
+ record.association(inverse_reflection_for(record).name)
109
+ end
110
+ end
111
+
112
+ # Can be redefined by subclasses, notably polymorphic belongs_to
113
+ # The record parameter is necessary to support polymorphic inverses as we must check for
114
+ # the association in the specific class of the record.
115
+ def inverse_reflection_for(record)
116
+ reflection.inverse_of
117
+ end
118
+
119
+ # Returns true if inverse association on the given record needs to be set.
120
+ # This method is redefined by subclasses.
121
+ def invertible_for?(record)
122
+ inverse_reflection_for(record)
123
+ end
124
+
125
+ def build_record(attributes)
126
+ reflection.build_association(attributes) do |record|
127
+ initialize_attributes(record, attributes)
128
+ yield(record) if block_given?
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This is the parent Association class which defines the variables
4
+ # used by all associations.
5
+ #
6
+ # The hierarchy is defined as follows:
7
+ # Association
8
+ # - SingularAssociation
9
+ # - BelongsToAssociation
10
+ # - HasOneAssociation
11
+ # - CollectionAssociation
12
+ # - HasManyAssociation
13
+
14
+ module ActiveEntity::Associations::Embedded::Builder # :nodoc:
15
+ class Association #:nodoc:
16
+ class << self
17
+ attr_accessor :extensions
18
+ end
19
+ self.extensions = []
20
+
21
+ VALID_OPTIONS = [:class_name, :anonymous_class, :validate] # :nodoc:
22
+
23
+ def self.build(model, name, options)
24
+ if model.dangerous_attribute_method?(name)
25
+ raise ArgumentError, "You tried to define an association named #{name} on the model #{model.name}, but " \
26
+ "this will conflict with a method #{name} already defined by Active Entity. " \
27
+ "Please choose a different association name."
28
+ end
29
+
30
+ reflection = create_reflection model, name, options
31
+ define_accessors model, reflection
32
+ define_callbacks model, reflection
33
+ define_validations model, reflection
34
+ reflection
35
+ end
36
+
37
+ def self.create_reflection(model, name, options)
38
+ raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)
39
+
40
+ validate_options(options)
41
+
42
+ ActiveEntity::Reflection.create(macro, name, nil, options, model)
43
+ end
44
+
45
+ def self.macro
46
+ raise NotImplementedError
47
+ end
48
+
49
+ def self.valid_options(options)
50
+ VALID_OPTIONS + Association.extensions.flat_map(&:valid_options)
51
+ end
52
+
53
+ def self.validate_options(options)
54
+ options.assert_valid_keys(valid_options(options))
55
+ end
56
+
57
+ def self.define_callbacks(model, reflection)
58
+ Association.extensions.each do |extension|
59
+ extension.build model, reflection
60
+ end
61
+ end
62
+
63
+ # Defines the setter and getter methods for the association
64
+ # class Post < ActiveEntity::Base
65
+ # has_many :comments
66
+ # end
67
+ #
68
+ # Post.first.comments and Post.first.comments= methods are defined by this method...
69
+ def self.define_accessors(model, reflection)
70
+ mixin = model.generated_association_methods
71
+ name = reflection.name
72
+ define_readers(mixin, name)
73
+ define_writers(mixin, name)
74
+ end
75
+
76
+ def self.define_readers(mixin, name)
77
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
78
+ def #{name}
79
+ association(:#{name}).reader
80
+ end
81
+ CODE
82
+ end
83
+
84
+ def self.define_writers(mixin, name)
85
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
86
+ def #{name}=(value)
87
+ association(:#{name}).writer(value)
88
+ end
89
+ CODE
90
+ end
91
+
92
+ def self.define_validations(_model, _reflection)
93
+ # noop
94
+ end
95
+
96
+ def self.valid_dependent_options
97
+ raise NotImplementedError
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_entity/associations"
4
+
5
+ module ActiveEntity::Associations::Embedded::Builder # :nodoc:
6
+ class CollectionAssociation < Association #:nodoc:
7
+ CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]
8
+
9
+ def self.valid_options(options)
10
+ super + [:before_add, :after_add, :before_remove, :after_remove, :extend]
11
+ end
12
+
13
+ def self.define_callbacks(model, reflection)
14
+ super
15
+ name = reflection.name
16
+ options = reflection.options
17
+ CALLBACKS.each { |callback_name|
18
+ define_callback(model, callback_name, name, options)
19
+ }
20
+ end
21
+
22
+ def self.define_extensions(model, name)
23
+ if block_given?
24
+ extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
25
+ extension = Module.new(&Proc.new)
26
+ model.module_parent.const_set(extension_module_name, extension)
27
+ end
28
+ end
29
+
30
+ def self.define_callback(model, callback_name, name, options)
31
+ full_callback_name = "#{callback_name}_for_#{name}"
32
+
33
+ # TODO : why do i need method_defined? I think its because of the inheritance chain
34
+ model.class_attribute full_callback_name unless model.method_defined?(full_callback_name)
35
+ callbacks = Array(options[callback_name.to_sym]).map do |callback|
36
+ case callback
37
+ when Symbol
38
+ ->(_method, owner, record) { owner.send(callback, record) }
39
+ when Proc
40
+ ->(_method, owner, record) { callback.call(owner, record) }
41
+ else
42
+ ->(method, owner, record) { callback.send(method, owner, record) }
43
+ end
44
+ end
45
+ model.send "#{full_callback_name}=", callbacks
46
+ end
47
+
48
+ # Defines the setter and getter methods for the collection_singular_ids.
49
+ def self.define_readers(mixin, name)
50
+ super
51
+
52
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
53
+ def #{name.to_s.singularize}_ids
54
+ association(:#{name}).ids_reader
55
+ end
56
+ CODE
57
+ end
58
+
59
+ def self.define_writers(mixin, name)
60
+ super
61
+
62
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
63
+ def #{name.to_s.singularize}_ids=(ids)
64
+ association(:#{name}).ids_writer(ids)
65
+ end
66
+ CODE
67
+ end
68
+ end
69
+ end