tallty_duck_record 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +41 -0
  3. data/README.md +82 -0
  4. data/Rakefile +28 -0
  5. data/lib/core_ext/array_without_blank.rb +46 -0
  6. data/lib/duck_record.rb +65 -0
  7. data/lib/duck_record/associations.rb +130 -0
  8. data/lib/duck_record/associations/association.rb +271 -0
  9. data/lib/duck_record/associations/belongs_to_association.rb +71 -0
  10. data/lib/duck_record/associations/builder/association.rb +127 -0
  11. data/lib/duck_record/associations/builder/belongs_to.rb +44 -0
  12. data/lib/duck_record/associations/builder/collection_association.rb +45 -0
  13. data/lib/duck_record/associations/builder/embeds_many.rb +9 -0
  14. data/lib/duck_record/associations/builder/embeds_one.rb +9 -0
  15. data/lib/duck_record/associations/builder/has_many.rb +11 -0
  16. data/lib/duck_record/associations/builder/has_one.rb +20 -0
  17. data/lib/duck_record/associations/builder/singular_association.rb +33 -0
  18. data/lib/duck_record/associations/collection_association.rb +476 -0
  19. data/lib/duck_record/associations/collection_proxy.rb +1160 -0
  20. data/lib/duck_record/associations/embeds_association.rb +92 -0
  21. data/lib/duck_record/associations/embeds_many_association.rb +203 -0
  22. data/lib/duck_record/associations/embeds_many_proxy.rb +892 -0
  23. data/lib/duck_record/associations/embeds_one_association.rb +48 -0
  24. data/lib/duck_record/associations/foreign_association.rb +11 -0
  25. data/lib/duck_record/associations/has_many_association.rb +17 -0
  26. data/lib/duck_record/associations/has_one_association.rb +39 -0
  27. data/lib/duck_record/associations/singular_association.rb +73 -0
  28. data/lib/duck_record/attribute.rb +213 -0
  29. data/lib/duck_record/attribute/user_provided_default.rb +30 -0
  30. data/lib/duck_record/attribute_assignment.rb +118 -0
  31. data/lib/duck_record/attribute_decorators.rb +89 -0
  32. data/lib/duck_record/attribute_methods.rb +325 -0
  33. data/lib/duck_record/attribute_methods/before_type_cast.rb +76 -0
  34. data/lib/duck_record/attribute_methods/dirty.rb +107 -0
  35. data/lib/duck_record/attribute_methods/read.rb +78 -0
  36. data/lib/duck_record/attribute_methods/serialization.rb +66 -0
  37. data/lib/duck_record/attribute_methods/write.rb +70 -0
  38. data/lib/duck_record/attribute_mutation_tracker.rb +108 -0
  39. data/lib/duck_record/attribute_set.rb +98 -0
  40. data/lib/duck_record/attribute_set/yaml_encoder.rb +41 -0
  41. data/lib/duck_record/attributes.rb +262 -0
  42. data/lib/duck_record/base.rb +300 -0
  43. data/lib/duck_record/callbacks.rb +324 -0
  44. data/lib/duck_record/coders/json.rb +13 -0
  45. data/lib/duck_record/coders/yaml_column.rb +48 -0
  46. data/lib/duck_record/core.rb +262 -0
  47. data/lib/duck_record/define_callbacks.rb +23 -0
  48. data/lib/duck_record/enum.rb +139 -0
  49. data/lib/duck_record/errors.rb +71 -0
  50. data/lib/duck_record/inheritance.rb +130 -0
  51. data/lib/duck_record/locale/en.yml +46 -0
  52. data/lib/duck_record/model_schema.rb +71 -0
  53. data/lib/duck_record/nested_attributes.rb +555 -0
  54. data/lib/duck_record/nested_validate_association.rb +262 -0
  55. data/lib/duck_record/persistence.rb +39 -0
  56. data/lib/duck_record/readonly_attributes.rb +36 -0
  57. data/lib/duck_record/reflection.rb +650 -0
  58. data/lib/duck_record/serialization.rb +26 -0
  59. data/lib/duck_record/translation.rb +22 -0
  60. data/lib/duck_record/type.rb +77 -0
  61. data/lib/duck_record/type/array.rb +36 -0
  62. data/lib/duck_record/type/array_without_blank.rb +36 -0
  63. data/lib/duck_record/type/date.rb +7 -0
  64. data/lib/duck_record/type/date_time.rb +7 -0
  65. data/lib/duck_record/type/decimal_without_scale.rb +13 -0
  66. data/lib/duck_record/type/internal/abstract_json.rb +33 -0
  67. data/lib/duck_record/type/internal/timezone.rb +15 -0
  68. data/lib/duck_record/type/json.rb +6 -0
  69. data/lib/duck_record/type/registry.rb +97 -0
  70. data/lib/duck_record/type/serialized.rb +63 -0
  71. data/lib/duck_record/type/text.rb +9 -0
  72. data/lib/duck_record/type/time.rb +19 -0
  73. data/lib/duck_record/type/unsigned_integer.rb +15 -0
  74. data/lib/duck_record/validations.rb +67 -0
  75. data/lib/duck_record/validations/subset.rb +74 -0
  76. data/lib/duck_record/validations/uniqueness_on_real_record.rb +248 -0
  77. data/lib/duck_record/version.rb +3 -0
  78. data/lib/tasks/acts_as_record_tasks.rake +4 -0
  79. metadata +181 -0
@@ -0,0 +1,262 @@
1
+ module DuckRecord
2
+ # = Active Record Autosave Association
3
+ #
4
+ # AutosaveAssociation is a module that takes care of automatically saving
5
+ # associated records when their parent is saved. In addition to saving, it
6
+ # also destroys any associated records that were marked for destruction.
7
+ # (See #mark_for_destruction and #marked_for_destruction?).
8
+ #
9
+ # Saving of the parent, its associations, and the destruction of marked
10
+ # associations, all happen inside a transaction. This should never leave the
11
+ # database in an inconsistent state.
12
+ #
13
+ # If validations for any of the associations fail, their error messages will
14
+ # be applied to the parent.
15
+ #
16
+ # Note that it also means that associations marked for destruction won't
17
+ # be destroyed directly. They will however still be marked for destruction.
18
+ #
19
+ # Note that <tt>autosave: false</tt> is not same as not declaring <tt>:autosave</tt>.
20
+ # When the <tt>:autosave</tt> option is not present then new association records are
21
+ # saved but the updated association records are not saved.
22
+ #
23
+ # == Validation
24
+ #
25
+ # Child records are validated unless <tt>:validate</tt> is +false+.
26
+ #
27
+ # == Callbacks
28
+ #
29
+ # Association with autosave option defines several callbacks on your
30
+ # model (before_save, after_create, after_update). Please note that
31
+ # callbacks are executed in the order they were defined in
32
+ # model. You should avoid modifying the association content, before
33
+ # autosave callbacks are executed. Placing your callbacks after
34
+ # associations is usually a good practice.
35
+ #
36
+ # === One-to-one Example
37
+ #
38
+ # class Post < ActiveRecord::Base
39
+ # has_one :author, autosave: true
40
+ # end
41
+ #
42
+ # Saving changes to the parent and its associated model can now be performed
43
+ # automatically _and_ atomically:
44
+ #
45
+ # post = Post.find(1)
46
+ # post.title # => "The current global position of migrating ducks"
47
+ # post.author.name # => "alloy"
48
+ #
49
+ # post.title = "On the migration of ducks"
50
+ # post.author.name = "Eloy Duran"
51
+ #
52
+ # post.save
53
+ # post.reload
54
+ # post.title # => "On the migration of ducks"
55
+ # post.author.name # => "Eloy Duran"
56
+ #
57
+ # Destroying an associated model, as part of the parent's save action, is as
58
+ # simple as marking it for destruction:
59
+ #
60
+ # post.author.mark_for_destruction
61
+ # post.author.marked_for_destruction? # => true
62
+ #
63
+ # Note that the model is _not_ yet removed from the database:
64
+ #
65
+ # id = post.author.id
66
+ # Author.find_by(id: id).nil? # => false
67
+ #
68
+ # post.save
69
+ # post.reload.author # => nil
70
+ #
71
+ # Now it _is_ removed from the database:
72
+ #
73
+ # Author.find_by(id: id).nil? # => true
74
+ #
75
+ # === One-to-many Example
76
+ #
77
+ # When <tt>:autosave</tt> is not declared new children are saved when their parent is saved:
78
+ #
79
+ # class Post < ActiveRecord::Base
80
+ # has_many :comments # :autosave option is not declared
81
+ # end
82
+ #
83
+ # post = Post.new(title: 'ruby rocks')
84
+ # post.comments.build(body: 'hello world')
85
+ # post.save # => saves both post and comment
86
+ #
87
+ # post = Post.create(title: 'ruby rocks')
88
+ # post.comments.build(body: 'hello world')
89
+ # post.save # => saves both post and comment
90
+ #
91
+ # post = Post.create(title: 'ruby rocks')
92
+ # post.comments.create(body: 'hello world')
93
+ # post.save # => saves both post and comment
94
+ #
95
+ # When <tt>:autosave</tt> is true all children are saved, no matter whether they
96
+ # are new records or not:
97
+ #
98
+ # class Post < ActiveRecord::Base
99
+ # has_many :comments, autosave: true
100
+ # end
101
+ #
102
+ # post = Post.create(title: 'ruby rocks')
103
+ # post.comments.create(body: 'hello world')
104
+ # post.comments[0].body = 'hi everyone'
105
+ # post.comments.build(body: "good morning.")
106
+ # post.title += "!"
107
+ # post.save # => saves both post and comments.
108
+ #
109
+ # Destroying one of the associated models as part of the parent's save action
110
+ # is as simple as marking it for destruction:
111
+ #
112
+ # post.comments # => [#<Comment id: 1, ...>, #<Comment id: 2, ...]>
113
+ # post.comments[1].mark_for_destruction
114
+ # post.comments[1].marked_for_destruction? # => true
115
+ # post.comments.length # => 2
116
+ #
117
+ # Note that the model is _not_ yet removed from the database:
118
+ #
119
+ # id = post.comments.last.id
120
+ # Comment.find_by(id: id).nil? # => false
121
+ #
122
+ # post.save
123
+ # post.reload.comments.length # => 1
124
+ #
125
+ # Now it _is_ removed from the database:
126
+ #
127
+ # Comment.find_by(id: id).nil? # => true
128
+ module NestedValidateAssociation
129
+ extend ActiveSupport::Concern
130
+
131
+ module AssociationBuilderExtension #:nodoc:
132
+ def self.build(model, reflection)
133
+ model.send(:add_nested_validate_association_callbacks, reflection)
134
+ end
135
+
136
+ def self.valid_options
137
+ []
138
+ end
139
+ end
140
+
141
+ included do
142
+ Associations::Builder::Association.extensions << AssociationBuilderExtension
143
+ mattr_accessor :index_nested_attribute_errors, instance_writer: false
144
+ self.index_nested_attribute_errors = false
145
+ end
146
+
147
+ module ClassMethods # :nodoc:
148
+ private
149
+
150
+ def define_non_cyclic_method(name, &block)
151
+ return if method_defined?(name)
152
+ define_method(name) do |*args|
153
+ result = true; @_already_called ||= {}
154
+ # Loop prevention for validation of associations
155
+ unless @_already_called[name]
156
+ begin
157
+ @_already_called[name] = true
158
+ result = instance_eval(&block)
159
+ ensure
160
+ @_already_called[name] = false
161
+ end
162
+ end
163
+
164
+ result
165
+ end
166
+ end
167
+
168
+ # Adds validation and save callbacks for the association as specified by
169
+ # the +reflection+.
170
+ #
171
+ # For performance reasons, we don't check whether to validate at runtime.
172
+ # However the validation and callback methods are lazy and those methods
173
+ # get created when they are invoked for the very first time. However,
174
+ # this can change, for instance, when using nested attributes, which is
175
+ # called _after_ the association has been defined. Since we don't want
176
+ # the callbacks to get defined multiple times, there are guards that
177
+ # check if the save or validation methods have already been defined
178
+ # before actually defining them.
179
+ def add_nested_validate_association_callbacks(reflection)
180
+ validation_method = :"validate_associated_records_for_#{reflection.name}"
181
+ if reflection.validate? && !method_defined?(validation_method)
182
+ if reflection.collection?
183
+ method = :validate_collection_association
184
+ else
185
+ method = :validate_single_association
186
+ end
187
+
188
+ define_non_cyclic_method(validation_method) do
189
+ send(method, reflection)
190
+ # TODO: remove the following line as soon as the return value of
191
+ # callbacks is ignored, that is, returning `false` does not
192
+ # display a deprecation warning or halts the callback chain.
193
+ true
194
+ end
195
+ validate validation_method
196
+ after_validation :_ensure_no_duplicate_errors
197
+ end
198
+ end
199
+ end
200
+
201
+ private
202
+
203
+ # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
204
+ # turned on for the association.
205
+ def validate_single_association(reflection)
206
+ association = association_instance_get(reflection.name)
207
+ record = association&.reader
208
+ association_valid?(reflection, record) if record
209
+ end
210
+
211
+ # Validate the associated records if <tt>:validate</tt> or
212
+ # <tt>:autosave</tt> is turned on for the association specified by
213
+ # +reflection+.
214
+ def validate_collection_association(reflection)
215
+ if association = association_instance_get(reflection.name)
216
+ if records = association.target
217
+ records.each_with_index { |record, index| association_valid?(reflection, record, index) }
218
+ end
219
+ end
220
+ end
221
+
222
+ # Returns whether or not the association is valid and applies any errors to
223
+ # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
224
+ # enabled records if they're marked_for_destruction? or destroyed.
225
+ def association_valid?(reflection, record, index = nil)
226
+ unless valid = record.valid?
227
+ indexed_attribute = !index.nil? && (reflection.options[:index_errors] || DuckRecord::Base.index_nested_attribute_errors)
228
+
229
+ record.errors.each do |attribute, message|
230
+ attribute = normalize_reflection_attribute(indexed_attribute, reflection, index, attribute)
231
+ errors[attribute] << message
232
+ errors[attribute].uniq!
233
+ end
234
+
235
+ record.errors.details.each_key do |attribute|
236
+ reflection_attribute =
237
+ normalize_reflection_attribute(indexed_attribute, reflection, index, attribute).to_sym
238
+
239
+ record.errors.details[attribute].each do |error|
240
+ errors.details[reflection_attribute] << error
241
+ errors.details[reflection_attribute].uniq!
242
+ end
243
+ end
244
+ end
245
+ valid
246
+ end
247
+
248
+ def normalize_reflection_attribute(indexed_attribute, reflection, index, attribute)
249
+ if indexed_attribute
250
+ "#{reflection.name}[#{index}].#{attribute}"
251
+ else
252
+ "#{reflection.name}.#{attribute}"
253
+ end
254
+ end
255
+
256
+ def _ensure_no_duplicate_errors
257
+ errors.messages.each_key do |attribute|
258
+ errors[attribute].uniq!
259
+ end
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,39 @@
1
+ module DuckRecord
2
+ # = DuckRecord \Persistence
3
+ module Persistence
4
+ extend ActiveSupport::Concern
5
+
6
+ def persisted?
7
+ false
8
+ end
9
+
10
+ def destroyed?
11
+ false
12
+ end
13
+
14
+ def new_record?
15
+ true
16
+ end
17
+
18
+ # Returns an instance of the specified +klass+ with the attributes of the
19
+ # current record. This is mostly useful in relation to single-table
20
+ # inheritance structures where you want a subclass to appear as the
21
+ # superclass. This can be used along with record identification in
22
+ # Action Pack to allow, say, <tt>Client < Company</tt> to do something
23
+ # like render <tt>partial: @client.becomes(Company)</tt> to render that
24
+ # instance using the companies/company partial instead of clients/client.
25
+ #
26
+ # Note: The new instance will share a link to the same attributes as the original class.
27
+ # Therefore the sti column value will still be the same.
28
+ # Any change to the attributes on either instance will affect both instances.
29
+ # If you want to change the sti column as well, use #becomes! instead.
30
+ def becomes(klass)
31
+ became = klass.new
32
+ became.instance_variable_set("@attributes", @attributes)
33
+ became.instance_variable_set("@mutation_tracker", @mutation_tracker) if defined?(@mutation_tracker)
34
+ became.instance_variable_set("@changed_attributes", attributes_changed_by_setter)
35
+ became.errors.copy!(errors)
36
+ became
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,36 @@
1
+ module DuckRecord
2
+ module ReadonlyAttributes
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ attr_accessor :_attr_readonly_enabled
7
+ class_attribute :_attr_readonly, instance_accessor: false
8
+ self._attr_readonly = []
9
+ end
10
+
11
+ def attr_readonly_enabled?
12
+ _attr_readonly_enabled
13
+ end
14
+
15
+ def enable_attr_readonly!
16
+ self._attr_readonly_enabled = true
17
+ end
18
+
19
+ def disable_attr_readonly!
20
+ self._attr_readonly_enabled = false
21
+ end
22
+
23
+ module ClassMethods
24
+ # Attributes listed as readonly will be used to create a new record but update operations will
25
+ # ignore these fields.
26
+ def attr_readonly(*attributes)
27
+ self._attr_readonly = Set.new(attributes.map(&:to_s)) + (_attr_readonly || [])
28
+ end
29
+
30
+ # Returns an array of all the attributes that have been specified as readonly.
31
+ def readonly_attributes
32
+ _attr_readonly
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,650 @@
1
+ require "thread"
2
+ require "active_support/core_ext/string/filters"
3
+ require "active_support/deprecation"
4
+
5
+ module DuckRecord
6
+ # = Active Record Reflection
7
+ module Reflection # :nodoc:
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ class_attribute :_reflections, instance_writer: false
12
+ self._reflections = {}
13
+ end
14
+
15
+ def self.create(macro, name, scope, options, ar)
16
+ klass = \
17
+ case macro
18
+ when :embeds_many
19
+ EmbedsManyReflection
20
+ when :embeds_one
21
+ EmbedsOneReflection
22
+ when :belongs_to
23
+ BelongsToReflection
24
+ when :has_many
25
+ HasManyReflection
26
+ when :has_one
27
+ HasOneReflection
28
+ else
29
+ raise "Unsupported Macro: #{macro}"
30
+ end
31
+
32
+ klass.new(name, scope, options, ar)
33
+ end
34
+
35
+ def self.add_reflection(ar, name, reflection)
36
+ ar.clear_reflections_cache
37
+ ar._reflections = ar._reflections.merge(name.to_s => reflection)
38
+ end
39
+
40
+ def has_reflection?(reflection_name)
41
+ _reflections.keys.include?(reflection_name.to_s)
42
+ end
43
+
44
+ # \Reflection enables the ability to examine the associations and aggregations of
45
+ # Active Record classes and objects. This information, for example,
46
+ # can be used in a form builder that takes an Active Record object
47
+ # and creates input fields for all of the attributes depending on their type
48
+ # and displays the associations to other objects.
49
+ #
50
+ # MacroReflection class has info for AggregateReflection and AssociationReflection
51
+ # classes.
52
+ module ClassMethods
53
+ # Returns a Hash of name of the reflection as the key and an AssociationReflection as the value.
54
+ #
55
+ # Account.reflections # => {"balance" => AggregateReflection}
56
+ #
57
+ def reflections
58
+ @__reflections ||= begin
59
+ ref = {}
60
+
61
+ _reflections.each do |name, reflection|
62
+ parent_reflection = reflection.parent_reflection
63
+
64
+ if parent_reflection
65
+ parent_name = parent_reflection.name
66
+ ref[parent_name.to_s] = parent_reflection
67
+ else
68
+ ref[name] = reflection
69
+ end
70
+ end
71
+
72
+ ref
73
+ end
74
+ end
75
+
76
+ # Returns an array of AssociationReflection objects for all the
77
+ # associations in the class. If you only want to reflect on a certain
78
+ # association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>,
79
+ # <tt>:belongs_to</tt>) as the first parameter.
80
+ #
81
+ # Example:
82
+ #
83
+ # Account.reflect_on_all_associations # returns an array of all associations
84
+ # Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
85
+ #
86
+ def reflect_on_all_associations(macro = nil)
87
+ association_reflections = reflections.values
88
+ association_reflections.select! { |reflection| reflection.macro == macro } if macro
89
+ association_reflections
90
+ end
91
+
92
+ # Returns the AssociationReflection object for the +association+ (use the symbol).
93
+ #
94
+ # Account.reflect_on_association(:owner) # returns the owner AssociationReflection
95
+ # Invoice.reflect_on_association(:line_items).macro # returns :has_many
96
+ #
97
+ def reflect_on_association(association)
98
+ reflections[association.to_s]
99
+ end
100
+
101
+ def _reflect_on_association(association) #:nodoc:
102
+ _reflections[association.to_s]
103
+ end
104
+
105
+ def clear_reflections_cache # :nodoc:
106
+ @__reflections = nil
107
+ end
108
+ end
109
+
110
+ # Holds all the methods that are shared between MacroReflection and ThroughReflection.
111
+ #
112
+ # AbstractReflection
113
+ # MacroReflection
114
+ # AggregateReflection
115
+ # AssociationReflection
116
+ # HasManyReflection
117
+ # HasOneReflection
118
+ # BelongsToReflection
119
+ # HasAndBelongsToManyReflection
120
+ # ThroughReflection
121
+ # PolymorphicReflection
122
+ # RuntimeReflection
123
+ class AbstractReflection
124
+ # Returns a new, unsaved instance of the associated class. +attributes+ will
125
+ # be passed to the class's constructor.
126
+ def build_association(attributes, &block)
127
+ klass.new(attributes, &block)
128
+ end
129
+
130
+ # Returns the class name for the macro.
131
+ #
132
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt>
133
+ # <tt>has_many :clients</tt> returns <tt>'Client'</tt>
134
+ def class_name
135
+ @class_name ||= (options[:class_name] || derive_class_name).to_s
136
+ end
137
+
138
+ def check_validity!
139
+ true
140
+ end
141
+
142
+ def alias_candidate(name)
143
+ "#{plural_name}_#{name}"
144
+ end
145
+ end
146
+
147
+ # Base class for AggregateReflection and AssociationReflection. Objects of
148
+ # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
149
+ class MacroReflection < AbstractReflection
150
+ # Returns the name of the macro.
151
+ #
152
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:balance</tt>
153
+ # <tt>has_many :clients</tt> returns <tt>:clients</tt>
154
+ attr_reader :name
155
+
156
+ attr_reader :scope
157
+
158
+ # Returns the hash of options used for the macro.
159
+ #
160
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>{ class_name: "Money" }</tt>
161
+ # <tt>has_many :clients</tt> returns <tt>{}</tt>
162
+ attr_reader :options
163
+
164
+ attr_reader :duck_record
165
+
166
+ attr_reader :plural_name # :nodoc:
167
+
168
+ def initialize(name, scope, options, duck_record)
169
+ @name = name
170
+ @scope = scope
171
+ @options = options
172
+ @duck_record = duck_record
173
+ @klass = options[:anonymous_class]
174
+ @plural_name = name.to_s.pluralize
175
+ end
176
+
177
+ # Returns the class for the macro.
178
+ #
179
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class
180
+ # <tt>has_many :clients</tt> returns the Client class
181
+ def klass
182
+ @klass ||= compute_class(class_name)
183
+ end
184
+
185
+ def compute_class(name)
186
+ name.constantize
187
+ end
188
+
189
+ # Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
190
+ # and +other_aggregation+ has an options hash assigned to it.
191
+ def ==(other_aggregation)
192
+ super ||
193
+ other_aggregation.kind_of?(self.class) &&
194
+ name == other_aggregation.name &&
195
+ !other_aggregation.options.nil? &&
196
+ active_record == other_aggregation.active_record
197
+ end
198
+
199
+ private
200
+
201
+ def derive_class_name
202
+ name.to_s.camelize
203
+ end
204
+ end
205
+
206
+ # Holds all the metadata about an association as it was specified in the
207
+ # Active Record class.
208
+ class EmbedsAssociationReflection < MacroReflection
209
+ # Returns the target association's class.
210
+ #
211
+ # class Author < ActiveRecord::Base
212
+ # has_many :books
213
+ # end
214
+ #
215
+ # Author.reflect_on_association(:books).klass
216
+ # # => Book
217
+ #
218
+ # <b>Note:</b> Do not call +klass.new+ or +klass.create+ to instantiate
219
+ # a new association object. Use +build_association+ or +create_association+
220
+ # instead. This allows plugins to hook into association object creation.
221
+ def klass
222
+ @klass ||= compute_class(class_name)
223
+ end
224
+
225
+ def compute_class(name)
226
+ duck_record.send(:compute_type, name)
227
+ end
228
+
229
+ attr_accessor :parent_reflection # Reflection
230
+
231
+ def initialize(name, scope, options, duck_record)
232
+ super
233
+ @constructable = calculate_constructable(macro, options)
234
+
235
+ if options[:class_name] && options[:class_name].class == Class
236
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
237
+ Passing a class to the `class_name` is deprecated and will raise
238
+ an ArgumentError in Rails 5.2. It eagerloads more classes than
239
+ necessary and potentially creates circular dependencies.
240
+
241
+ Please pass the class name as a string:
242
+ `#{macro} :#{name}, class_name: '#{options[:class_name]}'`
243
+ MSG
244
+ end
245
+ end
246
+
247
+ def constructable? # :nodoc:
248
+ @constructable
249
+ end
250
+
251
+ def source_reflection
252
+ self
253
+ end
254
+
255
+ def nested?
256
+ false
257
+ end
258
+
259
+ # Returns the macro type.
260
+ #
261
+ # <tt>has_many :clients</tt> returns <tt>:has_many</tt>
262
+ def macro; raise NotImplementedError; end
263
+
264
+ # Returns whether or not this association reflection is for a collection
265
+ # association. Returns +true+ if the +macro+ is either +has_many+ or
266
+ # +has_and_belongs_to_many+, +false+ otherwise.
267
+ def collection?
268
+ false
269
+ end
270
+
271
+ # Returns whether or not the association should be validated as part of
272
+ # the parent's validation.
273
+ #
274
+ # Unless you explicitly disable validation with
275
+ # <tt>validate: false</tt>, validation will take place when:
276
+ #
277
+ # * you explicitly enable validation; <tt>validate: true</tt>
278
+ # * you use autosave; <tt>autosave: true</tt>
279
+ # * the association is a +has_many+ association
280
+ def validate?
281
+ !options[:validate].nil? ? options[:validate] : collection?
282
+ end
283
+
284
+ # Returns +true+ if +self+ is a +has_one+ reflection.
285
+ def has_one?; false; end
286
+
287
+ def association_class; raise NotImplementedError; end
288
+
289
+ def add_as_source(seed)
290
+ seed
291
+ end
292
+
293
+ protected
294
+
295
+ def actual_source_reflection # FIXME: this is a horrible name
296
+ self
297
+ end
298
+
299
+ private
300
+
301
+ def calculate_constructable(macro, options)
302
+ true
303
+ end
304
+
305
+ def derive_class_name
306
+ class_name = name.to_s
307
+ class_name = class_name.singularize if collection?
308
+ class_name.camelize
309
+ end
310
+ end
311
+
312
+ class EmbedsManyReflection < EmbedsAssociationReflection
313
+ def macro; :embeds_many; end
314
+
315
+ def collection?; true; end
316
+
317
+ def association_class
318
+ Associations::EmbedsManyAssociation
319
+ end
320
+ end
321
+
322
+ class EmbedsOneReflection < EmbedsAssociationReflection
323
+ def macro; :embeds_one; end
324
+
325
+ def has_one?; true; end
326
+
327
+ def association_class
328
+ Associations::EmbedsOneAssociation
329
+ end
330
+ end
331
+
332
+ # Holds all the metadata about an association as it was specified in the
333
+ # Active Record class.
334
+ class AssociationReflection < MacroReflection
335
+ alias active_record duck_record
336
+
337
+ def quoted_table_name
338
+ klass.quoted_table_name
339
+ end
340
+
341
+ def primary_key_type
342
+ klass.type_for_attribute(klass.primary_key)
343
+ end
344
+
345
+ JoinKeys = Struct.new(:key, :foreign_key) # :nodoc:
346
+
347
+ def join_keys
348
+ get_join_keys klass
349
+ end
350
+
351
+ # Returns a list of scopes that should be applied for this Reflection
352
+ # object when querying the database.
353
+ def scopes
354
+ scope ? [scope] : []
355
+ end
356
+
357
+ def scope_chain
358
+ chain.map(&:scopes)
359
+ end
360
+ deprecate :scope_chain
361
+
362
+ def join_scopes(table, predicate_builder) # :nodoc:
363
+ if scope
364
+ [ActiveRecord::Relation.create(klass, table, predicate_builder)
365
+ .instance_exec(&scope)]
366
+ else
367
+ []
368
+ end
369
+ end
370
+
371
+ def klass_join_scope(table, predicate_builder) # :nodoc:
372
+ relation = ActiveRecord::Relation.create(klass, table, predicate_builder)
373
+ klass.scope_for_association(relation)
374
+ end
375
+
376
+ def constraints
377
+ chain.map(&:scopes).flatten
378
+ end
379
+
380
+ def alias_candidate(name)
381
+ "#{plural_name}_#{name}"
382
+ end
383
+
384
+ def chain
385
+ collect_join_chain
386
+ end
387
+
388
+ def get_join_keys(association_klass)
389
+ JoinKeys.new(join_pk(association_klass), join_fk)
390
+ end
391
+
392
+ # Returns the target association's class.
393
+ #
394
+ # class Author < ActiveRecord::Base
395
+ # has_many :books
396
+ # end
397
+ #
398
+ # Author.reflect_on_association(:books).klass
399
+ # # => Book
400
+ #
401
+ # <b>Note:</b> Do not call +klass.new+ or +klass.create+ to instantiate
402
+ # a new association object. Use +build_association+ or +create_association+
403
+ # instead. This allows plugins to hook into association object creation.
404
+ def klass
405
+ @klass ||= compute_class(class_name)
406
+ end
407
+
408
+ def compute_class(name)
409
+ active_record.send(:compute_type, name)
410
+ end
411
+
412
+ def table_name
413
+ klass.table_name
414
+ end
415
+
416
+ attr_reader :type, :foreign_type
417
+ attr_accessor :parent_reflection # Reflection
418
+
419
+ def initialize(name, scope, options, active_record)
420
+ super
421
+ @type = options[:as] && (options[:foreign_type] || "#{options[:as]}_type")
422
+ @foreign_type = options[:foreign_type] || "#{name}_type"
423
+ @constructable = calculate_constructable(macro, options)
424
+ @association_scope_cache = {}
425
+ @scope_lock = Mutex.new
426
+
427
+ if options[:class_name] && options[:class_name].class == Class
428
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
429
+ Passing a class to the `class_name` is deprecated and will raise
430
+ an ArgumentError in Rails 5.2. It eagerloads more classes than
431
+ necessary and potentially creates circular dependencies.
432
+
433
+ Please pass the class name as a string:
434
+ `#{macro} :#{name}, class_name: '#{options[:class_name]}'`
435
+ MSG
436
+ end
437
+ end
438
+
439
+ def association_scope_cache(conn, owner)
440
+ key = conn.prepared_statements
441
+ @association_scope_cache[key] ||= @scope_lock.synchronize {
442
+ @association_scope_cache[key] ||= yield
443
+ }
444
+ end
445
+
446
+ def constructable? # :nodoc:
447
+ @constructable
448
+ end
449
+
450
+ def join_table
451
+ @join_table ||= options[:join_table] || derive_join_table
452
+ end
453
+
454
+ def foreign_key
455
+ @foreign_key ||= options[:foreign_key] || derive_foreign_key.freeze
456
+ end
457
+
458
+ def association_foreign_key
459
+ @association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key
460
+ end
461
+
462
+ # klass option is necessary to support loading polymorphic associations
463
+ def association_primary_key(klass = nil)
464
+ options[:primary_key] || primary_key(klass || self.klass)
465
+ end
466
+
467
+ def association_primary_key_type
468
+ klass.type_for_attribute(association_primary_key.to_s)
469
+ end
470
+
471
+ def active_record_primary_key
472
+ @active_record_primary_key ||= options[:primary_key] || primary_key(active_record)
473
+ end
474
+
475
+ def check_validity!
476
+ unless klass < ActiveRecord::Base
477
+ raise ArgumentError, "#{klass} must be inherited from ActiveRecord::Base."
478
+ end
479
+ end
480
+
481
+ def check_preloadable!
482
+ return unless scope
483
+
484
+ if scope.arity > 0
485
+ raise ArgumentError, <<-MSG.squish
486
+ The association scope '#{name}' is instance dependent (the scope
487
+ block takes an argument). Preloading instance dependent scopes is
488
+ not supported.
489
+ MSG
490
+ end
491
+ end
492
+ alias :check_eager_loadable! :check_preloadable!
493
+
494
+ def join_id_for(owner) # :nodoc:
495
+ owner[active_record_primary_key]
496
+ end
497
+
498
+ def source_reflection
499
+ self
500
+ end
501
+
502
+ # A chain of reflections from this one back to the owner. For more see the explanation in
503
+ # ThroughReflection.
504
+ def collect_join_chain
505
+ [self]
506
+ end
507
+
508
+ # This is for clearing cache on the reflection. Useful for tests that need to compare
509
+ # SQL queries on associations.
510
+ def clear_association_scope_cache # :nodoc:
511
+ @association_scope_cache.clear
512
+ end
513
+
514
+ def nested?
515
+ false
516
+ end
517
+
518
+ def has_scope?
519
+ scope
520
+ end
521
+
522
+ # Returns the macro type.
523
+ #
524
+ # <tt>has_many :clients</tt> returns <tt>:has_many</tt>
525
+ def macro; raise NotImplementedError; end
526
+
527
+ # Returns whether or not this association reflection is for a collection
528
+ # association. Returns +true+ if the +macro+ is either +has_many+ or
529
+ # +has_and_belongs_to_many+, +false+ otherwise.
530
+ def collection?
531
+ false
532
+ end
533
+
534
+ # Returns whether or not the association should be validated as part of
535
+ # the parent's validation.
536
+ #
537
+ # Unless you explicitly disable validation with
538
+ # <tt>validate: false</tt>, validation will take place when:
539
+ #
540
+ # * you explicitly enable validation; <tt>validate: true</tt>
541
+ # * you use autosave; <tt>autosave: true</tt>
542
+ # * the association is a +has_many+ association
543
+ def validate?
544
+ !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || collection?)
545
+ end
546
+
547
+ # Returns +true+ if +self+ is a +belongs_to+ reflection.
548
+ def belongs_to?; false; end
549
+
550
+ # Returns +true+ if +self+ is a +has_one+ reflection.
551
+ def has_one?; false; end
552
+
553
+ def association_class; raise NotImplementedError; end
554
+
555
+ def add_as_source(seed)
556
+ seed
557
+ end
558
+
559
+ def extensions
560
+ Array(options[:extend])
561
+ end
562
+
563
+ protected
564
+
565
+ def actual_source_reflection # FIXME: this is a horrible name
566
+ self
567
+ end
568
+
569
+ private
570
+
571
+ def join_pk(_)
572
+ foreign_key
573
+ end
574
+
575
+ def join_fk
576
+ active_record_primary_key
577
+ end
578
+
579
+ def calculate_constructable(_macro, _options)
580
+ false
581
+ end
582
+
583
+ def derive_class_name
584
+ class_name = name.to_s
585
+ class_name = class_name.singularize if collection?
586
+ class_name.camelize
587
+ end
588
+
589
+ def derive_foreign_key
590
+ if options[:as]
591
+ "#{options[:as]}_id"
592
+ else
593
+ "#{name}_id"
594
+ end
595
+ end
596
+
597
+ def primary_key(klass)
598
+ klass.primary_key || raise(ActiveRecord::UnknownPrimaryKey.new(klass))
599
+ end
600
+ end
601
+
602
+ class BelongsToReflection < AssociationReflection # :nodoc:
603
+ def macro; :belongs_to; end
604
+
605
+ def belongs_to?; true; end
606
+
607
+ def association_class
608
+ Associations::BelongsToAssociation
609
+ end
610
+
611
+ def join_id_for(owner) # :nodoc:
612
+ owner[foreign_key]
613
+ end
614
+
615
+ private
616
+
617
+ def join_fk
618
+ foreign_key
619
+ end
620
+
621
+ def join_pk(_klass)
622
+ association_primary_key
623
+ end
624
+ end
625
+
626
+ class HasManyReflection < AssociationReflection # :nodoc:
627
+ def macro; :has_many; end
628
+
629
+ def collection?; true; end
630
+
631
+ def association_class
632
+ Associations::HasManyAssociation
633
+ end
634
+
635
+ def association_primary_key(klass = nil)
636
+ primary_key(klass || self.klass)
637
+ end
638
+ end
639
+
640
+ class HasOneReflection < AssociationReflection # :nodoc:
641
+ def macro; :has_one; end
642
+
643
+ def has_one?; true; end
644
+
645
+ def association_class
646
+ Associations::HasOneAssociation
647
+ end
648
+ end
649
+ end
650
+ end