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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +59 -0
- data/Rakefile +29 -0
- data/lib/duck_record/attribute/user_provided_default.rb +30 -0
- data/lib/duck_record/attribute.rb +221 -0
- data/lib/duck_record/attribute_assignment.rb +91 -0
- data/lib/duck_record/attribute_methods/before_type_cast.rb +76 -0
- data/lib/duck_record/attribute_methods/dirty.rb +124 -0
- data/lib/duck_record/attribute_methods/read.rb +78 -0
- data/lib/duck_record/attribute_methods/write.rb +65 -0
- data/lib/duck_record/attribute_methods.rb +332 -0
- data/lib/duck_record/attribute_mutation_tracker.rb +113 -0
- data/lib/duck_record/attribute_set/builder.rb +124 -0
- data/lib/duck_record/attribute_set/yaml_encoder.rb +41 -0
- data/lib/duck_record/attribute_set.rb +99 -0
- data/lib/duck_record/attributes.rb +262 -0
- data/lib/duck_record/base.rb +296 -0
- data/lib/duck_record/callbacks.rb +324 -0
- data/lib/duck_record/core.rb +253 -0
- data/lib/duck_record/define_callbacks.rb +23 -0
- data/lib/duck_record/errors.rb +44 -0
- data/lib/duck_record/inheritance.rb +130 -0
- data/lib/duck_record/locale/en.yml +48 -0
- data/lib/duck_record/model_schema.rb +64 -0
- data/lib/duck_record/serialization.rb +19 -0
- data/lib/duck_record/translation.rb +22 -0
- data/lib/duck_record/type/array.rb +36 -0
- data/lib/duck_record/type/decimal_without_scale.rb +13 -0
- data/lib/duck_record/type/internal/abstract_json.rb +33 -0
- data/lib/duck_record/type/json.rb +6 -0
- data/lib/duck_record/type/registry.rb +97 -0
- data/lib/duck_record/type/serialized.rb +63 -0
- data/lib/duck_record/type/text.rb +9 -0
- data/lib/duck_record/type/unsigned_integer.rb +15 -0
- data/lib/duck_record/type.rb +66 -0
- data/lib/duck_record/validations.rb +40 -0
- data/lib/duck_record/version.rb +3 -0
- data/lib/duck_record.rb +47 -0
- data/lib/tasks/acts_as_record_tasks.rake +4 -0
- 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,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,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,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
|