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