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