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.
- checksums.yaml +4 -4
- data/README.md +25 -6
- data/Rakefile +0 -1
- data/lib/duck_record/associations/association.rb +86 -0
- data/lib/duck_record/associations/builder/association.rb +98 -0
- data/lib/duck_record/associations/builder/collection_association.rb +48 -0
- data/lib/duck_record/associations/builder/has_many.rb +7 -0
- data/lib/duck_record/associations/builder/has_one.rb +15 -0
- data/lib/duck_record/associations/builder/singular_association.rb +22 -0
- data/lib/duck_record/associations/collection_association.rb +187 -0
- data/lib/duck_record/associations/collection_proxy.rb +894 -0
- data/lib/duck_record/associations/has_many_association.rb +12 -0
- data/lib/duck_record/associations/has_one_association.rb +12 -0
- data/lib/duck_record/associations/singular_association.rb +39 -0
- data/lib/duck_record/associations.rb +1317 -0
- data/lib/duck_record/base.rb +4 -0
- data/lib/duck_record/errors.rb +8 -0
- data/lib/duck_record/locale/en.yml +1 -5
- data/lib/duck_record/nested_attributes.rb +531 -0
- data/lib/duck_record/nested_validate_association.rb +262 -0
- data/lib/duck_record/reflection.rb +309 -0
- data/lib/duck_record/version.rb +1 -1
- data/lib/duck_record.rb +10 -4
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bc1667096dfb88fc6d2b74ea577440eb5bcf8fd0
|
4
|
+
data.tar.gz: 86a738d000265404c2309bf4d04ff6a5db11d446
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2e92f601db7fae6b7a687cb42caa647ba39ca2441f0174a6999feeb5a0d1537eab686c7297734981ca2bb77109b3eaf7f7a5cff7a1da63f2bff1ef6efe15f2a9
|
7
|
+
data.tar.gz: e61511a9b89654b61a1350d69a748e44cba5b4ef8fd3ef061667db04f12a1d57e00d1a6ac95dfa63a8c32ddf512d7b146c7432a1f0a7f9ea6ba7937dbd5c9977
|
data/README.md
CHANGED
@@ -7,19 +7,39 @@ Actually it's extract from Active Record.
|
|
7
7
|
## Usage
|
8
8
|
|
9
9
|
```ruby
|
10
|
+
class Person < DuckRecord::Base
|
11
|
+
attribute :name, :string
|
12
|
+
attribute :age, :integer
|
13
|
+
|
14
|
+
validates :name, presence: true
|
15
|
+
end
|
16
|
+
|
17
|
+
class Comment < DuckRecord::Base
|
18
|
+
attribute :content, :string
|
19
|
+
|
20
|
+
validates :content, presence: true
|
21
|
+
end
|
22
|
+
|
10
23
|
class Book < DuckRecord::Base
|
24
|
+
has_one :author, class_name: 'Person', validate: true
|
25
|
+
accepts_nested_attributes_for :author
|
26
|
+
|
27
|
+
has_many :comments, validate: true
|
28
|
+
accepts_nested_attributes_for :comments
|
29
|
+
|
11
30
|
attribute :title, :string
|
31
|
+
attribute :tags, :string, array: true
|
12
32
|
attribute :price, :decimal, default: 0
|
33
|
+
attribute :meta, :json, default: {}
|
13
34
|
attribute :bought_at, :datetime, default: -> { Time.new }
|
14
35
|
|
15
|
-
# some types that cheated from PG
|
16
|
-
attribute :tags, :string, array: true
|
17
|
-
attribute :meta, :json, default: {}
|
18
|
-
|
19
36
|
validates :title, presence: true
|
20
37
|
end
|
21
38
|
```
|
22
39
|
|
40
|
+
then use these models like a Active Record model,
|
41
|
+
but remember that can't be persisting!
|
42
|
+
|
23
43
|
## Installation
|
24
44
|
|
25
45
|
Since Duck Record is under early development,
|
@@ -43,12 +63,11 @@ $ gem install duck_record
|
|
43
63
|
|
44
64
|
## TODO
|
45
65
|
|
46
|
-
- `has_one`, `has_many`
|
47
66
|
- refactor that original design for database
|
48
67
|
- update docs
|
49
68
|
- add useful methods
|
50
69
|
- add tests
|
51
|
-
- let me
|
70
|
+
- let me know..
|
52
71
|
|
53
72
|
## Contributing
|
54
73
|
|
data/Rakefile
CHANGED
@@ -0,0 +1,86 @@
|
|
1
|
+
require "active_support/core_ext/array/wrap"
|
2
|
+
|
3
|
+
module DuckRecord
|
4
|
+
module Associations
|
5
|
+
# = Active Record Associations
|
6
|
+
#
|
7
|
+
# This is the root class of all associations ('+ Foo' signifies an included module Foo):
|
8
|
+
#
|
9
|
+
# Association
|
10
|
+
# SingularAssociation
|
11
|
+
# HasOneAssociation + ForeignAssociation
|
12
|
+
# HasOneThroughAssociation + ThroughAssociation
|
13
|
+
# BelongsToAssociation
|
14
|
+
# BelongsToPolymorphicAssociation
|
15
|
+
# CollectionAssociation
|
16
|
+
# HasManyAssociation + ForeignAssociation
|
17
|
+
# HasManyThroughAssociation + ThroughAssociation
|
18
|
+
class Association #:nodoc:
|
19
|
+
attr_reader :owner, :target, :reflection
|
20
|
+
|
21
|
+
delegate :options, to: :reflection
|
22
|
+
|
23
|
+
def initialize(owner, reflection)
|
24
|
+
reflection.check_validity!
|
25
|
+
|
26
|
+
@owner, @reflection = owner, reflection
|
27
|
+
|
28
|
+
reset
|
29
|
+
end
|
30
|
+
|
31
|
+
# Resets the \loaded flag to +false+ and sets the \target to +nil+.
|
32
|
+
def reset
|
33
|
+
@target = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
# Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+.
|
37
|
+
def target=(target)
|
38
|
+
@target = target
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns the class of the target. belongs_to polymorphic overrides this to look at the
|
42
|
+
# polymorphic_type field on the owner.
|
43
|
+
def klass
|
44
|
+
reflection.klass
|
45
|
+
end
|
46
|
+
|
47
|
+
# We can't dump @reflection since it contains the scope proc
|
48
|
+
def marshal_dump
|
49
|
+
ivars = (instance_variables - [:@reflection]).map { |name| [name, instance_variable_get(name)] }
|
50
|
+
[@reflection.name, ivars]
|
51
|
+
end
|
52
|
+
|
53
|
+
def marshal_load(data)
|
54
|
+
reflection_name, ivars = data
|
55
|
+
ivars.each { |name, val| instance_variable_set(name, val) }
|
56
|
+
@reflection = @owner.class._reflect_on_association(reflection_name)
|
57
|
+
end
|
58
|
+
|
59
|
+
def initialize_attributes(record, attributes = nil) #:nodoc:
|
60
|
+
record.assign_attributes(attributes)
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
# Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of
|
66
|
+
# the kind of the class of the associated objects. Meant to be used as
|
67
|
+
# a sanity check when you are about to assign an associated record.
|
68
|
+
def raise_on_type_mismatch!(record)
|
69
|
+
unless record.is_a?(reflection.klass)
|
70
|
+
fresh_class = reflection.class_name.safe_constantize
|
71
|
+
unless fresh_class && record.is_a?(fresh_class)
|
72
|
+
message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, "\
|
73
|
+
"got #{record.inspect} which is an instance of #{record.class}(##{record.class.object_id})"
|
74
|
+
raise ActiveRecord::AssociationTypeMismatch, message
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def build_record(attributes)
|
80
|
+
reflection.build_association(attributes) do |record|
|
81
|
+
initialize_attributes(record, attributes)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# This is the parent Association class which defines the variables
|
2
|
+
# used by all associations.
|
3
|
+
#
|
4
|
+
# The hierarchy is defined as follows:
|
5
|
+
# Association
|
6
|
+
# - SingularAssociation
|
7
|
+
# - BelongsToAssociation
|
8
|
+
# - HasOneAssociation
|
9
|
+
# - CollectionAssociation
|
10
|
+
# - HasManyAssociation
|
11
|
+
|
12
|
+
module DuckRecord::Associations::Builder # :nodoc:
|
13
|
+
class Association #:nodoc:
|
14
|
+
class << self
|
15
|
+
attr_accessor :extensions
|
16
|
+
end
|
17
|
+
self.extensions = []
|
18
|
+
|
19
|
+
VALID_OPTIONS = [:class_name, :anonymous_class, :validate] # :nodoc:
|
20
|
+
|
21
|
+
def self.build(model, name, options, &block)
|
22
|
+
if model.dangerous_attribute_method?(name)
|
23
|
+
raise ArgumentError, "You tried to define an association named #{name} on the model #{model.name}, but " \
|
24
|
+
"this will conflict with a method #{name} already defined by Active Record. " \
|
25
|
+
"Please choose a different association name."
|
26
|
+
end
|
27
|
+
|
28
|
+
extension = define_extensions model, name, &block
|
29
|
+
reflection = create_reflection model, name, options, extension
|
30
|
+
define_accessors model, reflection
|
31
|
+
define_callbacks model, reflection
|
32
|
+
define_validations model, reflection
|
33
|
+
reflection
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.create_reflection(model, name, options, extension = nil)
|
37
|
+
raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)
|
38
|
+
|
39
|
+
validate_options(options)
|
40
|
+
|
41
|
+
DuckRecord::Reflection.create(macro, name, options, model)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.macro
|
45
|
+
raise NotImplementedError
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.valid_options(options)
|
49
|
+
VALID_OPTIONS + Association.extensions.flat_map(&:valid_options)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.validate_options(options)
|
53
|
+
options.assert_valid_keys(valid_options(options))
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.define_extensions(model, name)
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.define_callbacks(model, reflection)
|
60
|
+
Association.extensions.each do |extension|
|
61
|
+
extension.build model, reflection
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Defines the setter and getter methods for the association
|
66
|
+
# class Post < ActiveRecord::Base
|
67
|
+
# has_many :comments
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# Post.first.comments and Post.first.comments= methods are defined by this method...
|
71
|
+
def self.define_accessors(model, reflection)
|
72
|
+
mixin = model.generated_association_methods
|
73
|
+
name = reflection.name
|
74
|
+
define_readers(mixin, name)
|
75
|
+
define_writers(mixin, name)
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.define_readers(mixin, name)
|
79
|
+
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
80
|
+
def #{name}(*args)
|
81
|
+
association(:#{name}).reader(*args)
|
82
|
+
end
|
83
|
+
CODE
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.define_writers(mixin, name)
|
87
|
+
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
88
|
+
def #{name}=(value)
|
89
|
+
association(:#{name}).writer(value)
|
90
|
+
end
|
91
|
+
CODE
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.define_validations(model, reflection)
|
95
|
+
# noop
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# This class is inherited by the has_many and has_many_and_belongs_to_many association classes
|
2
|
+
|
3
|
+
require "duck_record/associations"
|
4
|
+
|
5
|
+
module DuckRecord::Associations::Builder # :nodoc:
|
6
|
+
class CollectionAssociation < Association #:nodoc:
|
7
|
+
CALLBACKS = [:before_add, :after_add]
|
8
|
+
|
9
|
+
def self.valid_options(options)
|
10
|
+
super + [:before_add, :after_add]
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.define_callbacks(model, reflection)
|
14
|
+
super
|
15
|
+
name = reflection.name
|
16
|
+
options = reflection.options
|
17
|
+
CALLBACKS.each { |callback_name|
|
18
|
+
define_callback(model, callback_name, name, options)
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.define_extensions(model, name)
|
23
|
+
if block_given?
|
24
|
+
extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
|
25
|
+
extension = Module.new(&Proc.new)
|
26
|
+
model.parent.const_set(extension_module_name, extension)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.define_callback(model, callback_name, name, options)
|
31
|
+
full_callback_name = "#{callback_name}_for_#{name}"
|
32
|
+
|
33
|
+
# TODO : why do i need method_defined? I think its because of the inheritance chain
|
34
|
+
model.class_attribute full_callback_name unless model.method_defined?(full_callback_name)
|
35
|
+
callbacks = Array(options[callback_name.to_sym]).map do |callback|
|
36
|
+
case callback
|
37
|
+
when Symbol
|
38
|
+
->(_method, owner, record) { owner.send(callback, record) }
|
39
|
+
when Proc
|
40
|
+
->(_method, owner, record) { callback.call(owner, record) }
|
41
|
+
else
|
42
|
+
->(method, owner, record) { callback.send(method, owner, record) }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
model.send "#{full_callback_name}=", callbacks
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module DuckRecord::Associations::Builder # :nodoc:
|
2
|
+
class HasOne < SingularAssociation #:nodoc:
|
3
|
+
def self.macro
|
4
|
+
:has_one
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.define_validations(model, reflection)
|
8
|
+
super
|
9
|
+
|
10
|
+
if reflection.options[:required]
|
11
|
+
model.validates_presence_of reflection.name, message: :required
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# This class is inherited by the has_one and belongs_to association classes
|
2
|
+
|
3
|
+
module DuckRecord::Associations::Builder # :nodoc:
|
4
|
+
class SingularAssociation < Association #:nodoc:
|
5
|
+
def self.define_accessors(model, reflection)
|
6
|
+
super
|
7
|
+
mixin = model.generated_association_methods
|
8
|
+
name = reflection.name
|
9
|
+
|
10
|
+
define_constructors(mixin, name) if reflection.constructable?
|
11
|
+
end
|
12
|
+
|
13
|
+
# Defines the (build|create)_association methods for belongs_to or has_one association
|
14
|
+
def self.define_constructors(mixin, name)
|
15
|
+
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
16
|
+
def build_#{name}(*args, &block)
|
17
|
+
association(:#{name}).build(*args, &block)
|
18
|
+
end
|
19
|
+
CODE
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
module DuckRecord
|
2
|
+
module Associations
|
3
|
+
# = Active Record Association Collection
|
4
|
+
#
|
5
|
+
# CollectionAssociation is an abstract class that provides common stuff to
|
6
|
+
# ease the implementation of association proxies that represent
|
7
|
+
# collections. See the class hierarchy in Association.
|
8
|
+
#
|
9
|
+
# CollectionAssociation:
|
10
|
+
# HasManyAssociation => has_many
|
11
|
+
# HasManyThroughAssociation + ThroughAssociation => has_many :through
|
12
|
+
#
|
13
|
+
# The CollectionAssociation class provides common methods to the collections
|
14
|
+
# defined by +has_and_belongs_to_many+, +has_many+ or +has_many+ with
|
15
|
+
# the +:through association+ option.
|
16
|
+
#
|
17
|
+
# You need to be careful with assumptions regarding the target: The proxy
|
18
|
+
# does not fetch records from the database until it needs them, but new
|
19
|
+
# ones created with +build+ are added to the target. So, the target may be
|
20
|
+
# non-empty and still lack children waiting to be read from the database.
|
21
|
+
# If you look directly to the database you cannot assume that's the entire
|
22
|
+
# collection because new records may have been added to the target, etc.
|
23
|
+
#
|
24
|
+
# If you need to work on all current children, new and existing records,
|
25
|
+
# +load_target+ and the +loaded+ flag are your friends.
|
26
|
+
class CollectionAssociation < Association #:nodoc:
|
27
|
+
# Implements the reader method, e.g. foo.items for Foo.has_many :items
|
28
|
+
def reader
|
29
|
+
@_reader ||= CollectionProxy.new(klass, self)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Implements the writer method, e.g. foo.items= for Foo.has_many :items
|
33
|
+
def writer(records)
|
34
|
+
replace(records)
|
35
|
+
end
|
36
|
+
|
37
|
+
def reset
|
38
|
+
super
|
39
|
+
@target = []
|
40
|
+
end
|
41
|
+
|
42
|
+
def build(attributes = {}, &block)
|
43
|
+
if attributes.is_a?(Array)
|
44
|
+
attributes.collect { |attr| build(attr, &block) }
|
45
|
+
else
|
46
|
+
add_to_target(build_record(attributes)) do |record|
|
47
|
+
yield(record) if block_given?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Add +records+ to this association. Returns +self+ so method calls may
|
53
|
+
# be chained. Since << flattens its argument list and inserts each record,
|
54
|
+
# +push+ and +concat+ behave identically.
|
55
|
+
def concat(*records)
|
56
|
+
records = records.flatten
|
57
|
+
@target.concat records
|
58
|
+
end
|
59
|
+
|
60
|
+
# Removes all records from the association without calling callbacks
|
61
|
+
# on the associated records. It honors the +:dependent+ option. However
|
62
|
+
# if the +:dependent+ value is +:destroy+ then in that case the +:delete_all+
|
63
|
+
# deletion strategy for the association is applied.
|
64
|
+
#
|
65
|
+
# You can force a particular deletion strategy by passing a parameter.
|
66
|
+
#
|
67
|
+
# Example:
|
68
|
+
#
|
69
|
+
# @author.books.delete_all(:nullify)
|
70
|
+
# @author.books.delete_all(:delete_all)
|
71
|
+
#
|
72
|
+
# See delete for more info.
|
73
|
+
def delete_all
|
74
|
+
@target.clear
|
75
|
+
end
|
76
|
+
|
77
|
+
# Removes +records+ from this association calling +before_remove+ and
|
78
|
+
# +after_remove+ callbacks.
|
79
|
+
#
|
80
|
+
# This method is abstract in the sense that +delete_records+ has to be
|
81
|
+
# provided by descendants. Note this method does not imply the records
|
82
|
+
# are actually removed from the database, that depends precisely on
|
83
|
+
# +delete_records+. They are in any case removed from the collection.
|
84
|
+
def delete(*records)
|
85
|
+
return if records.empty?
|
86
|
+
@target = @target - records
|
87
|
+
end
|
88
|
+
|
89
|
+
# Deletes the +records+ and removes them from this association calling
|
90
|
+
# +before_remove+ , +after_remove+ , +before_destroy+ and +after_destroy+ callbacks.
|
91
|
+
#
|
92
|
+
# Note that this method removes records from the database ignoring the
|
93
|
+
# +:dependent+ option.
|
94
|
+
def destroy(*records)
|
95
|
+
return if records.empty?
|
96
|
+
records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) }
|
97
|
+
delete_or_destroy(records, :destroy)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns the size of the collection by executing a SELECT COUNT(*)
|
101
|
+
# query if the collection hasn't been loaded, and calling
|
102
|
+
# <tt>collection.size</tt> if it has.
|
103
|
+
#
|
104
|
+
# If the collection has been already loaded +size+ and +length+ are
|
105
|
+
# equivalent. If not and you are going to need the records anyway
|
106
|
+
# +length+ will take one less query. Otherwise +size+ is more efficient.
|
107
|
+
#
|
108
|
+
# This method is abstract in the sense that it relies on
|
109
|
+
# +count_records+, which is a method descendants have to provide.
|
110
|
+
def size
|
111
|
+
@target.size
|
112
|
+
end
|
113
|
+
|
114
|
+
def uniq
|
115
|
+
@target.uniq!
|
116
|
+
end
|
117
|
+
|
118
|
+
# Returns true if the collection is empty.
|
119
|
+
#
|
120
|
+
# If the collection has been loaded
|
121
|
+
# it is equivalent to <tt>collection.size.zero?</tt>. If the
|
122
|
+
# collection has not been loaded, it is equivalent to
|
123
|
+
# <tt>collection.exists?</tt>. If the collection has not already been
|
124
|
+
# loaded and you are going to fetch the records anyway it is better to
|
125
|
+
# check <tt>collection.length.zero?</tt>.
|
126
|
+
def empty?
|
127
|
+
@target.blank?
|
128
|
+
end
|
129
|
+
|
130
|
+
# Replace this collection with +other_array+. This will perform a diff
|
131
|
+
# and delete/add only records that have changed.
|
132
|
+
def replace(other_array)
|
133
|
+
@target = other_array
|
134
|
+
end
|
135
|
+
|
136
|
+
def include?(record)
|
137
|
+
@target.include?(record)
|
138
|
+
end
|
139
|
+
|
140
|
+
def add_to_target(record, skip_callbacks = false, &block)
|
141
|
+
index = @target.index(record)
|
142
|
+
|
143
|
+
replace_on_target(record, index, skip_callbacks, &block)
|
144
|
+
end
|
145
|
+
|
146
|
+
def replace_on_target(record, index, skip_callbacks)
|
147
|
+
callback(:before_add, record) unless skip_callbacks
|
148
|
+
|
149
|
+
begin
|
150
|
+
if index
|
151
|
+
record_was = target[index]
|
152
|
+
target[index] = record
|
153
|
+
else
|
154
|
+
target << record
|
155
|
+
end
|
156
|
+
|
157
|
+
yield(record) if block_given?
|
158
|
+
rescue
|
159
|
+
if index
|
160
|
+
target[index] = record_was
|
161
|
+
else
|
162
|
+
target.delete(record)
|
163
|
+
end
|
164
|
+
|
165
|
+
raise
|
166
|
+
end
|
167
|
+
|
168
|
+
callback(:after_add, record) unless skip_callbacks
|
169
|
+
|
170
|
+
record
|
171
|
+
end
|
172
|
+
|
173
|
+
private
|
174
|
+
|
175
|
+
def callback(method, record)
|
176
|
+
callbacks_for(method).each do |callback|
|
177
|
+
callback.call(method, owner, record)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def callbacks_for(callback_name)
|
182
|
+
full_callback_name = "#{callback_name}_for_#{reflection.name}"
|
183
|
+
owner.class.send(full_callback_name)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|