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