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,48 @@
1
+ en:
2
+ # Attributes names common to most models
3
+ #attributes:
4
+ #created_at: "Created at"
5
+ #updated_at: "Updated at"
6
+
7
+ # Default error messages
8
+ errors:
9
+ messages:
10
+ required: "must exist"
11
+ taken: "has already been taken"
12
+
13
+ # Active Record models configuration
14
+ activerecord:
15
+ errors:
16
+ messages:
17
+ record_invalid: "Validation failed: %{errors}"
18
+ restrict_dependent_destroy:
19
+ has_one: "Cannot delete record because a dependent %{record} exists"
20
+ has_many: "Cannot delete record because dependent %{record} exist"
21
+ # Append your own errors here or at the model/attributes scope.
22
+
23
+ # You can define own errors for models or model attributes.
24
+ # The values :model, :attribute and :value are always available for interpolation.
25
+ #
26
+ # For example,
27
+ # models:
28
+ # user:
29
+ # blank: "This is a custom blank message for %{model}: %{attribute}"
30
+ # attributes:
31
+ # login:
32
+ # blank: "This is a custom blank message for User login"
33
+ # Will define custom blank validation message for User model and
34
+ # custom blank validation message for login attribute of User model.
35
+ #models:
36
+
37
+ # Translate model names. Used in Model.human_name().
38
+ #models:
39
+ # For example,
40
+ # user: "Dude"
41
+ # will translate User model name to "Dude"
42
+
43
+ # Translate model attribute names. Used in Model.human_attribute_name(attribute).
44
+ #attributes:
45
+ # For example,
46
+ # user:
47
+ # login: "Handle"
48
+ # will translate User attribute "login" as "Handle"
@@ -0,0 +1,64 @@
1
+ module DuckRecord
2
+ module ModelSchema
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ delegate :type_for_attribute, to: :class
7
+ end
8
+
9
+ module ClassMethods
10
+
11
+ def attributes_builder # :nodoc:
12
+ @attributes_builder ||= AttributeSet::Builder.new(attribute_types)
13
+ end
14
+
15
+ def attribute_types # :nodoc:
16
+ load_schema
17
+ @attribute_types ||= Hash.new
18
+ end
19
+
20
+ def yaml_encoder # :nodoc:
21
+ @yaml_encoder ||= AttributeSet::YAMLEncoder.new(attribute_types)
22
+ end
23
+
24
+ # Returns the type of the attribute with the given name, after applying
25
+ # all modifiers. This method is the only valid source of information for
26
+ # anything related to the types of a model's attributes. This method will
27
+ # access the database and load the model's schema if it is required.
28
+ #
29
+ # The return value of this method will implement the interface described
30
+ # by ActiveModel::Type::Value (though the object itself may not subclass
31
+ # it).
32
+ #
33
+ # +attr_name+ The name of the attribute to retrieve the type for. Must be
34
+ # a string
35
+ def type_for_attribute(attr_name, &block)
36
+ if block
37
+ attribute_types.fetch(attr_name, &block)
38
+ else
39
+ attribute_types[attr_name]
40
+ end
41
+ end
42
+
43
+ def _default_attributes # :nodoc:
44
+ @default_attributes ||= AttributeSet.new({})
45
+ end
46
+
47
+ private
48
+
49
+ def schema_loaded?
50
+ defined?(@loaded) && @loaded
51
+ end
52
+
53
+ def load_schema
54
+ unless schema_loaded?
55
+ load_schema!
56
+ end
57
+ end
58
+
59
+ def load_schema!
60
+ @loaded = true
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,19 @@
1
+ module DuckRecord #:nodoc:
2
+ # = Active Record \Serialization
3
+ module Serialization
4
+ extend ActiveSupport::Concern
5
+ include ActiveModel::Serializers::JSON
6
+
7
+ included do
8
+ self.include_root_in_json = false
9
+ end
10
+
11
+ def serializable_hash(options = nil)
12
+ options = options.try(:dup) || {}
13
+
14
+ options[:except] = Array(options[:except]).map(&:to_s)
15
+
16
+ super(options)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ module DuckRecord
2
+ module Translation
3
+ include ActiveModel::Translation
4
+
5
+ # Set the lookup ancestors for ActiveModel.
6
+ def lookup_ancestors #:nodoc:
7
+ klass = self
8
+ classes = [klass]
9
+ return classes if klass == DuckRecord::Base
10
+
11
+ while klass != klass.base_class
12
+ classes << klass = klass.superclass
13
+ end
14
+ classes
15
+ end
16
+
17
+ # Set the i18n scope to overwrite ActiveModel.
18
+ def i18n_scope #:nodoc:
19
+ :activerecord
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,36 @@
1
+ module DuckRecord
2
+ module Type # :nodoc:
3
+ class Array < ActiveModel::Type::Value # :nodoc:
4
+ include ActiveModel::Type::Helpers::Mutable
5
+
6
+ attr_reader :subtype
7
+ delegate :type, :user_input_in_time_zone, :limit, to: :subtype
8
+
9
+ def initialize(subtype)
10
+ @subtype = subtype
11
+ end
12
+
13
+ def cast(value)
14
+ type_cast_array(value, :cast)
15
+ end
16
+
17
+ def ==(other)
18
+ other.is_a?(Array) && subtype == other.subtype
19
+ end
20
+
21
+ def map(value, &block)
22
+ value.map(&block)
23
+ end
24
+
25
+ private
26
+
27
+ def type_cast_array(value, method)
28
+ if value.is_a?(::Array)
29
+ value.map { |item| type_cast_array(item, method) }
30
+ else
31
+ @subtype.public_send(method, value)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,13 @@
1
+ module DuckRecord
2
+ module Type
3
+ class DecimalWithoutScale < ActiveModel::Type::BigInteger # :nodoc:
4
+ def type
5
+ :decimal
6
+ end
7
+
8
+ def type_cast_for_schema(value)
9
+ value.to_s.inspect
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,33 @@
1
+ module DuckRecord
2
+ module Type
3
+ module Internal # :nodoc:
4
+ class AbstractJson < ActiveModel::Type::Value # :nodoc:
5
+ include ActiveModel::Type::Helpers::Mutable
6
+
7
+ def type
8
+ :json
9
+ end
10
+
11
+ def deserialize(value)
12
+ if value.is_a?(::String)
13
+ ::ActiveSupport::JSON.decode(value) rescue nil
14
+ else
15
+ value
16
+ end
17
+ end
18
+
19
+ def serialize(value)
20
+ if value.nil?
21
+ nil
22
+ else
23
+ ::ActiveSupport::JSON.encode(value)
24
+ end
25
+ end
26
+
27
+ def accessor
28
+ DuckRecord::Store::StringKeyedHashAccessor
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,6 @@
1
+ module DuckRecord
2
+ module Type
3
+ class JSON < Internal::AbstractJson
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,97 @@
1
+ require 'active_model/type/registry'
2
+
3
+ module DuckRecord
4
+ # :stopdoc:
5
+ module Type
6
+ class Registry < ActiveModel::Type::Registry
7
+ def add_modifier(options, klass)
8
+ registrations << DecorationRegistration.new(options, klass)
9
+ end
10
+
11
+ private
12
+
13
+ def registration_klass
14
+ Registration
15
+ end
16
+
17
+ def find_registration(symbol, *args)
18
+ registrations
19
+ .select { |registration| registration.matches?(symbol, *args) }
20
+ .max
21
+ end
22
+ end
23
+
24
+ class Registration
25
+ def initialize(name, block, override: nil)
26
+ @name = name
27
+ @block = block
28
+ @override = override
29
+ end
30
+
31
+ def call(_registry, *args, **kwargs)
32
+ if kwargs.any? # https://bugs.ruby-lang.org/issues/10856
33
+ block.call(*args, **kwargs)
34
+ else
35
+ block.call(*args)
36
+ end
37
+ end
38
+
39
+ def matches?(type_name, *args, **kwargs)
40
+ type_name == name
41
+ end
42
+
43
+ def <=>(other)
44
+ priority <=> other.priority
45
+ end
46
+
47
+ # TODO Change this to private once we've dropped Ruby 2.2 support.
48
+ # Workaround for Ruby 2.2 "private attribute?" warning.
49
+ protected
50
+
51
+ attr_reader :name, :block, :adapter, :override
52
+
53
+ def priority
54
+ result = 0
55
+ if override
56
+ result |= 1
57
+ end
58
+ result
59
+ end
60
+ end
61
+
62
+ class DecorationRegistration < Registration
63
+ def initialize(options, klass)
64
+ @options = options
65
+ @klass = klass
66
+ end
67
+
68
+ def call(registry, *args, **kwargs)
69
+ subtype = registry.lookup(*args, **kwargs.except(*options.keys))
70
+ klass.new(subtype)
71
+ end
72
+
73
+ def matches?(*args, **kwargs)
74
+ matches_options?(**kwargs)
75
+ end
76
+
77
+ def priority
78
+ super | 4
79
+ end
80
+
81
+ # TODO Change this to private once we've dropped Ruby 2.2 support.
82
+ # Workaround for Ruby 2.2 "private attribute?" warning.
83
+ protected
84
+
85
+ attr_reader :options, :klass
86
+
87
+ private
88
+
89
+ def matches_options?(**kwargs)
90
+ options.all? do |key, value|
91
+ kwargs[key] == value
92
+ end
93
+ end
94
+ end
95
+ end
96
+ # :startdoc:
97
+ end
@@ -0,0 +1,63 @@
1
+ module DuckRecord
2
+ module Type
3
+ class Serialized < DelegateClass(ActiveModel::Type::Value) # :nodoc:
4
+ include ActiveModel::Type::Helpers::Mutable
5
+
6
+ attr_reader :subtype, :coder
7
+
8
+ def initialize(subtype, coder)
9
+ @subtype = subtype
10
+ @coder = coder
11
+ super(subtype)
12
+ end
13
+
14
+ def deserialize(value)
15
+ if default_value?(value)
16
+ value
17
+ else
18
+ coder.load(super)
19
+ end
20
+ end
21
+
22
+ def serialize(value)
23
+ return if value.nil?
24
+ unless default_value?(value)
25
+ super coder.dump(value)
26
+ end
27
+ end
28
+
29
+ def inspect
30
+ Kernel.instance_method(:inspect).bind(self).call
31
+ end
32
+
33
+ def changed_in_place?(raw_old_value, value)
34
+ return false if value.nil?
35
+ raw_new_value = encoded(value)
36
+ raw_old_value.nil? != raw_new_value.nil? ||
37
+ subtype.changed_in_place?(raw_old_value, raw_new_value)
38
+ end
39
+
40
+ def accessor
41
+ DuckRecord::Store::IndifferentHashAccessor
42
+ end
43
+
44
+ def assert_valid_value(value)
45
+ if coder.respond_to?(:assert_valid_value)
46
+ coder.assert_valid_value(value, action: "serialize")
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def default_value?(value)
53
+ value == coder.load(nil)
54
+ end
55
+
56
+ def encoded(value)
57
+ unless default_value?(value)
58
+ coder.dump(value)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,9 @@
1
+ module DuckRecord
2
+ module Type
3
+ class Text < ActiveModel::Type::String # :nodoc:
4
+ def type
5
+ :text
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ module DuckRecord
2
+ module Type
3
+ class UnsignedInteger < ActiveModel::Type::Integer # :nodoc:
4
+ private
5
+
6
+ def max_value
7
+ super * 2
8
+ end
9
+
10
+ def min_value
11
+ 0
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,66 @@
1
+ require 'active_model/type'
2
+
3
+ require 'duck_record/type/internal/abstract_json'
4
+ require 'duck_record/type/json'
5
+
6
+ require 'duck_record/type/array'
7
+
8
+ require 'duck_record/type/serialized'
9
+ require 'duck_record/type/registry'
10
+
11
+ module DuckRecord
12
+ module Type
13
+ @registry = Registry.new
14
+
15
+ class << self
16
+ attr_accessor :registry # :nodoc:
17
+ delegate :add_modifier, to: :registry
18
+
19
+ # Add a new type to the registry, allowing it to be referenced as a
20
+ # symbol by {ActiveRecord::Base.attribute}[rdoc-ref:Attributes::ClassMethods#attribute].
21
+ # If your type is only meant to be used with a specific database adapter, you can
22
+ # do so by passing <tt>adapter: :postgresql</tt>. If your type has the same
23
+ # name as a native type for the current adapter, an exception will be
24
+ # raised unless you specify an +:override+ option. <tt>override: true</tt> will
25
+ # cause your type to be used instead of the native type. <tt>override:
26
+ # false</tt> will cause the native type to be used over yours if one exists.
27
+ def register(type_name, klass = nil, **options, &block)
28
+ registry.register(type_name, klass, **options, &block)
29
+ end
30
+
31
+ def lookup(*args, **kwargs) # :nodoc:
32
+ registry.lookup(*args, **kwargs)
33
+ end
34
+ end
35
+
36
+ Helpers = ActiveModel::Type::Helpers
37
+ BigInteger = ActiveModel::Type::BigInteger
38
+ Binary = ActiveModel::Type::Binary
39
+ Boolean = ActiveModel::Type::Boolean
40
+ Decimal = ActiveModel::Type::Decimal
41
+ DecimalWithoutScale = ActiveModel::Type::DecimalWithoutScale
42
+ Float = ActiveModel::Type::Float
43
+ Integer = ActiveModel::Type::Integer
44
+ String = ActiveModel::Type::String
45
+ Text = ActiveModel::Type::Text
46
+ UnsignedInteger = ActiveModel::Type::UnsignedInteger
47
+ DateTime = ActiveModel::Type::DateTime
48
+ Time = ActiveModel::Type::Time
49
+ Date = ActiveModel::Type::Date
50
+
51
+ register(:big_integer, Type::BigInteger, override: false)
52
+ register(:binary, Type::Binary, override: false)
53
+ register(:boolean, Type::Boolean, override: false)
54
+ register(:date, Type::Date, override: false)
55
+ register(:datetime, Type::DateTime, override: false)
56
+ register(:decimal, Type::Decimal, override: false)
57
+ register(:float, Type::Float, override: false)
58
+ register(:integer, Type::Integer, override: false)
59
+ register(:string, Type::String, override: false)
60
+ register(:text, Type::Text, override: false)
61
+ register(:time, Type::Time, override: false)
62
+ register(:json, Type::JSON, override: false)
63
+
64
+ add_modifier({array: true}, Type::Array)
65
+ end
66
+ end