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,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