tallty_duck_record 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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