tallty_duck_record 1.0.0
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 +41 -0
- data/README.md +82 -0
- data/Rakefile +28 -0
- data/lib/core_ext/array_without_blank.rb +46 -0
- data/lib/duck_record.rb +65 -0
- data/lib/duck_record/associations.rb +130 -0
- data/lib/duck_record/associations/association.rb +271 -0
- data/lib/duck_record/associations/belongs_to_association.rb +71 -0
- data/lib/duck_record/associations/builder/association.rb +127 -0
- data/lib/duck_record/associations/builder/belongs_to.rb +44 -0
- data/lib/duck_record/associations/builder/collection_association.rb +45 -0
- data/lib/duck_record/associations/builder/embeds_many.rb +9 -0
- data/lib/duck_record/associations/builder/embeds_one.rb +9 -0
- data/lib/duck_record/associations/builder/has_many.rb +11 -0
- data/lib/duck_record/associations/builder/has_one.rb +20 -0
- data/lib/duck_record/associations/builder/singular_association.rb +33 -0
- data/lib/duck_record/associations/collection_association.rb +476 -0
- data/lib/duck_record/associations/collection_proxy.rb +1160 -0
- data/lib/duck_record/associations/embeds_association.rb +92 -0
- data/lib/duck_record/associations/embeds_many_association.rb +203 -0
- data/lib/duck_record/associations/embeds_many_proxy.rb +892 -0
- data/lib/duck_record/associations/embeds_one_association.rb +48 -0
- data/lib/duck_record/associations/foreign_association.rb +11 -0
- data/lib/duck_record/associations/has_many_association.rb +17 -0
- data/lib/duck_record/associations/has_one_association.rb +39 -0
- data/lib/duck_record/associations/singular_association.rb +73 -0
- data/lib/duck_record/attribute.rb +213 -0
- data/lib/duck_record/attribute/user_provided_default.rb +30 -0
- data/lib/duck_record/attribute_assignment.rb +118 -0
- data/lib/duck_record/attribute_decorators.rb +89 -0
- data/lib/duck_record/attribute_methods.rb +325 -0
- data/lib/duck_record/attribute_methods/before_type_cast.rb +76 -0
- data/lib/duck_record/attribute_methods/dirty.rb +107 -0
- data/lib/duck_record/attribute_methods/read.rb +78 -0
- data/lib/duck_record/attribute_methods/serialization.rb +66 -0
- data/lib/duck_record/attribute_methods/write.rb +70 -0
- data/lib/duck_record/attribute_mutation_tracker.rb +108 -0
- data/lib/duck_record/attribute_set.rb +98 -0
- data/lib/duck_record/attribute_set/yaml_encoder.rb +41 -0
- data/lib/duck_record/attributes.rb +262 -0
- data/lib/duck_record/base.rb +300 -0
- data/lib/duck_record/callbacks.rb +324 -0
- data/lib/duck_record/coders/json.rb +13 -0
- data/lib/duck_record/coders/yaml_column.rb +48 -0
- data/lib/duck_record/core.rb +262 -0
- data/lib/duck_record/define_callbacks.rb +23 -0
- data/lib/duck_record/enum.rb +139 -0
- data/lib/duck_record/errors.rb +71 -0
- data/lib/duck_record/inheritance.rb +130 -0
- data/lib/duck_record/locale/en.yml +46 -0
- data/lib/duck_record/model_schema.rb +71 -0
- data/lib/duck_record/nested_attributes.rb +555 -0
- data/lib/duck_record/nested_validate_association.rb +262 -0
- data/lib/duck_record/persistence.rb +39 -0
- data/lib/duck_record/readonly_attributes.rb +36 -0
- data/lib/duck_record/reflection.rb +650 -0
- data/lib/duck_record/serialization.rb +26 -0
- data/lib/duck_record/translation.rb +22 -0
- data/lib/duck_record/type.rb +77 -0
- data/lib/duck_record/type/array.rb +36 -0
- data/lib/duck_record/type/array_without_blank.rb +36 -0
- data/lib/duck_record/type/date.rb +7 -0
- data/lib/duck_record/type/date_time.rb +7 -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/internal/timezone.rb +15 -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/time.rb +19 -0
- data/lib/duck_record/type/unsigned_integer.rb +15 -0
- data/lib/duck_record/validations.rb +67 -0
- data/lib/duck_record/validations/subset.rb +74 -0
- data/lib/duck_record/validations/uniqueness_on_real_record.rb +248 -0
- data/lib/duck_record/version.rb +3 -0
- data/lib/tasks/acts_as_record_tasks.rake +4 -0
- metadata +181 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
module DuckRecord
|
2
|
+
# This module exists because `DuckRecord::AttributeMethods::Dirty` needs to
|
3
|
+
# define callbacks, but continue to have its version of `save` be the super
|
4
|
+
# method of `DuckRecord::Callbacks`. This will be removed when the removal
|
5
|
+
# of deprecated code removes this need.
|
6
|
+
module DefineCallbacks
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
CALLBACKS = [
|
10
|
+
:after_initialize, :before_validation, :after_validation,
|
11
|
+
]
|
12
|
+
|
13
|
+
module ClassMethods # :nodoc:
|
14
|
+
include ActiveModel::Callbacks
|
15
|
+
end
|
16
|
+
|
17
|
+
included do
|
18
|
+
include ActiveModel::Validations::Callbacks
|
19
|
+
|
20
|
+
define_model_callbacks :initialize, only: :after
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/object/deep_dup"
|
4
|
+
|
5
|
+
module DuckRecord
|
6
|
+
module Enum
|
7
|
+
def self.extended(base) # :nodoc:
|
8
|
+
base.class_attribute(:defined_enums, instance_writer: false)
|
9
|
+
base.defined_enums = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def inherited(base) # :nodoc:
|
13
|
+
base.defined_enums = defined_enums.deep_dup
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
class EnumType < ActiveModel::Type::Value # :nodoc:
|
18
|
+
delegate :type, to: :subtype
|
19
|
+
|
20
|
+
def initialize(name, mapping, subtype)
|
21
|
+
@name = name
|
22
|
+
@mapping = mapping
|
23
|
+
@subtype = subtype
|
24
|
+
end
|
25
|
+
|
26
|
+
def cast(value)
|
27
|
+
return if value.blank?
|
28
|
+
|
29
|
+
if mapping.has_key?(value)
|
30
|
+
value.to_s
|
31
|
+
elsif mapping.has_value?(value)
|
32
|
+
mapping.key(value)
|
33
|
+
else
|
34
|
+
assert_valid_value(value)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def deserialize(value)
|
39
|
+
return if value.nil?
|
40
|
+
mapping.key(subtype.deserialize(value))
|
41
|
+
end
|
42
|
+
|
43
|
+
def serialize(value)
|
44
|
+
mapping.fetch(value, value)
|
45
|
+
end
|
46
|
+
|
47
|
+
def assert_valid_value(value)
|
48
|
+
unless value.blank? || mapping.has_key?(value) || mapping.has_value?(value)
|
49
|
+
raise ArgumentError, "'#{value}' is not a valid #{name}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
attr_reader :name, :mapping, :subtype
|
56
|
+
end
|
57
|
+
|
58
|
+
def enum(definitions)
|
59
|
+
klass = self
|
60
|
+
enum_prefix = definitions.delete(:_prefix)
|
61
|
+
enum_suffix = definitions.delete(:_suffix)
|
62
|
+
definitions.each do |name, values|
|
63
|
+
# statuses = { }
|
64
|
+
enum_values = ActiveSupport::HashWithIndifferentAccess.new
|
65
|
+
name = name.to_sym
|
66
|
+
|
67
|
+
# def self.statuses() statuses end
|
68
|
+
detect_enum_conflict!(name, name.to_s.pluralize, true)
|
69
|
+
klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values }
|
70
|
+
|
71
|
+
detect_enum_conflict!(name, name)
|
72
|
+
detect_enum_conflict!(name, "#{name}=")
|
73
|
+
|
74
|
+
attr = attribute_alias?(name) ? attribute_alias(name) : name
|
75
|
+
decorate_attribute_type(attr, :enum) do |subtype|
|
76
|
+
EnumType.new(attr, enum_values, subtype)
|
77
|
+
end
|
78
|
+
|
79
|
+
_enum_methods_module.module_eval do
|
80
|
+
pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
|
81
|
+
pairs.each do |value, i|
|
82
|
+
if enum_prefix == true
|
83
|
+
prefix = "#{name}_"
|
84
|
+
elsif enum_prefix
|
85
|
+
prefix = "#{enum_prefix}_"
|
86
|
+
end
|
87
|
+
if enum_suffix == true
|
88
|
+
suffix = "_#{name}"
|
89
|
+
elsif enum_suffix
|
90
|
+
suffix = "_#{enum_suffix}"
|
91
|
+
end
|
92
|
+
|
93
|
+
value_method_name = "#{prefix}#{value}#{suffix}"
|
94
|
+
enum_values[value] = i
|
95
|
+
|
96
|
+
# def active?() status == 0 end
|
97
|
+
klass.send(:detect_enum_conflict!, name, "#{value_method_name}?")
|
98
|
+
define_method("#{value_method_name}?") { self[attr] == value.to_s }
|
99
|
+
end
|
100
|
+
end
|
101
|
+
defined_enums[name.to_s] = enum_values
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
def _enum_methods_module
|
107
|
+
@_enum_methods_module ||= begin
|
108
|
+
mod = Module.new
|
109
|
+
include mod
|
110
|
+
mod
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
ENUM_CONFLICT_MESSAGE = \
|
115
|
+
"You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \
|
116
|
+
"this will generate a %{type} method \"%{method}\", which is already defined " \
|
117
|
+
"by %{source}."
|
118
|
+
|
119
|
+
def detect_enum_conflict!(enum_name, method_name, klass_method = false)
|
120
|
+
if klass_method && dangerous_class_method?(method_name)
|
121
|
+
raise_conflict_error(enum_name, method_name, type: "class")
|
122
|
+
elsif !klass_method && dangerous_attribute_method?(method_name)
|
123
|
+
raise_conflict_error(enum_name, method_name)
|
124
|
+
elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
|
125
|
+
raise_conflict_error(enum_name, method_name, source: "another enum")
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def raise_conflict_error(enum_name, method_name, type: "instance", source: "Active Record")
|
130
|
+
raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
|
131
|
+
enum: enum_name,
|
132
|
+
klass: name,
|
133
|
+
type: type,
|
134
|
+
method: method_name,
|
135
|
+
source: source
|
136
|
+
}
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module DuckRecord
|
2
|
+
# = Active Record Errors
|
3
|
+
#
|
4
|
+
# Generic Active Record exception class.
|
5
|
+
class DuckRecordError < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
# Raised on attempt to update record that is instantiated as read only.
|
9
|
+
class ReadOnlyRecord < DuckRecordError
|
10
|
+
end
|
11
|
+
|
12
|
+
# Raised when attribute has a name reserved by Active Record (when attribute
|
13
|
+
# has name of one of Active Record instance methods).
|
14
|
+
class DangerousAttributeError < DuckRecordError
|
15
|
+
end
|
16
|
+
|
17
|
+
# Raised when association is being configured improperly or user tries to use
|
18
|
+
# offset and limit together with
|
19
|
+
# {ActiveRecord::Base.has_many}[rdoc-ref:Associations::ClassMethods#has_many] or
|
20
|
+
# {ActiveRecord::Base.has_and_belongs_to_many}[rdoc-ref:Associations::ClassMethods#has_and_belongs_to_many]
|
21
|
+
# associations.
|
22
|
+
class ConfigurationError < DuckRecordError
|
23
|
+
end
|
24
|
+
|
25
|
+
# Raised when an object assigned to an association has an incorrect type.
|
26
|
+
#
|
27
|
+
# class Ticket < ActiveRecord::Base
|
28
|
+
# has_many :patches
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# class Patch < ActiveRecord::Base
|
32
|
+
# belongs_to :ticket
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# # Comments are not patches, this assignment raises AssociationTypeMismatch.
|
36
|
+
# @ticket.patches << Comment.new(content: "Please attach tests to your patch.")
|
37
|
+
class AssociationTypeMismatch < DuckRecordError
|
38
|
+
end
|
39
|
+
|
40
|
+
# Raised when unknown attributes are supplied via mass assignment.
|
41
|
+
UnknownAttributeError = ActiveModel::UnknownAttributeError
|
42
|
+
|
43
|
+
# Raised when an error occurred while doing a mass assignment to an attribute through the
|
44
|
+
# {DuckRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method.
|
45
|
+
# The exception has an +attribute+ property that is the name of the offending attribute.
|
46
|
+
class AttributeAssignmentError < DuckRecordError
|
47
|
+
attr_reader :exception, :attribute
|
48
|
+
|
49
|
+
def initialize(message = nil, exception = nil, attribute = nil)
|
50
|
+
super(message)
|
51
|
+
@exception = exception
|
52
|
+
@attribute = attribute
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Raised when unserialized object's type mismatches one specified for serializable field.
|
57
|
+
class SerializationTypeMismatch < DuckRecordError
|
58
|
+
end
|
59
|
+
|
60
|
+
# Raised when there are multiple errors while doing a mass assignment through the
|
61
|
+
# {DuckRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=]
|
62
|
+
# method. The exception has an +errors+ property that contains an array of AttributeAssignmentError
|
63
|
+
# objects, each corresponding to the error while assigning to an attribute.
|
64
|
+
class MultiparameterAssignmentErrors < DuckRecordError
|
65
|
+
attr_reader :errors
|
66
|
+
|
67
|
+
def initialize(errors = nil)
|
68
|
+
@errors = errors
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require "active_support/core_ext/hash/indifferent_access"
|
2
|
+
|
3
|
+
module DuckRecord
|
4
|
+
# == Single table inheritance
|
5
|
+
#
|
6
|
+
# Active Record allows inheritance by storing the name of the class in a column that by
|
7
|
+
# default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>).
|
8
|
+
# This means that an inheritance looking like this:
|
9
|
+
#
|
10
|
+
# class Company < DuckRecord::Base; end
|
11
|
+
# class Firm < Company; end
|
12
|
+
# class Client < Company; end
|
13
|
+
# class PriorityClient < Client; end
|
14
|
+
#
|
15
|
+
# When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in
|
16
|
+
# the companies table with type = "Firm". You can then fetch this row again using
|
17
|
+
# <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object.
|
18
|
+
#
|
19
|
+
# Be aware that because the type column is an attribute on the record every new
|
20
|
+
# subclass will instantly be marked as dirty and the type column will be included
|
21
|
+
# in the list of changed attributes on the record. This is different from non
|
22
|
+
# Single Table Inheritance(STI) classes:
|
23
|
+
#
|
24
|
+
# Company.new.changed? # => false
|
25
|
+
# Firm.new.changed? # => true
|
26
|
+
# Firm.new.changes # => {"type"=>["","Firm"]}
|
27
|
+
#
|
28
|
+
# If you don't have a type column defined in your table, single-table inheritance won't
|
29
|
+
# be triggered. In that case, it'll work just like normal subclasses with no special magic
|
30
|
+
# for differentiating between them or reloading the right type with find.
|
31
|
+
#
|
32
|
+
# Note, all the attributes for all the cases are kept in the same table. Read more:
|
33
|
+
# http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
|
34
|
+
#
|
35
|
+
module Inheritance
|
36
|
+
extend ActiveSupport::Concern
|
37
|
+
|
38
|
+
module ClassMethods
|
39
|
+
# Determines if one of the attributes passed in is the inheritance column,
|
40
|
+
# and if the inheritance column is attr accessible, it initializes an
|
41
|
+
# instance of the given subclass instead of the base class.
|
42
|
+
def new(*args, &block)
|
43
|
+
if abstract_class? || self == Base
|
44
|
+
raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated."
|
45
|
+
end
|
46
|
+
|
47
|
+
super
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the class descending directly from DuckRecord::Base, or
|
51
|
+
# an abstract class, if any, in the inheritance hierarchy.
|
52
|
+
#
|
53
|
+
# If A extends DuckRecord::Base, A.base_class will return A. If B descends from A
|
54
|
+
# through some arbitrarily deep hierarchy, B.base_class will return A.
|
55
|
+
#
|
56
|
+
# If B < A and C < B and if A is an abstract_class then both B.base_class
|
57
|
+
# and C.base_class would return B as the answer since A is an abstract_class.
|
58
|
+
def base_class
|
59
|
+
unless self < Base
|
60
|
+
raise DuckRecordError, "#{name} doesn't belong in a hierarchy descending from DuckRecord"
|
61
|
+
end
|
62
|
+
|
63
|
+
if superclass == Base || superclass.abstract_class?
|
64
|
+
self
|
65
|
+
else
|
66
|
+
superclass.base_class
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Set this to true if this is an abstract class (see <tt>abstract_class?</tt>).
|
71
|
+
# If you are using inheritance with DuckRecord and don't want child classes
|
72
|
+
# to utilize the implied STI table name of the parent class, this will need to be true.
|
73
|
+
# For example, given the following:
|
74
|
+
#
|
75
|
+
# class SuperClass < DuckRecord::Base
|
76
|
+
# self.abstract_class = true
|
77
|
+
# end
|
78
|
+
# class Child < SuperClass
|
79
|
+
# self.table_name = 'the_table_i_really_want'
|
80
|
+
# end
|
81
|
+
#
|
82
|
+
#
|
83
|
+
# <tt>self.abstract_class = true</tt> is required to make <tt>Child<.find,.create, or any Arel method></tt> use <tt>the_table_i_really_want</tt> instead of a table called <tt>super_classes</tt>
|
84
|
+
#
|
85
|
+
attr_accessor :abstract_class
|
86
|
+
|
87
|
+
# Returns whether this class is an abstract class or not.
|
88
|
+
def abstract_class?
|
89
|
+
defined?(@abstract_class) && @abstract_class == true
|
90
|
+
end
|
91
|
+
|
92
|
+
def inherited(subclass)
|
93
|
+
subclass.instance_variable_set(:@_type_candidates_cache, Concurrent::Map.new)
|
94
|
+
super
|
95
|
+
end
|
96
|
+
|
97
|
+
protected
|
98
|
+
|
99
|
+
# Returns the class type of the record using the current module as a prefix. So descendants of
|
100
|
+
# MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
|
101
|
+
def compute_type(type_name)
|
102
|
+
if type_name.start_with?("::".freeze)
|
103
|
+
# If the type is prefixed with a scope operator then we assume that
|
104
|
+
# the type_name is an absolute reference.
|
105
|
+
ActiveSupport::Dependencies.constantize(type_name)
|
106
|
+
else
|
107
|
+
type_candidate = @_type_candidates_cache[type_name]
|
108
|
+
if type_candidate && type_constant = ActiveSupport::Dependencies.safe_constantize(type_candidate)
|
109
|
+
return type_constant
|
110
|
+
end
|
111
|
+
|
112
|
+
# Build a list of candidates to search for
|
113
|
+
candidates = []
|
114
|
+
type_name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" }
|
115
|
+
candidates << type_name
|
116
|
+
|
117
|
+
candidates.each do |candidate|
|
118
|
+
constant = ActiveSupport::Dependencies.safe_constantize(candidate)
|
119
|
+
if candidate == constant.to_s
|
120
|
+
@_type_candidates_cache[type_name] = candidate
|
121
|
+
return constant
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
raise NameError.new("uninitialized constant #{candidates.first}", candidates.first)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,46 @@
|
|
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
|
+
subset: "is not included in the list"
|
13
|
+
|
14
|
+
# Active Record models configuration
|
15
|
+
duck_record:
|
16
|
+
errors:
|
17
|
+
messages:
|
18
|
+
record_invalid: "Validation failed: %{errors}"
|
19
|
+
# Append your own errors here or at the model/attributes scope.
|
20
|
+
|
21
|
+
# You can define own errors for models or model attributes.
|
22
|
+
# The values :model, :attribute and :value are always available for interpolation.
|
23
|
+
#
|
24
|
+
# For example,
|
25
|
+
# models:
|
26
|
+
# user:
|
27
|
+
# blank: "This is a custom blank message for %{model}: %{attribute}"
|
28
|
+
# attributes:
|
29
|
+
# login:
|
30
|
+
# blank: "This is a custom blank message for User login"
|
31
|
+
# Will define custom blank validation message for User model and
|
32
|
+
# custom blank validation message for login attribute of User model.
|
33
|
+
#models:
|
34
|
+
|
35
|
+
# Translate model names. Used in Model.human_name().
|
36
|
+
#models:
|
37
|
+
# For example,
|
38
|
+
# user: "Dude"
|
39
|
+
# will translate User model name to "Dude"
|
40
|
+
|
41
|
+
# Translate model attribute names. Used in Model.human_attribute_name(attribute).
|
42
|
+
#attributes:
|
43
|
+
# For example,
|
44
|
+
# user:
|
45
|
+
# login: "Handle"
|
46
|
+
# will translate User attribute "login" as "Handle"
|
@@ -0,0 +1,71 @@
|
|
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
|
+
def attribute_types # :nodoc:
|
11
|
+
load_schema
|
12
|
+
@attribute_types ||= Hash.new(Type.default_value)
|
13
|
+
end
|
14
|
+
|
15
|
+
def yaml_encoder # :nodoc:
|
16
|
+
@yaml_encoder ||= AttributeSet::YAMLEncoder.new(attribute_types)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns the type of the attribute with the given name, after applying
|
20
|
+
# all modifiers. This method is the only valid source of information for
|
21
|
+
# anything related to the types of a model's attributes. This method will
|
22
|
+
# access the database and load the model's schema if it is required.
|
23
|
+
#
|
24
|
+
# The return value of this method will implement the interface described
|
25
|
+
# by ActiveModel::Type::Value (though the object itself may not subclass
|
26
|
+
# it).
|
27
|
+
#
|
28
|
+
# +attr_name+ The name of the attribute to retrieve the type for. Must be
|
29
|
+
# a string
|
30
|
+
def type_for_attribute(attr_name, &block)
|
31
|
+
if block
|
32
|
+
attribute_types.fetch(attr_name, &block)
|
33
|
+
else
|
34
|
+
attribute_types[attr_name]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def _default_attributes # :nodoc:
|
39
|
+
@default_attributes ||= AttributeSet.new({})
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def schema_loaded?
|
45
|
+
defined?(@schema_loaded) && @schema_loaded
|
46
|
+
end
|
47
|
+
|
48
|
+
def load_schema
|
49
|
+
unless schema_loaded?
|
50
|
+
load_schema!
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def load_schema!
|
55
|
+
@schema_loaded = true
|
56
|
+
end
|
57
|
+
|
58
|
+
def reload_schema_from_cache
|
59
|
+
@attribute_types = nil
|
60
|
+
@default_attributes = nil
|
61
|
+
@attributes_builder = nil
|
62
|
+
@schema_loaded = false
|
63
|
+
@attribute_names = nil
|
64
|
+
@yaml_encoder = nil
|
65
|
+
direct_descendants.each do |descendant|
|
66
|
+
descendant.send(:reload_schema_from_cache)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|