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