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,48 @@
1
+ module DuckRecord
2
+ module Associations
3
+ class EmbedsOneAssociation < EmbedsAssociation #:nodoc:
4
+ # Implements the reader method, e.g. foo.bar for Foo.has_one :bar
5
+ def reader
6
+ target
7
+ end
8
+
9
+ # Implements the writer method, e.g. foo.bar= for Foo.belongs_to :bar
10
+ def writer(record)
11
+ replace(record)
12
+ end
13
+
14
+ def build(attributes = {})
15
+ record = build_record(attributes)
16
+ yield(record) if block_given?
17
+ set_new_record(record)
18
+ record
19
+ end
20
+
21
+ # Implements the reload reader method, e.g. foo.reload_bar for
22
+ # Foo.has_one :bar
23
+ def force_reload_reader
24
+ klass.uncached { reload }
25
+ target
26
+ end
27
+
28
+ private
29
+
30
+ def replace(record)
31
+ self.target =
32
+ if record.is_a? klass
33
+ record
34
+ elsif record.nil?
35
+ nil
36
+ elsif record.respond_to?(:to_h)
37
+ build_record(record.to_h)
38
+ end
39
+ rescue
40
+ raise_on_type_mismatch!(record)
41
+ end
42
+
43
+ def set_new_record(record)
44
+ replace(record)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,11 @@
1
+ module DuckRecord::Associations
2
+ module ForeignAssociation # :nodoc:
3
+ def foreign_key_present?
4
+ if reflection.klass.primary_key
5
+ owner.attribute_present?(reflection.active_record_primary_key)
6
+ else
7
+ false
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ module DuckRecord
2
+ # = Active Record Has Many Association
3
+ module Associations
4
+ # This is the proxy that handles a has many association.
5
+ #
6
+ # If the association has a <tt>:through</tt> option further specialization
7
+ # is provided by its child HasManyThroughAssociation.
8
+ class HasManyAssociation < CollectionAssociation #:nodoc:
9
+ include ForeignAssociation
10
+
11
+ def insert_record(record, validate = true, raise = false)
12
+ set_owner_attributes(record)
13
+ super
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,39 @@
1
+ module DuckRecord
2
+ # = Active Record Has One Association
3
+ module Associations
4
+ class HasOneAssociation < SingularAssociation #:nodoc:
5
+ include ForeignAssociation
6
+
7
+ def replace(record)
8
+ if owner.class.readonly_attributes.include?(reflection.foreign_key.to_s)
9
+ return
10
+ end
11
+
12
+ raise_on_type_mismatch!(record) if record
13
+ load_target
14
+
15
+ return target unless target || record
16
+
17
+ self.target = record
18
+ end
19
+
20
+ private
21
+
22
+ def foreign_key_present?
23
+ true
24
+ end
25
+
26
+ # The reason that the save param for replace is false, if for create (not just build),
27
+ # is because the setting of the foreign keys is actually handled by the scoping when
28
+ # the record is instantiated, and so they are set straight away and do not need to be
29
+ # updated within replace.
30
+ def set_new_record(record)
31
+ replace(record)
32
+ end
33
+
34
+ def nullify_owner_attributes(record)
35
+ record[reflection.foreign_key] = nil
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,73 @@
1
+ module DuckRecord
2
+ module Associations
3
+ class SingularAssociation < Association #:nodoc:
4
+ # Implements the reader method, e.g. foo.bar for Foo.has_one :bar
5
+ def reader
6
+ if !loaded? || stale_target?
7
+ reload
8
+ end
9
+
10
+ target
11
+ end
12
+
13
+ # Implements the writer method, e.g. foo.bar= for Foo.belongs_to :bar
14
+ def writer(record)
15
+ replace(record)
16
+ end
17
+
18
+ def build(attributes = {})
19
+ record = build_record(attributes)
20
+ yield(record) if block_given?
21
+ set_new_record(record)
22
+ record
23
+ end
24
+
25
+ # Implements the reload reader method, e.g. foo.reload_bar for
26
+ # Foo.has_one :bar
27
+ def force_reload_reader
28
+ klass.uncached { reload }
29
+ target
30
+ end
31
+
32
+ private
33
+
34
+ def create_scope
35
+ scope.scope_for_create.stringify_keys.except(klass.primary_key)
36
+ end
37
+
38
+ def find_target
39
+ return scope.take if skip_statement_cache?
40
+
41
+ conn = klass.connection
42
+ sc = reflection.association_scope_cache(conn, owner) do
43
+ ActiveRecord::StatementCache.create(conn) { |params|
44
+ as = ActiveRecord::Associations::AssociationScope.create { params.bind }
45
+ target_scope.merge(as.scope(self, conn)).limit(1)
46
+ }
47
+ end
48
+
49
+ binds = ActiveRecord::Associations::AssociationScope.get_bind_values(owner, reflection.chain)
50
+ sc.execute(binds, klass, conn).first
51
+ rescue ::RangeError
52
+ nil
53
+ end
54
+
55
+ def replace(record)
56
+ raise NotImplementedError, "Subclasses must implement a replace(record) method"
57
+ end
58
+
59
+ def set_new_record(record)
60
+ replace(record)
61
+ end
62
+
63
+ def _create_record(attributes, raise_error = false)
64
+ record = build_record(attributes)
65
+ yield(record) if block_given?
66
+ saved = record.save
67
+ set_new_record(record)
68
+ raise ActiveRecord::RecordInvalid.new(record) if !saved && raise_error
69
+ record
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,213 @@
1
+ module DuckRecord
2
+ class Attribute # :nodoc:
3
+ class << self
4
+ def from_database(name, value, type)
5
+ FromDatabase.new(name, value, type)
6
+ end
7
+
8
+ def from_user(name, value, type, original_attribute = nil)
9
+ FromUser.new(name, value, type, original_attribute)
10
+ end
11
+
12
+ def with_cast_value(name, value, type)
13
+ WithCastValue.new(name, value, type)
14
+ end
15
+
16
+ def null(name)
17
+ Null.new(name)
18
+ end
19
+
20
+ def uninitialized(name, type)
21
+ Uninitialized.new(name, type)
22
+ end
23
+ end
24
+
25
+ attr_reader :name, :value_before_type_cast, :type
26
+
27
+ # This method should not be called directly.
28
+ # Use #from_database or #from_user
29
+ def initialize(name, value_before_type_cast, type, original_attribute = nil)
30
+ @name = name
31
+ @value_before_type_cast = value_before_type_cast
32
+ @type = type
33
+ @original_attribute = original_attribute
34
+ end
35
+
36
+ def value
37
+ # `defined?` is cheaper than `||=` when we get back falsy values
38
+ @value = type_cast(value_before_type_cast) unless defined?(@value)
39
+ @value
40
+ end
41
+
42
+ def original_value
43
+ if assigned?
44
+ original_attribute.original_value
45
+ else
46
+ type_cast(value_before_type_cast)
47
+ end
48
+ end
49
+
50
+ def value_for_database
51
+ type.serialize(value)
52
+ end
53
+
54
+ def changed?
55
+ changed_from_assignment? || changed_in_place?
56
+ end
57
+
58
+ def changed_in_place?
59
+ has_been_read? && type.changed_in_place?(original_value_for_database, value)
60
+ end
61
+
62
+ def forgetting_assignment
63
+ with_value_from_database(value_for_database)
64
+ end
65
+
66
+ def with_value_from_user(value)
67
+ type.assert_valid_value(value)
68
+ self.class.from_user(name, value, type, original_attribute || self)
69
+ end
70
+
71
+ def with_value_from_database(value)
72
+ self.class.from_database(name, value, type)
73
+ end
74
+
75
+ def with_cast_value(value)
76
+ self.class.with_cast_value(name, value, type)
77
+ end
78
+
79
+ def with_type(type)
80
+ self.class.new(name, value_before_type_cast, type, original_attribute)
81
+ end
82
+
83
+ def type_cast(*)
84
+ raise NotImplementedError
85
+ end
86
+
87
+ def initialized?
88
+ true
89
+ end
90
+
91
+ def came_from_user?
92
+ false
93
+ end
94
+
95
+ def has_been_read?
96
+ defined?(@value)
97
+ end
98
+
99
+ def ==(other)
100
+ self.class == other.class &&
101
+ name == other.name &&
102
+ value_before_type_cast == other.value_before_type_cast &&
103
+ type == other.type
104
+ end
105
+ alias eql? ==
106
+
107
+ def hash
108
+ [self.class, name, value_before_type_cast, type].hash
109
+ end
110
+
111
+ protected
112
+
113
+ attr_reader :original_attribute
114
+ alias_method :assigned?, :original_attribute
115
+
116
+ def initialize_dup(other)
117
+ if defined?(@value) && @value.duplicable?
118
+ @value = @value.dup
119
+ end
120
+ end
121
+
122
+ def changed_from_assignment?
123
+ assigned? && type.changed?(original_value, value, value_before_type_cast)
124
+ end
125
+
126
+ def original_value_for_database
127
+ if assigned?
128
+ original_attribute.original_value_for_database
129
+ else
130
+ _original_value_for_database
131
+ end
132
+ end
133
+
134
+ def _original_value_for_database
135
+ type.serialize(original_value)
136
+ end
137
+
138
+ class FromDatabase < Attribute # :nodoc:
139
+ def type_cast(value)
140
+ type.deserialize(value)
141
+ end
142
+
143
+ def _original_value_for_database
144
+ value_before_type_cast
145
+ end
146
+ end
147
+
148
+ class FromUser < Attribute # :nodoc:
149
+ def type_cast(value)
150
+ type.cast(value)
151
+ end
152
+
153
+ def came_from_user?
154
+ true
155
+ end
156
+ end
157
+
158
+ class WithCastValue < Attribute # :nodoc:
159
+ def type_cast(value)
160
+ value
161
+ end
162
+
163
+ def changed_in_place?
164
+ false
165
+ end
166
+ end
167
+
168
+ class Null < Attribute # :nodoc:
169
+ def initialize(name)
170
+ super(name, nil, Type::Value.new)
171
+ end
172
+
173
+ def type_cast(*)
174
+ nil
175
+ end
176
+
177
+ def with_type(type)
178
+ self.class.with_cast_value(name, nil, type)
179
+ end
180
+
181
+ def with_value_from_database(value)
182
+ raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
183
+ end
184
+ alias_method :with_value_from_user, :with_value_from_database
185
+ end
186
+
187
+ class Uninitialized < Attribute # :nodoc:
188
+ UNINITIALIZED_ORIGINAL_VALUE = Object.new
189
+
190
+ def initialize(name, type)
191
+ super(name, nil, type)
192
+ end
193
+
194
+ def value
195
+ if block_given?
196
+ yield name
197
+ end
198
+ end
199
+
200
+ def original_value
201
+ UNINITIALIZED_ORIGINAL_VALUE
202
+ end
203
+
204
+ def value_for_database
205
+ end
206
+
207
+ def initialized?
208
+ false
209
+ end
210
+ end
211
+ private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue
212
+ end
213
+ end
@@ -0,0 +1,30 @@
1
+ require "duck_record/attribute"
2
+
3
+ module DuckRecord
4
+ class Attribute # :nodoc:
5
+ class UserProvidedDefault < FromUser # :nodoc:
6
+ def initialize(name, value, type, default)
7
+ @user_provided_value = value
8
+ super(name, value, type, default)
9
+ end
10
+
11
+ def value_before_type_cast
12
+ if user_provided_value.is_a?(Proc)
13
+ @memoized_value_before_type_cast ||= user_provided_value.call
14
+ else
15
+ @user_provided_value
16
+ end
17
+ end
18
+
19
+ def with_type(type)
20
+ self.class.new(name, user_provided_value, type, original_attribute)
21
+ end
22
+
23
+ # TODO Change this to private once we've dropped Ruby 2.2 support.
24
+ # Workaround for Ruby 2.2 "private attribute?" warning.
25
+ protected
26
+
27
+ attr_reader :user_provided_value
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,118 @@
1
+ require "active_support/core_ext/hash/keys"
2
+ require "active_model/forbidden_attributes_protection"
3
+
4
+ module DuckRecord
5
+ module AttributeAssignment
6
+ extend ActiveSupport::Concern
7
+ include ActiveModel::ForbiddenAttributesProtection
8
+
9
+ # Alias for ActiveModel::AttributeAssignment#assign_attributes. See ActiveModel::AttributeAssignment.
10
+ def attributes=(attributes)
11
+ assign_attributes(attributes)
12
+ end
13
+
14
+ def assign_attributes(new_attributes)
15
+ unless new_attributes.respond_to?(:stringify_keys)
16
+ raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
17
+ end
18
+ return if new_attributes.nil? || new_attributes.empty?
19
+
20
+ attributes = new_attributes.stringify_keys
21
+ _assign_attributes(sanitize_for_mass_assignment(attributes))
22
+ end
23
+
24
+ private
25
+
26
+ def _assign_attributes(attributes)
27
+ multi_parameter_attributes = {}
28
+ nested_parameter_attributes = {}
29
+
30
+ attributes.each do |k, v|
31
+ if k.include?("(")
32
+ multi_parameter_attributes[k] = attributes.delete(k)
33
+ elsif v.is_a?(Hash)
34
+ nested_parameter_attributes[k] = attributes.delete(k)
35
+ end
36
+ end
37
+
38
+ attributes.each do |k, v|
39
+ _assign_attribute(k, v)
40
+ end
41
+
42
+ unless nested_parameter_attributes.empty?
43
+ assign_nested_parameter_attributes(nested_parameter_attributes)
44
+ end
45
+
46
+ unless multi_parameter_attributes.empty?
47
+ assign_multiparameter_attributes(multi_parameter_attributes)
48
+ end
49
+ end
50
+
51
+ # Assign any deferred nested attributes after the base attributes have been set.
52
+ def assign_nested_parameter_attributes(pairs)
53
+ pairs.each { |k, v| _assign_attribute(k, v) }
54
+ end
55
+
56
+ # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
57
+ # by calling new on the column type or aggregation type (through composed_of) object with these parameters.
58
+ # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
59
+ # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
60
+ # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Integer and
61
+ # f for Float. If all the values for a given attribute are empty, the attribute will be set to +nil+.
62
+ def assign_multiparameter_attributes(pairs)
63
+ execute_callstack_for_multiparameter_attributes(
64
+ extract_callstack_for_multiparameter_attributes(pairs)
65
+ )
66
+ end
67
+
68
+ def execute_callstack_for_multiparameter_attributes(callstack)
69
+ errors = []
70
+ callstack.each do |name, values_with_empty_parameters|
71
+ begin
72
+ if values_with_empty_parameters.each_value.all?(&:nil?)
73
+ values = nil
74
+ else
75
+ values = values_with_empty_parameters
76
+ end
77
+ send("#{name}=", values)
78
+ rescue => ex
79
+ errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name)
80
+ end
81
+ end
82
+ unless errors.empty?
83
+ error_descriptions = errors.map(&:message).join(",")
84
+ raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]"
85
+ end
86
+ end
87
+
88
+ def extract_callstack_for_multiparameter_attributes(pairs)
89
+ attributes = {}
90
+
91
+ pairs.each do |(multiparameter_name, value)|
92
+ attribute_name = multiparameter_name.split("(").first
93
+ attributes[attribute_name] ||= {}
94
+
95
+ parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
96
+ attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
97
+ end
98
+
99
+ attributes
100
+ end
101
+
102
+ def type_cast_attribute_value(multiparameter_name, value)
103
+ multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
104
+ end
105
+
106
+ def find_parameter_position(multiparameter_name)
107
+ multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
108
+ end
109
+
110
+ def _assign_attribute(k, v)
111
+ if respond_to?("#{k}=")
112
+ public_send("#{k}=", v)
113
+ else
114
+ raise UnknownAttributeError.new(self, k)
115
+ end
116
+ end
117
+ end
118
+ end