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.
- 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
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "active_support/core_ext/module/attribute_accessors"
|
3
|
+
require "duck_record/attribute_mutation_tracker"
|
4
|
+
|
5
|
+
module DuckRecord
|
6
|
+
module AttributeMethods
|
7
|
+
module Dirty # :nodoc:
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
include ActiveModel::Dirty
|
11
|
+
|
12
|
+
def initialize_dup(other) # :nodoc:
|
13
|
+
super
|
14
|
+
@attributes = self.class._default_attributes.map do |attr|
|
15
|
+
attr.with_value_from_user(@attributes.fetch_value(attr.name))
|
16
|
+
end
|
17
|
+
@mutation_tracker = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def changes_applied
|
21
|
+
@previous_mutation_tracker = mutation_tracker
|
22
|
+
@changed_attributes = HashWithIndifferentAccess.new
|
23
|
+
store_original_attributes
|
24
|
+
end
|
25
|
+
|
26
|
+
def clear_changes_information
|
27
|
+
@previous_mutation_tracker = nil
|
28
|
+
@changed_attributes = HashWithIndifferentAccess.new
|
29
|
+
store_original_attributes
|
30
|
+
end
|
31
|
+
|
32
|
+
def raw_write_attribute(attr_name, *)
|
33
|
+
result = super
|
34
|
+
clear_attribute_change(attr_name)
|
35
|
+
result
|
36
|
+
end
|
37
|
+
|
38
|
+
def clear_attribute_changes(attr_names)
|
39
|
+
super
|
40
|
+
attr_names.each do |attr_name|
|
41
|
+
clear_attribute_change(attr_name)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def changed_attributes
|
46
|
+
# This should only be set by methods which will call changed_attributes
|
47
|
+
# multiple times when it is known that the computed value cannot change.
|
48
|
+
if defined?(@cached_changed_attributes)
|
49
|
+
@cached_changed_attributes
|
50
|
+
else
|
51
|
+
super.reverse_merge(mutation_tracker.changed_values).freeze
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def changes
|
56
|
+
cache_changed_attributes do
|
57
|
+
super
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def previous_changes
|
62
|
+
previous_mutation_tracker.changes
|
63
|
+
end
|
64
|
+
|
65
|
+
def attribute_changed_in_place?(attr_name)
|
66
|
+
mutation_tracker.changed_in_place?(attr_name)
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def mutation_tracker
|
72
|
+
unless defined?(@mutation_tracker)
|
73
|
+
@mutation_tracker = nil
|
74
|
+
end
|
75
|
+
@mutation_tracker ||= AttributeMutationTracker.new(@attributes)
|
76
|
+
end
|
77
|
+
|
78
|
+
def changes_include?(attr_name)
|
79
|
+
super || mutation_tracker.changed?(attr_name)
|
80
|
+
end
|
81
|
+
|
82
|
+
def clear_attribute_change(attr_name)
|
83
|
+
mutation_tracker.forget_change(attr_name)
|
84
|
+
end
|
85
|
+
|
86
|
+
def store_original_attributes
|
87
|
+
@attributes = @attributes.map(&:forgetting_assignment)
|
88
|
+
@mutation_tracker = nil
|
89
|
+
end
|
90
|
+
|
91
|
+
def previous_mutation_tracker
|
92
|
+
@previous_mutation_tracker ||= NullMutationTracker.instance
|
93
|
+
end
|
94
|
+
|
95
|
+
def cache_changed_attributes
|
96
|
+
@cached_changed_attributes = changed_attributes
|
97
|
+
yield
|
98
|
+
ensure
|
99
|
+
clear_changed_attributes_cache
|
100
|
+
end
|
101
|
+
|
102
|
+
def clear_changed_attributes_cache
|
103
|
+
remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module DuckRecord
|
2
|
+
module AttributeMethods
|
3
|
+
module Read
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
private
|
8
|
+
|
9
|
+
# We want to generate the methods via module_eval rather than
|
10
|
+
# define_method, because define_method is slower on dispatch.
|
11
|
+
# Evaluating many similar methods may use more memory as the instruction
|
12
|
+
# sequences are duplicated and cached (in MRI). define_method may
|
13
|
+
# be slower on dispatch, but if you're careful about the closure
|
14
|
+
# created, then define_method will consume much less memory.
|
15
|
+
#
|
16
|
+
# But sometimes the database might return columns with
|
17
|
+
# characters that are not allowed in normal method names (like
|
18
|
+
# 'my_column(omg)'. So to work around this we first define with
|
19
|
+
# the __temp__ identifier, and then use alias method to rename
|
20
|
+
# it to what we want.
|
21
|
+
#
|
22
|
+
# We are also defining a constant to hold the frozen string of
|
23
|
+
# the attribute name. Using a constant means that we do not have
|
24
|
+
# to allocate an object on each call to the attribute method.
|
25
|
+
# Making it frozen means that it doesn't get duped when used to
|
26
|
+
# key the @attributes in read_attribute.
|
27
|
+
def define_method_attribute(name)
|
28
|
+
safe_name = name.unpack("h*".freeze).first
|
29
|
+
temp_method = "__temp__#{safe_name}"
|
30
|
+
|
31
|
+
DuckRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
|
32
|
+
|
33
|
+
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
|
34
|
+
def #{temp_method}
|
35
|
+
name = ::DuckRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
|
36
|
+
_read_attribute(name) { |n| missing_attribute(n, caller) }
|
37
|
+
end
|
38
|
+
STR
|
39
|
+
|
40
|
+
generated_attribute_methods.module_eval do
|
41
|
+
alias_method name, temp_method
|
42
|
+
undef_method temp_method
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns the value of the attribute identified by <tt>attr_name</tt> after
|
48
|
+
# it has been typecast (for example, "2004-12-12" in a date column is cast
|
49
|
+
# to a date object, like Date.new(2004, 12, 12)).
|
50
|
+
def read_attribute(attr_name, &block)
|
51
|
+
name = if self.class.attribute_alias?(attr_name)
|
52
|
+
self.class.attribute_alias(attr_name).to_s
|
53
|
+
else
|
54
|
+
attr_name.to_s
|
55
|
+
end
|
56
|
+
|
57
|
+
_read_attribute(name, &block)
|
58
|
+
end
|
59
|
+
|
60
|
+
# This method exists to avoid the expensive primary_key check internally, without
|
61
|
+
# breaking compatibility with the read_attribute API
|
62
|
+
if defined?(JRUBY_VERSION)
|
63
|
+
# This form is significantly faster on JRuby, and this is one of our biggest hotspots.
|
64
|
+
# https://github.com/jruby/jruby/pull/2562
|
65
|
+
def _read_attribute(attr_name, &block) # :nodoc
|
66
|
+
@attributes.fetch_value(attr_name.to_s, &block)
|
67
|
+
end
|
68
|
+
else
|
69
|
+
def _read_attribute(attr_name) # :nodoc:
|
70
|
+
@attributes.fetch_value(attr_name.to_s) { |n| yield n if block_given? }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
alias :attribute :_read_attribute
|
75
|
+
private :attribute
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module DuckRecord
|
2
|
+
module AttributeMethods
|
3
|
+
module Serialization
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
# If you have an attribute that needs to be saved to the database as an
|
8
|
+
# object, and retrieved as the same object, then specify the name of that
|
9
|
+
# attribute using this method and it will be handled automatically. The
|
10
|
+
# serialization is done through YAML. If +class_name+ is specified, the
|
11
|
+
# serialized object must be of that class on assignment and retrieval.
|
12
|
+
# Otherwise SerializationTypeMismatch will be raised.
|
13
|
+
#
|
14
|
+
# Empty objects as <tt>{}</tt>, in the case of +Hash+, or <tt>[]</tt>, in the case of
|
15
|
+
# +Array+, will always be persisted as null.
|
16
|
+
#
|
17
|
+
# Keep in mind that database adapters handle certain serialization tasks
|
18
|
+
# for you. For instance: +json+ and +jsonb+ types in PostgreSQL will be
|
19
|
+
# converted between JSON object/array syntax and Ruby +Hash+ or +Array+
|
20
|
+
# objects transparently. There is no need to use #serialize in this
|
21
|
+
# case.
|
22
|
+
#
|
23
|
+
# For more complex cases, such as conversion to or from your application
|
24
|
+
# domain objects, consider using the ActiveRecord::Attributes API.
|
25
|
+
#
|
26
|
+
# ==== Parameters
|
27
|
+
#
|
28
|
+
# * +attr_name+ - The field name that should be serialized.
|
29
|
+
# * +class_name_or_coder+ - Optional, a coder object, which responds to +.load+ and +.dump+
|
30
|
+
# or a class name that the object type should be equal to.
|
31
|
+
#
|
32
|
+
# ==== Example
|
33
|
+
#
|
34
|
+
# # Serialize a preferences attribute.
|
35
|
+
# class User < ActiveRecord::Base
|
36
|
+
# serialize :preferences
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# # Serialize preferences using JSON as coder.
|
40
|
+
# class User < ActiveRecord::Base
|
41
|
+
# serialize :preferences, JSON
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# # Serialize preferences as Hash using YAML coder.
|
45
|
+
# class User < ActiveRecord::Base
|
46
|
+
# serialize :preferences, Hash
|
47
|
+
# end
|
48
|
+
def serialize(attr_name, class_name_or_coder = Object)
|
49
|
+
# When ::JSON is used, force it to go through the Active Support JSON encoder
|
50
|
+
# to ensure special objects (e.g. Active Record models) are dumped correctly
|
51
|
+
# using the #as_json hook.
|
52
|
+
coder = if class_name_or_coder == ::JSON
|
53
|
+
Coders::JSON
|
54
|
+
elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) }
|
55
|
+
class_name_or_coder
|
56
|
+
else
|
57
|
+
Coders::YAMLColumn.new(attr_name, class_name_or_coder)
|
58
|
+
end
|
59
|
+
decorate_attribute_type(attr_name, :serialize) do |type|
|
60
|
+
Type::Serialized.new(type, coder)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module DuckRecord
|
2
|
+
module AttributeMethods
|
3
|
+
module Write
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
attribute_method_suffix "="
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
private
|
12
|
+
|
13
|
+
def define_method_attribute=(name)
|
14
|
+
safe_name = name.unpack("h*".freeze).first
|
15
|
+
DuckRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
|
16
|
+
|
17
|
+
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
|
18
|
+
def __temp__#{safe_name}=(value)
|
19
|
+
name = ::DuckRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
|
20
|
+
write_attribute(name, value)
|
21
|
+
end
|
22
|
+
alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
|
23
|
+
undef_method :__temp__#{safe_name}=
|
24
|
+
STR
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Updates the attribute identified by <tt>attr_name</tt> with the
|
29
|
+
# specified +value+. Empty strings for Integer and Float columns are
|
30
|
+
# turned into +nil+.
|
31
|
+
def write_attribute(attr_name, value)
|
32
|
+
name =
|
33
|
+
if self.class.attribute_alias?(attr_name)
|
34
|
+
self.class.attribute_alias(attr_name).to_s
|
35
|
+
else
|
36
|
+
attr_name.to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
if self.class.readonly_attributes.include?(name) && attr_readonly_enabled?
|
40
|
+
return
|
41
|
+
end
|
42
|
+
|
43
|
+
write_attribute_with_type_cast(name, value, true)
|
44
|
+
end
|
45
|
+
|
46
|
+
def raw_write_attribute(attr_name, value) # :nodoc:
|
47
|
+
write_attribute_with_type_cast(attr_name, value, false)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# Handle *= for method_missing.
|
53
|
+
def attribute=(attribute_name, value)
|
54
|
+
write_attribute(attribute_name, value)
|
55
|
+
end
|
56
|
+
|
57
|
+
def write_attribute_with_type_cast(attr_name, value, should_type_cast)
|
58
|
+
attr_name = attr_name.to_s
|
59
|
+
|
60
|
+
if should_type_cast
|
61
|
+
@attributes.write_from_user(attr_name, value)
|
62
|
+
else
|
63
|
+
@attributes.write_cast_value(attr_name, value)
|
64
|
+
end
|
65
|
+
|
66
|
+
value
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module DuckRecord
|
2
|
+
class AttributeMutationTracker # :nodoc:
|
3
|
+
OPTION_NOT_GIVEN = Object.new
|
4
|
+
|
5
|
+
def initialize(attributes)
|
6
|
+
@attributes = attributes
|
7
|
+
@forced_changes = Set.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def changed_values
|
11
|
+
attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
|
12
|
+
if changed?(attr_name)
|
13
|
+
result[attr_name] = attributes[attr_name].original_value
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def changes
|
19
|
+
attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
|
20
|
+
change = change_to_attribute(attr_name)
|
21
|
+
if change
|
22
|
+
result[attr_name] = change
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def change_to_attribute(attr_name)
|
28
|
+
if changed?(attr_name)
|
29
|
+
[attributes[attr_name].original_value, attributes.fetch_value(attr_name)]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def any_changes?
|
34
|
+
attr_names.any? { |attr| changed?(attr) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
|
38
|
+
attr_name = attr_name.to_s
|
39
|
+
forced_changes.include?(attr_name) ||
|
40
|
+
attributes[attr_name].changed? &&
|
41
|
+
(OPTION_NOT_GIVEN == from || attributes[attr_name].original_value == from) &&
|
42
|
+
(OPTION_NOT_GIVEN == to || attributes[attr_name].value == to)
|
43
|
+
end
|
44
|
+
|
45
|
+
def changed_in_place?(attr_name)
|
46
|
+
attributes[attr_name].changed_in_place?
|
47
|
+
end
|
48
|
+
|
49
|
+
def forget_change(attr_name)
|
50
|
+
attr_name = attr_name.to_s
|
51
|
+
attributes[attr_name] = attributes[attr_name].forgetting_assignment
|
52
|
+
forced_changes.delete(attr_name)
|
53
|
+
end
|
54
|
+
|
55
|
+
def original_value(attr_name)
|
56
|
+
attributes[attr_name].original_value
|
57
|
+
end
|
58
|
+
|
59
|
+
def force_change(attr_name)
|
60
|
+
forced_changes << attr_name.to_s
|
61
|
+
end
|
62
|
+
|
63
|
+
# TODO Change this to private once we've dropped Ruby 2.2 support.
|
64
|
+
# Workaround for Ruby 2.2 "private attribute?" warning.
|
65
|
+
protected
|
66
|
+
|
67
|
+
attr_reader :attributes, :forced_changes
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def attr_names
|
72
|
+
attributes.keys
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class NullMutationTracker # :nodoc:
|
77
|
+
include Singleton
|
78
|
+
|
79
|
+
def changed_values(*)
|
80
|
+
{}
|
81
|
+
end
|
82
|
+
|
83
|
+
def changes(*)
|
84
|
+
{}
|
85
|
+
end
|
86
|
+
|
87
|
+
def change_to_attribute(_)
|
88
|
+
end
|
89
|
+
|
90
|
+
def any_changes?(*)
|
91
|
+
false
|
92
|
+
end
|
93
|
+
|
94
|
+
def changed?(*)
|
95
|
+
false
|
96
|
+
end
|
97
|
+
|
98
|
+
def changed_in_place?(*)
|
99
|
+
false
|
100
|
+
end
|
101
|
+
|
102
|
+
def forget_change(*)
|
103
|
+
end
|
104
|
+
|
105
|
+
def original_value(*)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require "duck_record/attribute_set/yaml_encoder"
|
2
|
+
|
3
|
+
module DuckRecord
|
4
|
+
class AttributeSet # :nodoc:
|
5
|
+
delegate :each_value, :fetch, to: :attributes
|
6
|
+
|
7
|
+
def initialize(attributes)
|
8
|
+
@attributes = attributes
|
9
|
+
end
|
10
|
+
|
11
|
+
def [](name)
|
12
|
+
attributes[name] || Attribute.null(name)
|
13
|
+
end
|
14
|
+
|
15
|
+
def []=(name, value)
|
16
|
+
attributes[name] = value
|
17
|
+
end
|
18
|
+
|
19
|
+
def values_before_type_cast
|
20
|
+
attributes.transform_values(&:value_before_type_cast)
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_hash
|
24
|
+
initialized_attributes.transform_values(&:value)
|
25
|
+
end
|
26
|
+
alias_method :to_h, :to_hash
|
27
|
+
|
28
|
+
def key?(name)
|
29
|
+
attributes.key?(name) && self[name].initialized?
|
30
|
+
end
|
31
|
+
|
32
|
+
def keys
|
33
|
+
attributes.each_key.select { |name| self[name].initialized? }
|
34
|
+
end
|
35
|
+
|
36
|
+
if defined?(JRUBY_VERSION)
|
37
|
+
# This form is significantly faster on JRuby, and this is one of our biggest hotspots.
|
38
|
+
# https://github.com/jruby/jruby/pull/2562
|
39
|
+
def fetch_value(name, &block)
|
40
|
+
self[name].value(&block)
|
41
|
+
end
|
42
|
+
else
|
43
|
+
def fetch_value(name)
|
44
|
+
self[name].value { |n| yield n if block_given? }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def write_from_user(name, value)
|
49
|
+
attributes[name] = self[name].with_value_from_user(value)
|
50
|
+
end
|
51
|
+
|
52
|
+
def write_cast_value(name, value)
|
53
|
+
attributes[name] = self[name].with_cast_value(value)
|
54
|
+
end
|
55
|
+
|
56
|
+
def freeze
|
57
|
+
@attributes.freeze
|
58
|
+
super
|
59
|
+
end
|
60
|
+
|
61
|
+
def deep_dup
|
62
|
+
dup.tap do |copy|
|
63
|
+
copy.instance_variable_set(:@attributes, attributes.deep_dup)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def initialize_dup(_)
|
68
|
+
@attributes = attributes.dup
|
69
|
+
super
|
70
|
+
end
|
71
|
+
|
72
|
+
def initialize_clone(_)
|
73
|
+
@attributes = attributes.clone
|
74
|
+
super
|
75
|
+
end
|
76
|
+
|
77
|
+
def map(&block)
|
78
|
+
new_attributes = attributes.transform_values(&block)
|
79
|
+
AttributeSet.new(new_attributes)
|
80
|
+
end
|
81
|
+
|
82
|
+
def ==(other)
|
83
|
+
attributes == other.attributes
|
84
|
+
end
|
85
|
+
|
86
|
+
# TODO Change this to private once we've dropped Ruby 2.2 support.
|
87
|
+
# Workaround for Ruby 2.2 "private attribute?" warning.
|
88
|
+
protected
|
89
|
+
|
90
|
+
attr_reader :attributes
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def initialized_attributes
|
95
|
+
attributes.select { |_, attr| attr.initialized? }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|