duck_record 0.0.1

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