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