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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 45f98d3af5325759f0a61afac5487e16fadcec80c6f0516f68ddbb7d4d0beddd
4
+ data.tar.gz: f4e17fdbb154b415ced973b7dcaf4546cc0264378b5d54b97bc5d784b005c99b
5
+ SHA512:
6
+ metadata.gz: 6b599f24166fc44208e16e07ff8563f952bd9cc4d01561e21f7980cedb669d4c71107e4b2529372f5a765daab03c89f2b10108aec1cf533cda8ef0bc2fc414d8
7
+ data.tar.gz: e09ccf3f1a5a0b400c1000738aea0a2bb21ba0980128eb873323fc69eb7d28adcf5c40484d038a9ef2c31b7f0b1e87c68a2d2d62bd7ed6c8b85bbfa69e474491
@@ -0,0 +1,41 @@
1
+ Copyright 2017 Jun Jiang
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ Copyright (c) 2004-2017 David Heinemeier Hansson
23
+
24
+ Permission is hereby granted, free of charge, to any person obtaining
25
+ a copy of this software and associated documentation files (the
26
+ "Software"), to deal in the Software without restriction, including
27
+ without limitation the rights to use, copy, modify, merge, publish,
28
+ distribute, sublicense, and/or sell copies of the Software, and to
29
+ permit persons to whom the Software is furnished to do so, subject to
30
+ the following conditions:
31
+
32
+ The above copyright notice and this permission notice shall be
33
+ included in all copies or substantial portions of the Software.
34
+
35
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
36
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
37
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
38
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
39
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
40
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
41
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,82 @@
1
+ Duck Record
2
+ ====
3
+
4
+ It looks like Active Record and quacks like Active Record, it's Duck Record!
5
+ Actually it's extract from Active Record.
6
+
7
+ ## Usage
8
+
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
+
23
+ class Book < DuckRecord::Base
24
+ embeds_one :author, class_name: 'Person', validate: true
25
+ accepts_nested_attributes_for :author
26
+
27
+ embeds_many :comments, validate: true
28
+ accepts_nested_attributes_for :comments
29
+
30
+ attribute :title, :string
31
+ attribute :tags, :string, array: true
32
+ attribute :price, :decimal, default: 0
33
+ attribute :meta, :json, default: {}
34
+ attribute :bought_at, :datetime, default: -> { Time.new }
35
+
36
+ validates :title, presence: true
37
+ end
38
+ ```
39
+
40
+ then use these models like a Active Record model,
41
+ but remember that can't be persisting!
42
+
43
+ ## Installation
44
+
45
+ Since Duck Record is under early development,
46
+ I suggest you fetch the gem through GitHub.
47
+
48
+ Add this line to your application's Gemfile:
49
+
50
+ ```ruby
51
+ gem 'duck_record', github: 'jasl/duck_record'
52
+ ```
53
+
54
+ And then execute:
55
+ ```bash
56
+ $ bundle
57
+ ```
58
+
59
+ Or install it yourself as:
60
+ ```bash
61
+ $ gem install duck_record
62
+ ```
63
+
64
+ ## TODO
65
+
66
+ - refactor that original design for database
67
+ - update docs
68
+ - add useful methods
69
+ - add tests
70
+ - let me know..
71
+
72
+ ## Contributing
73
+
74
+ - Fork the project.
75
+ - Make your feature addition or bug fix.
76
+ - Add tests for it. This is important so I don't break it in a future version unintentionally.
77
+ - Commit, do not mess with Rakefile or version (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
78
+ - Send me a pull request. Bonus points for topic branches.
79
+
80
+ ## License
81
+
82
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,28 @@
1
+ begin
2
+ require "bundler/setup"
3
+ rescue LoadError
4
+ puts "You must `gem install bundler` and `bundle install` to run rake tasks"
5
+ end
6
+
7
+ require "rdoc/task"
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = "rdoc"
11
+ rdoc.title = "DuckRecord"
12
+ rdoc.options << "--line-numbers"
13
+ rdoc.rdoc_files.include("README.md")
14
+ rdoc.rdoc_files.include("lib/**/*.rb")
15
+ end
16
+
17
+ require "bundler/gem_tasks"
18
+
19
+ require "rake/testtask"
20
+
21
+ Rake::TestTask.new(:test) do |t|
22
+ t.libs << "lib"
23
+ t.libs << "test"
24
+ t.pattern = "test/**/*_test.rb"
25
+ t.verbose = false
26
+ end
27
+
28
+ task default: :test
@@ -0,0 +1,46 @@
1
+ class ArrayWithoutBlank < Array
2
+ def self.new(*several_variants)
3
+ arr = super
4
+ arr.reject!(&:blank?)
5
+ arr
6
+ end
7
+
8
+ def initialize_copy(other_ary)
9
+ super other_ary.reject(&:blank?)
10
+ end
11
+
12
+ def replace(other_ary)
13
+ super other_ary.reject(&:blank?)
14
+ end
15
+
16
+ def push(obj, *smth)
17
+ return self if obj.blank?
18
+ super
19
+ end
20
+
21
+ def insert(*args)
22
+ super *args.reject(&:blank?)
23
+ end
24
+
25
+ def []=(index, obj)
26
+ return self[index] if obj.blank?
27
+ super
28
+ end
29
+
30
+ def concat(other_ary)
31
+ super other_ary.reject(&:blank?)
32
+ end
33
+
34
+ def +(other_ary)
35
+ super other_ary.reject(&:blank?)
36
+ end
37
+
38
+ def <<(obj)
39
+ return self if obj.blank?
40
+ super
41
+ end
42
+
43
+ def to_ary
44
+ Array.new(self)
45
+ end
46
+ end
@@ -0,0 +1,65 @@
1
+ require "active_support"
2
+ require "active_support/rails"
3
+ require "active_model"
4
+
5
+ require "core_ext/array_without_blank"
6
+
7
+ require "duck_record/type"
8
+ require "duck_record/attribute_set"
9
+
10
+ module DuckRecord
11
+ extend ActiveSupport::Autoload
12
+
13
+ autoload :Attribute
14
+ autoload :AttributeDecorators
15
+ autoload :Base
16
+ autoload :Callbacks
17
+ autoload :Core
18
+ autoload :Enum
19
+ autoload :Inheritance
20
+ autoload :Persistence
21
+ autoload :ModelSchema
22
+ autoload :NestedAttributes
23
+ autoload :ReadonlyAttributes
24
+ autoload :Reflection
25
+ autoload :Serialization
26
+ autoload :Translation
27
+ autoload :Validations
28
+
29
+ eager_autoload do
30
+ autoload :DuckRecordError, "duck_record/errors"
31
+
32
+ autoload :Associations
33
+ autoload :AttributeAssignment
34
+ autoload :AttributeMethods
35
+ autoload :NestedValidateAssociation
36
+ end
37
+
38
+ module Coders
39
+ autoload :YAMLColumn, "duck_record/coders/yaml_column"
40
+ autoload :JSON, "duck_record/coders/json"
41
+ end
42
+
43
+ module AttributeMethods
44
+ extend ActiveSupport::Autoload
45
+
46
+ eager_autoload do
47
+ autoload :BeforeTypeCast
48
+ autoload :Dirty
49
+ autoload :Read
50
+ autoload :Serialization
51
+ autoload :Write
52
+ end
53
+ end
54
+
55
+ def self.eager_load!
56
+ super
57
+
58
+ DuckRecord::Associations.eager_load!
59
+ DuckRecord::AttributeMethods.eager_load!
60
+ end
61
+ end
62
+
63
+ ActiveSupport.on_load(:i18n) do
64
+ I18n.load_path << File.dirname(__FILE__) + "/duck_record/locale/en.yml"
65
+ end
@@ -0,0 +1,130 @@
1
+ require "active_support/core_ext/enumerable"
2
+ require "active_support/core_ext/string/conversions"
3
+ require "active_support/core_ext/module/remove_method"
4
+ require "duck_record/errors"
5
+
6
+ module DuckRecord
7
+ class AssociationNotFoundError < ConfigurationError #:nodoc:
8
+ def initialize(record = nil, association_name = nil)
9
+ if record && association_name
10
+ super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?")
11
+ else
12
+ super("Association was not found.")
13
+ end
14
+ end
15
+ end
16
+
17
+ # See ActiveRecord::Associations::ClassMethods for documentation.
18
+ module Associations # :nodoc:
19
+ extend ActiveSupport::Autoload
20
+ extend ActiveSupport::Concern
21
+
22
+ # These classes will be loaded when associations are created.
23
+ # So there is no need to eager load them.
24
+ autoload :EmbedsAssociation
25
+ autoload :EmbedsManyProxy
26
+
27
+ autoload :Association
28
+ autoload :SingularAssociation
29
+ autoload :CollectionAssociation
30
+ autoload :ForeignAssociation
31
+ autoload :CollectionProxy
32
+ autoload :ThroughAssociation
33
+
34
+ module Builder #:nodoc:
35
+ autoload :Association, "duck_record/associations/builder/association"
36
+ autoload :SingularAssociation, "duck_record/associations/builder/singular_association"
37
+ autoload :CollectionAssociation, "duck_record/associations/builder/collection_association"
38
+
39
+ autoload :EmbedsOne, "duck_record/associations/builder/embeds_one"
40
+ autoload :EmbedsMany, "duck_record/associations/builder/embeds_many"
41
+
42
+ autoload :BelongsTo, "duck_record/associations/builder/belongs_to"
43
+ autoload :HasOne, "duck_record/associations/builder/has_one"
44
+ autoload :HasMany, "duck_record/associations/builder/has_many"
45
+ end
46
+
47
+ eager_autoload do
48
+ autoload :EmbedsManyAssociation
49
+ autoload :EmbedsOneAssociation
50
+
51
+ autoload :BelongsToAssociation
52
+ autoload :HasOneAssociation
53
+ autoload :HasOneThroughAssociation
54
+ autoload :HasManyAssociation
55
+ autoload :HasManyThroughAssociation
56
+ end
57
+
58
+ # Returns the association instance for the given name, instantiating it if it doesn't already exist
59
+ def association(name) #:nodoc:
60
+ association = association_instance_get(name)
61
+
62
+ if association.nil?
63
+ unless reflection = self.class._reflect_on_association(name)
64
+ raise AssociationNotFoundError.new(self, name)
65
+ end
66
+ association = reflection.association_class.new(self, reflection)
67
+ association_instance_set(name, association)
68
+ end
69
+
70
+ association
71
+ end
72
+
73
+ def association_cached?(name) # :nodoc
74
+ @association_cache.key?(name)
75
+ end
76
+
77
+ def initialize_dup(*) # :nodoc:
78
+ @association_cache = {}
79
+ super
80
+ end
81
+
82
+ private
83
+ # Clears out the association cache.
84
+ def clear_association_cache
85
+ @association_cache.clear if persisted?
86
+ end
87
+
88
+ def init_internals
89
+ @association_cache = {}
90
+ super
91
+ end
92
+
93
+ # Returns the specified association instance if it exists, +nil+ otherwise.
94
+ def association_instance_get(name)
95
+ @association_cache[name]
96
+ end
97
+
98
+ # Set the specified association instance.
99
+ def association_instance_set(name, association)
100
+ @association_cache[name] = association
101
+ end
102
+
103
+ module ClassMethods
104
+ def embeds_many(name, options = {}, &extension)
105
+ reflection = Builder::EmbedsMany.build(self, name, nil, options, &extension)
106
+ Reflection.add_reflection self, name, reflection
107
+ end
108
+
109
+ def embeds_one(name, options = {})
110
+ reflection = Builder::EmbedsOne.build(self, name, nil, options)
111
+ Reflection.add_reflection self, name, reflection
112
+ end
113
+
114
+ def belongs_to(name, scope = nil, options = {})
115
+ reflection = Builder::BelongsTo.build(self, name, scope, options)
116
+ Reflection.add_reflection self, name, reflection
117
+ end
118
+
119
+ def has_one(name, scope = nil, options = {})
120
+ reflection = Builder::HasOne.build(self, name, scope, options)
121
+ Reflection.add_reflection self, name, reflection
122
+ end
123
+
124
+ def has_many(name, scope = nil, options = {}, &extension)
125
+ reflection = Builder::HasMany.build(self, name, scope, options, &extension)
126
+ Reflection.add_reflection self, name, reflection
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,271 @@
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
+ reset_scope
30
+ end
31
+
32
+ # Returns the name of the table of the associated class:
33
+ #
34
+ # post.comments.aliased_table_name # => "comments"
35
+ #
36
+ def aliased_table_name
37
+ klass.table_name
38
+ end
39
+
40
+ # Resets the \loaded flag to +false+ and sets the \target to +nil+.
41
+ def reset
42
+ @loaded = false
43
+ @target = nil
44
+ @stale_state = nil
45
+ end
46
+
47
+ # Reloads the \target and returns +self+ on success.
48
+ def reload
49
+ reset
50
+ reset_scope
51
+ load_target
52
+ self unless target.nil?
53
+ end
54
+
55
+ # Has the \target been already \loaded?
56
+ def loaded?
57
+ @loaded
58
+ end
59
+
60
+ # Asserts the \target has been loaded setting the \loaded flag to +true+.
61
+ def loaded!
62
+ @loaded = true
63
+ @stale_state = stale_state
64
+ end
65
+
66
+ # The target is stale if the target no longer points to the record(s) that the
67
+ # relevant foreign_key(s) refers to. If stale, the association accessor method
68
+ # on the owner will reload the target. It's up to subclasses to implement the
69
+ # stale_state method if relevant.
70
+ #
71
+ # Note that if the target has not been loaded, it is not considered stale.
72
+ def stale_target?
73
+ loaded? && @stale_state != stale_state
74
+ end
75
+
76
+ # Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+.
77
+ def target=(target)
78
+ @target = target
79
+ loaded!
80
+ end
81
+
82
+ def scope
83
+ target_scope.merge!(association_scope)
84
+ end
85
+
86
+ # The scope for this association.
87
+ #
88
+ # Note that the association_scope is merged into the target_scope only when the
89
+ # scope method is called. This is because at that point the call may be surrounded
90
+ # by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which
91
+ # actually gets built.
92
+ def association_scope
93
+ return unless klass
94
+
95
+ @association_scope ||= ActiveRecord::Associations::AssociationScope.scope(self)
96
+ rescue ArgumentError
97
+ @association_scope ||= ActiveRecord::Associations::AssociationScope.scope(self, klass.connection)
98
+ end
99
+
100
+ def reset_scope
101
+ @association_scope = nil
102
+ end
103
+
104
+ # Set the inverse association, if possible
105
+ def set_inverse_instance(record)
106
+ record
107
+ end
108
+
109
+ # Remove the inverse association, if possible
110
+ def remove_inverse_instance(_record); end
111
+
112
+ # Returns the class of the target. belongs_to polymorphic overrides this to look at the
113
+ # polymorphic_type field on the owner.
114
+ def klass
115
+ reflection.klass
116
+ end
117
+
118
+ # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
119
+ # through association's scope)
120
+ def target_scope
121
+ ActiveRecord::AssociationRelation.create(klass, self).merge!(klass.all)
122
+ rescue ArgumentError
123
+ ActiveRecord::AssociationRelation.create(klass, klass.arel_table, klass.predicate_builder, self).merge!(klass.all)
124
+ end
125
+
126
+ def extensions
127
+ extensions = klass.default_extensions | reflection.extensions
128
+
129
+ if scope = reflection.scope
130
+ extensions |= klass.unscoped.instance_exec(owner, &scope).extensions
131
+ end
132
+
133
+ extensions
134
+ end
135
+
136
+ # Loads the \target if needed and returns it.
137
+ #
138
+ # This method is abstract in the sense that it relies on +find_target+,
139
+ # which is expected to be provided by descendants.
140
+ #
141
+ # If the \target is already \loaded it is just returned. Thus, you can call
142
+ # +load_target+ unconditionally to get the \target.
143
+ #
144
+ # ActiveRecord::RecordNotFound is rescued within the method, and it is
145
+ # not reraised. The proxy is \reset and +nil+ is the return value.
146
+ def load_target
147
+ @target = find_target if (@stale_state && stale_target?) || find_target?
148
+
149
+ loaded! unless loaded?
150
+ target
151
+ rescue ActiveRecord::RecordNotFound
152
+ reset
153
+ end
154
+
155
+ def interpolate(sql, record = nil)
156
+ if sql.respond_to?(:to_proc)
157
+ owner.instance_exec(record, &sql)
158
+ else
159
+ sql
160
+ end
161
+ end
162
+
163
+ # We can't dump @reflection since it contains the scope proc
164
+ def marshal_dump
165
+ ivars = (instance_variables - [:@reflection]).map { |name| [name, instance_variable_get(name)] }
166
+ [@reflection.name, ivars]
167
+ end
168
+
169
+ def marshal_load(data)
170
+ reflection_name, ivars = data
171
+ ivars.each { |name, val| instance_variable_set(name, val) }
172
+ @reflection = @owner.class._reflect_on_association(reflection_name)
173
+ end
174
+
175
+ def initialize_attributes(record, except_from_scope_attributes = nil) #:nodoc:
176
+ except_from_scope_attributes ||= {}
177
+ skip_assign = [reflection.foreign_key, reflection.type].compact
178
+ assigned_keys = record.changed_attribute_names_to_save
179
+ assigned_keys += except_from_scope_attributes.keys.map(&:to_s)
180
+ attributes = create_scope.except(*(assigned_keys - skip_assign))
181
+ record.assign_attributes(attributes)
182
+ end
183
+
184
+ def create(attributes = {}, &block)
185
+ _create_record(attributes, &block)
186
+ end
187
+
188
+ def create!(attributes = {}, &block)
189
+ _create_record(attributes, true, &block)
190
+ end
191
+
192
+ private
193
+
194
+ def find_target?
195
+ !loaded? && foreign_key_present? && klass
196
+ end
197
+
198
+ def creation_attributes
199
+ attributes = {}
200
+
201
+ if (reflection.has_one? || reflection.collection?) && !options[:through]
202
+ attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
203
+
204
+ if reflection.options[:as]
205
+ attributes[reflection.type] = owner.class.base_class.name
206
+ end
207
+ end
208
+
209
+ attributes
210
+ end
211
+
212
+ # Sets the owner attributes on the given record
213
+ def set_owner_attributes(record)
214
+ creation_attributes.each { |key, value| record[key] = value }
215
+ end
216
+
217
+ # Returns true if there is a foreign key present on the owner which
218
+ # references the target. This is used to determine whether we can load
219
+ # the target if the owner is currently a new record (and therefore
220
+ # without a key). If the owner is a new record then foreign_key must
221
+ # be present in order to load target.
222
+ #
223
+ # Currently implemented by belongs_to (vanilla and polymorphic) and
224
+ # has_one/has_many :through associations which go through a belongs_to.
225
+ def foreign_key_present?
226
+ false
227
+ end
228
+
229
+ # Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of
230
+ # the kind of the class of the associated objects. Meant to be used as
231
+ # a sanity check when you are about to assign an associated record.
232
+ def raise_on_type_mismatch!(record)
233
+ unless record.is_a?(reflection.klass)
234
+ fresh_class = reflection.class_name.safe_constantize
235
+ unless fresh_class && record.is_a?(fresh_class)
236
+ message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, "\
237
+ "got #{record.inspect} which is an instance of #{record.class}(##{record.class.object_id})"
238
+ raise DuckRecord::AssociationTypeMismatch, message
239
+ end
240
+ end
241
+ end
242
+
243
+ # Returns true if record contains the foreign_key
244
+ def foreign_key_for?(record)
245
+ record.has_attribute?(reflection.foreign_key)
246
+ end
247
+
248
+ # This should be implemented to return the values of the relevant key(s) on the owner,
249
+ # so that when stale_state is different from the value stored on the last find_target,
250
+ # the target is stale.
251
+ #
252
+ # This is only relevant to certain associations, which is why it returns +nil+ by default.
253
+ def stale_state
254
+ end
255
+
256
+ def build_record(attributes)
257
+ reflection.build_association(attributes) do |record|
258
+ initialize_attributes(record, attributes)
259
+ end
260
+ end
261
+
262
+ # Returns true if statement cache should be skipped on the association reader.
263
+ def skip_statement_cache?
264
+ reflection.has_scope? ||
265
+ scope.eager_loading? ||
266
+ klass.scope_attributes? ||
267
+ reflection.source_reflection.active_record.try(:default_scopes)&.any?
268
+ end
269
+ end
270
+ end
271
+ end