tallty_duck_record 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +41 -0
- data/README.md +82 -0
- data/Rakefile +28 -0
- data/lib/core_ext/array_without_blank.rb +46 -0
- data/lib/duck_record.rb +65 -0
- data/lib/duck_record/associations.rb +130 -0
- data/lib/duck_record/associations/association.rb +271 -0
- data/lib/duck_record/associations/belongs_to_association.rb +71 -0
- data/lib/duck_record/associations/builder/association.rb +127 -0
- data/lib/duck_record/associations/builder/belongs_to.rb +44 -0
- data/lib/duck_record/associations/builder/collection_association.rb +45 -0
- data/lib/duck_record/associations/builder/embeds_many.rb +9 -0
- data/lib/duck_record/associations/builder/embeds_one.rb +9 -0
- data/lib/duck_record/associations/builder/has_many.rb +11 -0
- data/lib/duck_record/associations/builder/has_one.rb +20 -0
- data/lib/duck_record/associations/builder/singular_association.rb +33 -0
- data/lib/duck_record/associations/collection_association.rb +476 -0
- data/lib/duck_record/associations/collection_proxy.rb +1160 -0
- data/lib/duck_record/associations/embeds_association.rb +92 -0
- data/lib/duck_record/associations/embeds_many_association.rb +203 -0
- data/lib/duck_record/associations/embeds_many_proxy.rb +892 -0
- data/lib/duck_record/associations/embeds_one_association.rb +48 -0
- data/lib/duck_record/associations/foreign_association.rb +11 -0
- data/lib/duck_record/associations/has_many_association.rb +17 -0
- data/lib/duck_record/associations/has_one_association.rb +39 -0
- data/lib/duck_record/associations/singular_association.rb +73 -0
- data/lib/duck_record/attribute.rb +213 -0
- data/lib/duck_record/attribute/user_provided_default.rb +30 -0
- data/lib/duck_record/attribute_assignment.rb +118 -0
- data/lib/duck_record/attribute_decorators.rb +89 -0
- data/lib/duck_record/attribute_methods.rb +325 -0
- data/lib/duck_record/attribute_methods/before_type_cast.rb +76 -0
- data/lib/duck_record/attribute_methods/dirty.rb +107 -0
- data/lib/duck_record/attribute_methods/read.rb +78 -0
- data/lib/duck_record/attribute_methods/serialization.rb +66 -0
- data/lib/duck_record/attribute_methods/write.rb +70 -0
- data/lib/duck_record/attribute_mutation_tracker.rb +108 -0
- data/lib/duck_record/attribute_set.rb +98 -0
- data/lib/duck_record/attribute_set/yaml_encoder.rb +41 -0
- data/lib/duck_record/attributes.rb +262 -0
- data/lib/duck_record/base.rb +300 -0
- data/lib/duck_record/callbacks.rb +324 -0
- data/lib/duck_record/coders/json.rb +13 -0
- data/lib/duck_record/coders/yaml_column.rb +48 -0
- data/lib/duck_record/core.rb +262 -0
- data/lib/duck_record/define_callbacks.rb +23 -0
- data/lib/duck_record/enum.rb +139 -0
- data/lib/duck_record/errors.rb +71 -0
- data/lib/duck_record/inheritance.rb +130 -0
- data/lib/duck_record/locale/en.yml +46 -0
- data/lib/duck_record/model_schema.rb +71 -0
- data/lib/duck_record/nested_attributes.rb +555 -0
- data/lib/duck_record/nested_validate_association.rb +262 -0
- data/lib/duck_record/persistence.rb +39 -0
- data/lib/duck_record/readonly_attributes.rb +36 -0
- data/lib/duck_record/reflection.rb +650 -0
- data/lib/duck_record/serialization.rb +26 -0
- data/lib/duck_record/translation.rb +22 -0
- data/lib/duck_record/type.rb +77 -0
- data/lib/duck_record/type/array.rb +36 -0
- data/lib/duck_record/type/array_without_blank.rb +36 -0
- data/lib/duck_record/type/date.rb +7 -0
- data/lib/duck_record/type/date_time.rb +7 -0
- data/lib/duck_record/type/decimal_without_scale.rb +13 -0
- data/lib/duck_record/type/internal/abstract_json.rb +33 -0
- data/lib/duck_record/type/internal/timezone.rb +15 -0
- data/lib/duck_record/type/json.rb +6 -0
- data/lib/duck_record/type/registry.rb +97 -0
- data/lib/duck_record/type/serialized.rb +63 -0
- data/lib/duck_record/type/text.rb +9 -0
- data/lib/duck_record/type/time.rb +19 -0
- data/lib/duck_record/type/unsigned_integer.rb +15 -0
- data/lib/duck_record/validations.rb +67 -0
- data/lib/duck_record/validations/subset.rb +74 -0
- data/lib/duck_record/validations/uniqueness_on_real_record.rb +248 -0
- data/lib/duck_record/version.rb +3 -0
- data/lib/tasks/acts_as_record_tasks.rake +4 -0
- metadata +181 -0
checksums.yaml
ADDED
@@ -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
|
data/MIT-LICENSE
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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).
|
data/Rakefile
ADDED
@@ -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
|
data/lib/duck_record.rb
ADDED
@@ -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
|