tallty_duck_record 1.0.0

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