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