duck_record 0.0.3 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,309 @@
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, options, ar)
16
+ klass = \
17
+ case macro
18
+ when :has_many
19
+ HasManyReflection
20
+ when :has_one
21
+ HasOneReflection
22
+ else
23
+ raise "Unsupported Macro: #{macro}"
24
+ end
25
+
26
+ klass.new(name, options, ar)
27
+ end
28
+
29
+ def self.add_reflection(ar, name, reflection)
30
+ ar.clear_reflections_cache
31
+ ar._reflections = ar._reflections.merge(name.to_s => reflection)
32
+ end
33
+
34
+ # \Reflection enables the ability to examine the associations and aggregations of
35
+ # Active Record classes and objects. This information, for example,
36
+ # can be used in a form builder that takes an Active Record object
37
+ # and creates input fields for all of the attributes depending on their type
38
+ # and displays the associations to other objects.
39
+ #
40
+ # MacroReflection class has info for AggregateReflection and AssociationReflection
41
+ # classes.
42
+ module ClassMethods
43
+ # Returns a Hash of name of the reflection as the key and an AssociationReflection as the value.
44
+ #
45
+ # Account.reflections # => {"balance" => AggregateReflection}
46
+ #
47
+ def reflections
48
+ @__reflections ||= begin
49
+ ref = {}
50
+
51
+ _reflections.each do |name, reflection|
52
+ parent_reflection = reflection.parent_reflection
53
+
54
+ if parent_reflection
55
+ parent_name = parent_reflection.name
56
+ ref[parent_name.to_s] = parent_reflection
57
+ else
58
+ ref[name] = reflection
59
+ end
60
+ end
61
+
62
+ ref
63
+ end
64
+ end
65
+
66
+ # Returns an array of AssociationReflection objects for all the
67
+ # associations in the class. If you only want to reflect on a certain
68
+ # association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>,
69
+ # <tt>:belongs_to</tt>) as the first parameter.
70
+ #
71
+ # Example:
72
+ #
73
+ # Account.reflect_on_all_associations # returns an array of all associations
74
+ # Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
75
+ #
76
+ def reflect_on_all_associations(macro = nil)
77
+ association_reflections = reflections.values
78
+ association_reflections.select! { |reflection| reflection.macro == macro } if macro
79
+ association_reflections
80
+ end
81
+
82
+ # Returns the AssociationReflection object for the +association+ (use the symbol).
83
+ #
84
+ # Account.reflect_on_association(:owner) # returns the owner AssociationReflection
85
+ # Invoice.reflect_on_association(:line_items).macro # returns :has_many
86
+ #
87
+ def reflect_on_association(association)
88
+ reflections[association.to_s]
89
+ end
90
+
91
+ def _reflect_on_association(association) #:nodoc:
92
+ _reflections[association.to_s]
93
+ end
94
+
95
+ def clear_reflections_cache # :nodoc:
96
+ @__reflections = nil
97
+ end
98
+ end
99
+
100
+ # Holds all the methods that are shared between MacroReflection and ThroughReflection.
101
+ #
102
+ # AbstractReflection
103
+ # MacroReflection
104
+ # AggregateReflection
105
+ # AssociationReflection
106
+ # HasManyReflection
107
+ # HasOneReflection
108
+ # BelongsToReflection
109
+ # HasAndBelongsToManyReflection
110
+ # ThroughReflection
111
+ # PolymorphicReflection
112
+ # RuntimeReflection
113
+ class AbstractReflection # :nodoc:
114
+ # Returns a new, unsaved instance of the associated class. +attributes+ will
115
+ # be passed to the class's constructor.
116
+ def build_association(attributes, &block)
117
+ klass.new(attributes, &block)
118
+ end
119
+
120
+ # Returns the class name for the macro.
121
+ #
122
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt>
123
+ # <tt>has_many :clients</tt> returns <tt>'Client'</tt>
124
+ def class_name
125
+ @class_name ||= (options[:class_name] || derive_class_name).to_s
126
+ end
127
+
128
+ def check_validity!
129
+ true
130
+ end
131
+
132
+ def alias_candidate(name)
133
+ "#{plural_name}_#{name}"
134
+ end
135
+ end
136
+
137
+ # Base class for AggregateReflection and AssociationReflection. Objects of
138
+ # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
139
+ class MacroReflection < AbstractReflection
140
+ # Returns the name of the macro.
141
+ #
142
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:balance</tt>
143
+ # <tt>has_many :clients</tt> returns <tt>:clients</tt>
144
+ attr_reader :name
145
+
146
+ # Returns the hash of options used for the macro.
147
+ #
148
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>{ class_name: "Money" }</tt>
149
+ # <tt>has_many :clients</tt> returns <tt>{}</tt>
150
+ attr_reader :options
151
+
152
+ attr_reader :active_record
153
+
154
+ attr_reader :plural_name # :nodoc:
155
+
156
+ def initialize(name, options, active_record)
157
+ @name = name
158
+ @options = options
159
+ @active_record = active_record
160
+ @klass = options[:anonymous_class]
161
+ @plural_name = name.to_s.pluralize
162
+ end
163
+
164
+ # Returns the class for the macro.
165
+ #
166
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class
167
+ # <tt>has_many :clients</tt> returns the Client class
168
+ def klass
169
+ @klass ||= compute_class(class_name)
170
+ end
171
+
172
+ def compute_class(name)
173
+ name.constantize
174
+ end
175
+
176
+ private
177
+
178
+ def derive_class_name
179
+ name.to_s.camelize
180
+ end
181
+ end
182
+
183
+ # Holds all the metadata about an association as it was specified in the
184
+ # Active Record class.
185
+ class AssociationReflection < MacroReflection #:nodoc:
186
+ # Returns the target association's class.
187
+ #
188
+ # class Author < ActiveRecord::Base
189
+ # has_many :books
190
+ # end
191
+ #
192
+ # Author.reflect_on_association(:books).klass
193
+ # # => Book
194
+ #
195
+ # <b>Note:</b> Do not call +klass.new+ or +klass.create+ to instantiate
196
+ # a new association object. Use +build_association+ or +create_association+
197
+ # instead. This allows plugins to hook into association object creation.
198
+ def klass
199
+ @klass ||= compute_class(class_name)
200
+ end
201
+
202
+ def compute_class(name)
203
+ active_record.send(:compute_type, name)
204
+ end
205
+
206
+ attr_accessor :parent_reflection # Reflection
207
+
208
+ def initialize(name, options, active_record)
209
+ super
210
+ @constructable = calculate_constructable(macro, options)
211
+
212
+ if options[:class_name] && options[:class_name].class == Class
213
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
214
+ Passing a class to the `class_name` is deprecated and will raise
215
+ an ArgumentError in Rails 5.2. It eagerloads more classes than
216
+ necessary and potentially creates circular dependencies.
217
+
218
+ Please pass the class name as a string:
219
+ `#{macro} :#{name}, class_name: '#{options[:class_name]}'`
220
+ MSG
221
+ end
222
+ end
223
+
224
+ def constructable? # :nodoc:
225
+ @constructable
226
+ end
227
+
228
+ def source_reflection
229
+ self
230
+ end
231
+
232
+ def nested?
233
+ false
234
+ end
235
+
236
+ # Returns the macro type.
237
+ #
238
+ # <tt>has_many :clients</tt> returns <tt>:has_many</tt>
239
+ def macro; raise NotImplementedError; end
240
+
241
+ # Returns whether or not this association reflection is for a collection
242
+ # association. Returns +true+ if the +macro+ is either +has_many+ or
243
+ # +has_and_belongs_to_many+, +false+ otherwise.
244
+ def collection?
245
+ false
246
+ end
247
+
248
+ # Returns whether or not the association should be validated as part of
249
+ # the parent's validation.
250
+ #
251
+ # Unless you explicitly disable validation with
252
+ # <tt>validate: false</tt>, validation will take place when:
253
+ #
254
+ # * you explicitly enable validation; <tt>validate: true</tt>
255
+ # * you use autosave; <tt>autosave: true</tt>
256
+ # * the association is a +has_many+ association
257
+ def validate?
258
+ !options[:validate].nil? ? options[:validate] : collection?
259
+ end
260
+
261
+ # Returns +true+ if +self+ is a +has_one+ reflection.
262
+ def has_one?; false; end
263
+
264
+ def association_class; raise NotImplementedError; end
265
+
266
+ def add_as_source(seed)
267
+ seed
268
+ end
269
+
270
+ protected
271
+
272
+ def actual_source_reflection # FIXME: this is a horrible name
273
+ self
274
+ end
275
+
276
+ private
277
+
278
+ def calculate_constructable(macro, options)
279
+ true
280
+ end
281
+
282
+ def derive_class_name
283
+ class_name = name.to_s
284
+ class_name = class_name.singularize if collection?
285
+ class_name.camelize
286
+ end
287
+ end
288
+
289
+ class HasManyReflection < AssociationReflection # :nodoc:
290
+ def macro; :has_many; end
291
+
292
+ def collection?; true; end
293
+
294
+ def association_class
295
+ Associations::HasManyAssociation
296
+ end
297
+ end
298
+
299
+ class HasOneReflection < AssociationReflection # :nodoc:
300
+ def macro; :has_one; end
301
+
302
+ def has_one?; true; end
303
+
304
+ def association_class
305
+ Associations::HasOneAssociation
306
+ end
307
+ end
308
+ end
309
+ end
@@ -1,3 +1,3 @@
1
1
  module DuckRecord
2
- VERSION = '0.0.3'
2
+ VERSION = '0.0.5'
3
3
  end
data/lib/duck_record.rb CHANGED
@@ -14,6 +14,8 @@ module DuckRecord
14
14
  autoload :Core
15
15
  autoload :Inheritance
16
16
  autoload :ModelSchema
17
+ autoload :NestedAttributes
18
+ autoload :Reflection
17
19
  autoload :Serialization
18
20
  autoload :Translation
19
21
  autoload :Validations
@@ -21,8 +23,10 @@ module DuckRecord
21
23
  eager_autoload do
22
24
  autoload :DuckRecordError, 'duck_record/errors'
23
25
 
26
+ autoload :Associations
24
27
  autoload :AttributeAssignment
25
28
  autoload :AttributeMethods
29
+ autoload :NestedValidateAssociation
26
30
  end
27
31
 
28
32
  module AttributeMethods
@@ -38,10 +42,12 @@ module DuckRecord
38
42
 
39
43
  def self.eager_load!
40
44
  super
41
- ActiveRecord::AttributeMethods.eager_load!
45
+
46
+ DuckRecord::Associations.eager_load!
47
+ DuckRecord::AttributeMethods.eager_load!
42
48
  end
43
49
  end
44
50
 
45
- # ActiveSupport.on_load(:i18n) do
46
- # I18n.load_path << File.dirname(__FILE__) + '/duck_record/locale/en.yml'
47
- # end
51
+ ActiveSupport.on_load(:i18n) do
52
+ I18n.load_path << File.dirname(__FILE__) + '/duck_record/locale/en.yml'
53
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: duck_record
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - jasl
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-03-11 00:00:00.000000000 Z
11
+ date: 2017-03-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -66,6 +66,18 @@ files:
66
66
  - README.md
67
67
  - Rakefile
68
68
  - lib/duck_record.rb
69
+ - lib/duck_record/associations.rb
70
+ - lib/duck_record/associations/association.rb
71
+ - lib/duck_record/associations/builder/association.rb
72
+ - lib/duck_record/associations/builder/collection_association.rb
73
+ - lib/duck_record/associations/builder/has_many.rb
74
+ - lib/duck_record/associations/builder/has_one.rb
75
+ - lib/duck_record/associations/builder/singular_association.rb
76
+ - lib/duck_record/associations/collection_association.rb
77
+ - lib/duck_record/associations/collection_proxy.rb
78
+ - lib/duck_record/associations/has_many_association.rb
79
+ - lib/duck_record/associations/has_one_association.rb
80
+ - lib/duck_record/associations/singular_association.rb
69
81
  - lib/duck_record/attribute.rb
70
82
  - lib/duck_record/attribute/user_provided_default.rb
71
83
  - lib/duck_record/attribute_assignment.rb
@@ -86,6 +98,9 @@ files:
86
98
  - lib/duck_record/inheritance.rb
87
99
  - lib/duck_record/locale/en.yml
88
100
  - lib/duck_record/model_schema.rb
101
+ - lib/duck_record/nested_attributes.rb
102
+ - lib/duck_record/nested_validate_association.rb
103
+ - lib/duck_record/reflection.rb
89
104
  - lib/duck_record/serialization.rb
90
105
  - lib/duck_record/translation.rb
91
106
  - lib/duck_record/type.rb