duck_record 0.0.1

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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +59 -0
  4. data/Rakefile +29 -0
  5. data/lib/duck_record/attribute/user_provided_default.rb +30 -0
  6. data/lib/duck_record/attribute.rb +221 -0
  7. data/lib/duck_record/attribute_assignment.rb +91 -0
  8. data/lib/duck_record/attribute_methods/before_type_cast.rb +76 -0
  9. data/lib/duck_record/attribute_methods/dirty.rb +124 -0
  10. data/lib/duck_record/attribute_methods/read.rb +78 -0
  11. data/lib/duck_record/attribute_methods/write.rb +65 -0
  12. data/lib/duck_record/attribute_methods.rb +332 -0
  13. data/lib/duck_record/attribute_mutation_tracker.rb +113 -0
  14. data/lib/duck_record/attribute_set/builder.rb +124 -0
  15. data/lib/duck_record/attribute_set/yaml_encoder.rb +41 -0
  16. data/lib/duck_record/attribute_set.rb +99 -0
  17. data/lib/duck_record/attributes.rb +262 -0
  18. data/lib/duck_record/base.rb +296 -0
  19. data/lib/duck_record/callbacks.rb +324 -0
  20. data/lib/duck_record/core.rb +253 -0
  21. data/lib/duck_record/define_callbacks.rb +23 -0
  22. data/lib/duck_record/errors.rb +44 -0
  23. data/lib/duck_record/inheritance.rb +130 -0
  24. data/lib/duck_record/locale/en.yml +48 -0
  25. data/lib/duck_record/model_schema.rb +64 -0
  26. data/lib/duck_record/serialization.rb +19 -0
  27. data/lib/duck_record/translation.rb +22 -0
  28. data/lib/duck_record/type/array.rb +36 -0
  29. data/lib/duck_record/type/decimal_without_scale.rb +13 -0
  30. data/lib/duck_record/type/internal/abstract_json.rb +33 -0
  31. data/lib/duck_record/type/json.rb +6 -0
  32. data/lib/duck_record/type/registry.rb +97 -0
  33. data/lib/duck_record/type/serialized.rb +63 -0
  34. data/lib/duck_record/type/text.rb +9 -0
  35. data/lib/duck_record/type/unsigned_integer.rb +15 -0
  36. data/lib/duck_record/type.rb +66 -0
  37. data/lib/duck_record/validations.rb +40 -0
  38. data/lib/duck_record/version.rb +3 -0
  39. data/lib/duck_record.rb +47 -0
  40. data/lib/tasks/acts_as_record_tasks.rake +4 -0
  41. metadata +126 -0
@@ -0,0 +1,113 @@
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
+ @deprecated_forced_changes = Set.new
9
+ end
10
+
11
+ def changed_values
12
+ attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
13
+ if changed?(attr_name)
14
+ result[attr_name] = attributes[attr_name].original_value
15
+ end
16
+ end
17
+ end
18
+
19
+ def changes
20
+ attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
21
+ change = change_to_attribute(attr_name)
22
+ if change
23
+ result[attr_name] = change
24
+ end
25
+ end
26
+ end
27
+
28
+ def change_to_attribute(attr_name)
29
+ if changed?(attr_name)
30
+ [attributes[attr_name].original_value, attributes.fetch_value(attr_name)]
31
+ end
32
+ end
33
+
34
+ def any_changes?
35
+ attr_names.any? { |attr| changed?(attr) } || deprecated_forced_changes.any?
36
+ end
37
+
38
+ def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
39
+ attr_name = attr_name.to_s
40
+ forced_changes.include?(attr_name) ||
41
+ attributes[attr_name].changed? &&
42
+ (OPTION_NOT_GIVEN == from || attributes[attr_name].original_value == from) &&
43
+ (OPTION_NOT_GIVEN == to || attributes[attr_name].value == to)
44
+ end
45
+
46
+ def changed_in_place?(attr_name)
47
+ attributes[attr_name].changed_in_place?
48
+ end
49
+
50
+ def forget_change(attr_name)
51
+ attr_name = attr_name.to_s
52
+ attributes[attr_name] = attributes[attr_name].forgetting_assignment
53
+ forced_changes.delete(attr_name)
54
+ end
55
+
56
+ def original_value(attr_name)
57
+ attributes[attr_name].original_value
58
+ end
59
+
60
+ def force_change(attr_name)
61
+ forced_changes << attr_name.to_s
62
+ end
63
+
64
+ def deprecated_force_change(attr_name)
65
+ deprecated_forced_changes << attr_name.to_s
66
+ end
67
+
68
+ # TODO Change this to private once we've dropped Ruby 2.2 support.
69
+ # Workaround for Ruby 2.2 "private attribute?" warning.
70
+ protected
71
+
72
+ attr_reader :attributes, :forced_changes, :deprecated_forced_changes
73
+
74
+ private
75
+
76
+ def attr_names
77
+ attributes.keys
78
+ end
79
+ end
80
+
81
+ class NullMutationTracker # :nodoc:
82
+ include Singleton
83
+
84
+ def changed_values(*)
85
+ {}
86
+ end
87
+
88
+ def changes(*)
89
+ {}
90
+ end
91
+
92
+ def change_to_attribute(attr_name)
93
+ end
94
+
95
+ def any_changes?(*)
96
+ false
97
+ end
98
+
99
+ def changed?(*)
100
+ false
101
+ end
102
+
103
+ def changed_in_place?(*)
104
+ false
105
+ end
106
+
107
+ def forget_change(*)
108
+ end
109
+
110
+ def original_value(*)
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,124 @@
1
+ require 'duck_record/attribute'
2
+
3
+ module DuckRecord
4
+ class AttributeSet # :nodoc:
5
+ class Builder # :nodoc:
6
+ attr_reader :types, :always_initialized, :default
7
+
8
+ def initialize(types, always_initialized = nil, &default)
9
+ @types = types
10
+ @always_initialized = always_initialized
11
+ @default = default
12
+ end
13
+
14
+ def build_from_user(values = {}, additional_types = {})
15
+ if always_initialized && !values.key?(always_initialized)
16
+ values[always_initialized] = nil
17
+ end
18
+
19
+ attributes = LazyAttributeHash.new(types, values, additional_types, &default)
20
+ AttributeSet.new(attributes)
21
+ end
22
+ end
23
+ end
24
+
25
+ class LazyAttributeHash # :nodoc:
26
+ delegate :transform_values, :each_key, :each_value, :fetch, to: :materialize
27
+
28
+ def initialize(types, values, additional_types, &default)
29
+ @types = types
30
+ @values = values
31
+ @additional_types = additional_types
32
+ @materialized = false
33
+ @delegate_hash = {}
34
+ @default = default || proc {}
35
+ end
36
+
37
+ def key?(key)
38
+ delegate_hash.key?(key) || values.key?(key) || types.key?(key)
39
+ end
40
+
41
+ def [](key)
42
+ delegate_hash[key] || assign_default_value(key)
43
+ end
44
+
45
+ def []=(key, value)
46
+ if frozen?
47
+ raise RuntimeError, "Can't modify frozen hash"
48
+ end
49
+ delegate_hash[key] = value
50
+ end
51
+
52
+ def deep_dup
53
+ dup.tap do |copy|
54
+ copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup))
55
+ end
56
+ end
57
+
58
+ def initialize_dup(_)
59
+ @delegate_hash = Hash[delegate_hash]
60
+ super
61
+ end
62
+
63
+ def select
64
+ keys = types.keys | values.keys | delegate_hash.keys
65
+ keys.each_with_object({}) do |key, hash|
66
+ attribute = self[key]
67
+ if yield(key, attribute)
68
+ hash[key] = attribute
69
+ end
70
+ end
71
+ end
72
+
73
+ def ==(other)
74
+ if other.is_a?(LazyAttributeHash)
75
+ materialize == other.materialize
76
+ else
77
+ materialize == other
78
+ end
79
+ end
80
+
81
+ def marshal_dump
82
+ materialize
83
+ end
84
+
85
+ def marshal_load(delegate_hash)
86
+ @delegate_hash = delegate_hash
87
+ @types = {}
88
+ @values = {}
89
+ @additional_types = {}
90
+ @materialized = true
91
+ end
92
+
93
+ # TODO Change this to private once we've dropped Ruby 2.2 support.
94
+ # Workaround for Ruby 2.2 "private attribute?" warning.
95
+ protected
96
+
97
+ attr_reader :types, :values, :additional_types, :delegate_hash, :default
98
+
99
+ def materialize
100
+ unless @materialized
101
+ values.each_key { |key| self[key] }
102
+ types.each_key { |key| self[key] }
103
+ unless frozen?
104
+ @materialized = true
105
+ end
106
+ end
107
+ delegate_hash
108
+ end
109
+
110
+ private
111
+
112
+ def assign_default_value(name)
113
+ type = additional_types.fetch(name, types[name])
114
+ value_present = true
115
+ value = values.fetch(name) { value_present = false }
116
+
117
+ if value_present
118
+ delegate_hash[name] = Attribute.from_user(name, value, type)
119
+ elsif types.key?(name)
120
+ delegate_hash[name] = default.call(name) || Attribute.uninitialized(name, type)
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,41 @@
1
+ module DuckRecord
2
+ class AttributeSet
3
+ # Attempts to do more intelligent YAML dumping of an
4
+ # DuckRecord::AttributeSet to reduce the size of the resulting string
5
+ class YAMLEncoder # :nodoc:
6
+ def initialize(default_types)
7
+ @default_types = default_types
8
+ end
9
+
10
+ def encode(attribute_set, coder)
11
+ coder['concise_attributes'] = attribute_set.each_value.map do |attr|
12
+ if attr.type.equal?(default_types[attr.name])
13
+ attr.with_type(nil)
14
+ else
15
+ attr
16
+ end
17
+ end
18
+ end
19
+
20
+ def decode(coder)
21
+ if coder['attributes']
22
+ coder['attributes']
23
+ else
24
+ attributes_hash = Hash[coder['concise_attributes'].map do |attr|
25
+ if attr.type.nil?
26
+ attr = attr.with_type(default_types[attr.name])
27
+ end
28
+ [attr.name, attr]
29
+ end]
30
+ AttributeSet.new(attributes_hash)
31
+ end
32
+ end
33
+
34
+ # TODO Change this to private once we've dropped Ruby 2.2 support.
35
+ # Workaround for Ruby 2.2 'private attribute?' warning.
36
+ protected
37
+
38
+ attr_reader :default_types
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,99 @@
1
+ require 'duck_record/attribute_set/builder'
2
+ require 'duck_record/attribute_set/yaml_encoder'
3
+
4
+ module DuckRecord
5
+ class AttributeSet # :nodoc:
6
+ delegate :each_value, :fetch, to: :attributes
7
+
8
+ def initialize(attributes)
9
+ @attributes = attributes
10
+ end
11
+
12
+ def [](name)
13
+ attributes[name] || Attribute.null(name)
14
+ end
15
+
16
+ def []=(name, value)
17
+ attributes[name] = value
18
+ end
19
+
20
+ def values_before_type_cast
21
+ attributes.transform_values(&:value_before_type_cast)
22
+ end
23
+
24
+ def to_hash
25
+ initialized_attributes.transform_values(&:value)
26
+ end
27
+ alias_method :to_h, :to_hash
28
+
29
+ def key?(name)
30
+ attributes.key?(name) && self[name].initialized?
31
+ end
32
+
33
+ def keys
34
+ attributes.each_key.select { |name| self[name].initialized? }
35
+ end
36
+
37
+ if defined?(JRUBY_VERSION)
38
+ # This form is significantly faster on JRuby, and this is one of our biggest hotspots.
39
+ # https://github.com/jruby/jruby/pull/2562
40
+ def fetch_value(name, &block)
41
+ self[name].value(&block)
42
+ end
43
+ else
44
+ def fetch_value(name)
45
+ self[name].value { |n| yield n if block_given? }
46
+ end
47
+ end
48
+
49
+ def write_from_user(name, value)
50
+ attributes[name] = self[name].with_value_from_user(value)
51
+ end
52
+
53
+ def write_cast_value(name, value)
54
+ attributes[name] = self[name].with_cast_value(value)
55
+ end
56
+
57
+ def freeze
58
+ @attributes.freeze
59
+ super
60
+ end
61
+
62
+ def deep_dup
63
+ dup.tap do |copy|
64
+ copy.instance_variable_set(:@attributes, attributes.deep_dup)
65
+ end
66
+ end
67
+
68
+ def initialize_dup(_)
69
+ @attributes = attributes.dup
70
+ super
71
+ end
72
+
73
+ def initialize_clone(_)
74
+ @attributes = attributes.clone
75
+ super
76
+ end
77
+
78
+ def map(&block)
79
+ new_attributes = attributes.transform_values(&block)
80
+ AttributeSet.new(new_attributes)
81
+ end
82
+
83
+ def ==(other)
84
+ attributes == other.attributes
85
+ end
86
+
87
+ # TODO Change this to private once we've dropped Ruby 2.2 support.
88
+ # Workaround for Ruby 2.2 "private attribute?" warning.
89
+ protected
90
+
91
+ attr_reader :attributes
92
+
93
+ private
94
+
95
+ def initialized_attributes
96
+ attributes.select { |_, attr| attr.initialized? }
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,262 @@
1
+ require "duck_record/attribute/user_provided_default"
2
+
3
+ module DuckRecord
4
+ # See DuckRecord::Attributes::ClassMethods for documentation
5
+ module Attributes
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :attributes_to_define, instance_accessor: false # :internal:
10
+ self.attributes_to_define = {}
11
+ end
12
+
13
+ module ClassMethods
14
+ # Defines an attribute with a type on this model. It will override the
15
+ # type of existing attributes if needed. This allows control over how
16
+ # values are converted to and from SQL when assigned to a model. It also
17
+ # changes the behavior of values passed to
18
+ # {DuckRecord::Base.where}[rdoc-ref:QueryMethods#where]. This will let you use
19
+ # your domain objects across much of Active Record, without having to
20
+ # rely on implementation details or monkey patching.
21
+ #
22
+ # +name+ The name of the methods to define attribute methods for, and the
23
+ # column which this will persist to.
24
+ #
25
+ # +cast_type+ A symbol such as +:string+ or +:integer+, or a type object
26
+ # to be used for this attribute. See the examples below for more
27
+ # information about providing custom type objects.
28
+ #
29
+ # ==== Options
30
+ #
31
+ # The following options are accepted:
32
+ #
33
+ # +default+ The default value to use when no value is provided. If this option
34
+ # is not passed, the previous default value (if any) will be used.
35
+ # Otherwise, the default will be +nil+.
36
+ #
37
+ # +array+ (PostgreSQL only) specifies that the type should be an array (see the
38
+ # examples below).
39
+ #
40
+ # +range+ (PostgreSQL only) specifies that the type should be a range (see the
41
+ # examples below).
42
+ #
43
+ # ==== Examples
44
+ #
45
+ # The type detected by Active Record can be overridden.
46
+ #
47
+ # # db/schema.rb
48
+ # create_table :store_listings, force: true do |t|
49
+ # t.decimal :price_in_cents
50
+ # end
51
+ #
52
+ # # app/models/store_listing.rb
53
+ # class StoreListing < DuckRecord::Base
54
+ # end
55
+ #
56
+ # store_listing = StoreListing.new(price_in_cents: '10.1')
57
+ #
58
+ # # before
59
+ # store_listing.price_in_cents # => BigDecimal.new(10.1)
60
+ #
61
+ # class StoreListing < DuckRecord::Base
62
+ # attribute :price_in_cents, :integer
63
+ # end
64
+ #
65
+ # # after
66
+ # store_listing.price_in_cents # => 10
67
+ #
68
+ # A default can also be provided.
69
+ #
70
+ # # db/schema.rb
71
+ # create_table :store_listings, force: true do |t|
72
+ # t.string :my_string, default: "original default"
73
+ # end
74
+ #
75
+ # StoreListing.new.my_string # => "original default"
76
+ #
77
+ # # app/models/store_listing.rb
78
+ # class StoreListing < DuckRecord::Base
79
+ # attribute :my_string, :string, default: "new default"
80
+ # end
81
+ #
82
+ # StoreListing.new.my_string # => "new default"
83
+ #
84
+ # class Product < DuckRecord::Base
85
+ # attribute :my_default_proc, :datetime, default: -> { Time.now }
86
+ # end
87
+ #
88
+ # Product.new.my_default_proc # => 2015-05-30 11:04:48 -0600
89
+ # sleep 1
90
+ # Product.new.my_default_proc # => 2015-05-30 11:04:49 -0600
91
+ #
92
+ # \Attributes do not need to be backed by a database column.
93
+ #
94
+ # # app/models/my_model.rb
95
+ # class MyModel < DuckRecord::Base
96
+ # attribute :my_string, :string
97
+ # attribute :my_int_array, :integer, array: true
98
+ # attribute :my_float_range, :float, range: true
99
+ # end
100
+ #
101
+ # model = MyModel.new(
102
+ # my_string: "string",
103
+ # my_int_array: ["1", "2", "3"],
104
+ # my_float_range: "[1,3.5]",
105
+ # )
106
+ # model.attributes
107
+ # # =>
108
+ # {
109
+ # my_string: "string",
110
+ # my_int_array: [1, 2, 3],
111
+ # my_float_range: 1.0..3.5
112
+ # }
113
+ #
114
+ # ==== Creating Custom Types
115
+ #
116
+ # Users may also define their own custom types, as long as they respond
117
+ # to the methods defined on the value type. The method +deserialize+ or
118
+ # +cast+ will be called on your type object, with raw input from the
119
+ # database or from your controllers. See ActiveModel::Type::Value for the
120
+ # expected API. It is recommended that your type objects inherit from an
121
+ # existing type, or from DuckRecord::Type::Value
122
+ #
123
+ # class MoneyType < DuckRecord::Type::Integer
124
+ # def cast(value)
125
+ # if !value.kind_of?(Numeric) && value.include?('$')
126
+ # price_in_dollars = value.gsub(/\$/, '').to_f
127
+ # super(price_in_dollars * 100)
128
+ # else
129
+ # super
130
+ # end
131
+ # end
132
+ # end
133
+ #
134
+ # # config/initializers/types.rb
135
+ # DuckRecord::Type.register(:money, MoneyType)
136
+ #
137
+ # # app/models/store_listing.rb
138
+ # class StoreListing < DuckRecord::Base
139
+ # attribute :price_in_cents, :money
140
+ # end
141
+ #
142
+ # store_listing = StoreListing.new(price_in_cents: '$10.00')
143
+ # store_listing.price_in_cents # => 1000
144
+ #
145
+ # For more details on creating custom types, see the documentation for
146
+ # ActiveModel::Type::Value. For more details on registering your types
147
+ # to be referenced by a symbol, see DuckRecord::Type.register. You can
148
+ # also pass a type object directly, in place of a symbol.
149
+ #
150
+ # ==== \Querying
151
+ #
152
+ # When {DuckRecord::Base.where}[rdoc-ref:QueryMethods#where] is called, it will
153
+ # use the type defined by the model class to convert the value to SQL,
154
+ # calling +serialize+ on your type object. For example:
155
+ #
156
+ # class Money < Struct.new(:amount, :currency)
157
+ # end
158
+ #
159
+ # class MoneyType < Type::Value
160
+ # def initialize(currency_converter:)
161
+ # @currency_converter = currency_converter
162
+ # end
163
+ #
164
+ # # value will be the result of +deserialize+ or
165
+ # # +cast+. Assumed to be an instance of +Money+ in
166
+ # # this case.
167
+ # def serialize(value)
168
+ # value_in_bitcoins = @currency_converter.convert_to_bitcoins(value)
169
+ # value_in_bitcoins.amount
170
+ # end
171
+ # end
172
+ #
173
+ # # config/initializers/types.rb
174
+ # DuckRecord::Type.register(:money, MoneyType)
175
+ #
176
+ # # app/models/product.rb
177
+ # class Product < DuckRecord::Base
178
+ # currency_converter = ConversionRatesFromTheInternet.new
179
+ # attribute :price_in_bitcoins, :money, currency_converter: currency_converter
180
+ # end
181
+ #
182
+ # Product.where(price_in_bitcoins: Money.new(5, "USD"))
183
+ # # => SELECT * FROM products WHERE price_in_bitcoins = 0.02230
184
+ #
185
+ # Product.where(price_in_bitcoins: Money.new(5, "GBP"))
186
+ # # => SELECT * FROM products WHERE price_in_bitcoins = 0.03412
187
+ #
188
+ # ==== Dirty Tracking
189
+ #
190
+ # The type of an attribute is given the opportunity to change how dirty
191
+ # tracking is performed. The methods +changed?+ and +changed_in_place?+
192
+ # will be called from ActiveModel::Dirty. See the documentation for those
193
+ # methods in ActiveModel::Type::Value for more details.
194
+ def attribute(name, cast_type = Type::Value.new, **options)
195
+ name = name.to_s
196
+
197
+ self.attributes_to_define =
198
+ attributes_to_define.merge(
199
+ name => [cast_type, options]
200
+ )
201
+ end
202
+
203
+ # This is the low level API which sits beneath +attribute+. It only
204
+ # accepts type objects, and will do its work immediately instead of
205
+ # waiting for the schema to load. Automatic schema detection and
206
+ # ClassMethods#attribute both call this under the hood. While this method
207
+ # is provided so it can be used by plugin authors, application code
208
+ # should probably use ClassMethods#attribute.
209
+ #
210
+ # +name+ The name of the attribute being defined. Expected to be a +String+.
211
+ #
212
+ # +cast_type+ The type object to use for this attribute.
213
+ #
214
+ # +default+ The default value to use when no value is provided. If this option
215
+ # is not passed, the previous default value (if any) will be used.
216
+ # Otherwise, the default will be +nil+. A proc can also be passed, and
217
+ # will be called once each time a new value is needed.
218
+ #
219
+ # +user_provided_default+ Whether the default value should be cast using
220
+ # +cast+ or +deserialize+.
221
+ def define_attribute(
222
+ name,
223
+ cast_type,
224
+ default: NO_DEFAULT_PROVIDED,
225
+ user_provided_default: true
226
+ )
227
+ attribute_types[name] = cast_type
228
+ define_default_attribute(name, default, cast_type)
229
+ end
230
+
231
+ def load_schema! # :nodoc:
232
+ super
233
+ attributes_to_define.each do |name, (type, options)|
234
+ if type.is_a?(Symbol)
235
+ type = DuckRecord::Type.lookup(type, **options.except(:default))
236
+ end
237
+
238
+ define_attribute(name, type, **options.slice(:default))
239
+ end
240
+ end
241
+
242
+ private
243
+
244
+ NO_DEFAULT_PROVIDED = Object.new # :nodoc:
245
+ private_constant :NO_DEFAULT_PROVIDED
246
+
247
+ def define_default_attribute(name, value, type)
248
+ if value == NO_DEFAULT_PROVIDED
249
+ default_attribute = _default_attributes[name].with_type(type)
250
+ else
251
+ default_attribute = Attribute::UserProvidedDefault.new(
252
+ name,
253
+ value,
254
+ type,
255
+ _default_attributes.fetch(name.to_s) { nil },
256
+ )
257
+ end
258
+ _default_attributes[name] = default_attribute
259
+ end
260
+ end
261
+ end
262
+ end