formed 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/README.md +146 -0
- data/Rakefile +12 -0
- data/lib/active_form.rb +12 -0
- data/lib/formed/acts_like_model.rb +27 -0
- data/lib/formed/association_relation.rb +22 -0
- data/lib/formed/associations/association.rb +193 -0
- data/lib/formed/associations/builder/association.rb +116 -0
- data/lib/formed/associations/builder/collection_association.rb +71 -0
- data/lib/formed/associations/builder/has_many.rb +24 -0
- data/lib/formed/associations/builder/has_one.rb +44 -0
- data/lib/formed/associations/builder/singular_association.rb +46 -0
- data/lib/formed/associations/builder.rb +13 -0
- data/lib/formed/associations/collection_association.rb +296 -0
- data/lib/formed/associations/collection_proxy.rb +519 -0
- data/lib/formed/associations/foreign_association.rb +37 -0
- data/lib/formed/associations/has_many_association.rb +63 -0
- data/lib/formed/associations/has_one_association.rb +27 -0
- data/lib/formed/associations/singular_association.rb +66 -0
- data/lib/formed/associations.rb +62 -0
- data/lib/formed/attributes.rb +42 -0
- data/lib/formed/base.rb +183 -0
- data/lib/formed/core.rb +73 -0
- data/lib/formed/from_model.rb +41 -0
- data/lib/formed/from_params.rb +33 -0
- data/lib/formed/inheritance.rb +179 -0
- data/lib/formed/nested_attributes.rb +287 -0
- data/lib/formed/reflection.rb +781 -0
- data/lib/formed/relation/delegation.rb +147 -0
- data/lib/formed/relation.rb +113 -0
- data/lib/formed/version.rb +3 -0
- data/lib/generators/active_form/form_generator.rb +72 -0
- data/lib/generators/active_form/templates/form.rb.tt +8 -0
- data/lib/generators/active_form/templates/form_spec.rb.tt +5 -0
- data/lib/generators/active_form/templates/module.rb.tt +4 -0
- metadata +203 -0
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formed
|
4
|
+
module Associations
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
def association(name) # :nodoc:
|
8
|
+
association = association_instance_get(name)
|
9
|
+
|
10
|
+
if association.nil?
|
11
|
+
unless (reflection = self.class._reflect_on_association(name))
|
12
|
+
raise AssociationNotFoundError.new(self, name)
|
13
|
+
end
|
14
|
+
|
15
|
+
association = reflection.association_class.new(self, reflection)
|
16
|
+
association_instance_set(name, association)
|
17
|
+
end
|
18
|
+
|
19
|
+
association
|
20
|
+
end
|
21
|
+
|
22
|
+
def association_cached?(name) # :nodoc:
|
23
|
+
@association_cache.key?(name)
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize_dup(*) # :nodoc:
|
27
|
+
@association_cache = {}
|
28
|
+
super
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def init_internals
|
34
|
+
@association_cache = {}
|
35
|
+
super
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns the specified association instance if it exists, +nil+ otherwise.
|
39
|
+
def association_instance_get(name)
|
40
|
+
@association_cache[name]
|
41
|
+
end
|
42
|
+
|
43
|
+
# Set the specified association instance.
|
44
|
+
def association_instance_set(name, association)
|
45
|
+
@association_cache[name] = association
|
46
|
+
end
|
47
|
+
|
48
|
+
class_methods do
|
49
|
+
def has_many(name, scope = nil, **options, &extension)
|
50
|
+
reflection = Builder::HasMany.build(self, name, scope, options, &extension)
|
51
|
+
Reflection.add_reflection self, name, reflection
|
52
|
+
accepts_nested_attributes_for name
|
53
|
+
end
|
54
|
+
|
55
|
+
def has_one(name, scope = nil, **options)
|
56
|
+
reflection = Builder::HasOne.build(self, name, scope, options)
|
57
|
+
Reflection.add_reflection self, name, reflection
|
58
|
+
accepts_nested_attributes_for name
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formed
|
4
|
+
module Attributes
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
def _has_attribute?(name)
|
8
|
+
attributes.key?(name)
|
9
|
+
end
|
10
|
+
|
11
|
+
def attribute_present?(attr_name)
|
12
|
+
attr_name = attr_name.to_s
|
13
|
+
attr_name = self.class.attribute_aliases[attr_name] || attr_name
|
14
|
+
value = _read_attribute(attr_name)
|
15
|
+
!value.nil? && !(value.respond_to?(:empty?) && value.empty?)
|
16
|
+
end
|
17
|
+
|
18
|
+
def type_for_attribute(attr)
|
19
|
+
self.class.attribute_types[attr].type
|
20
|
+
end
|
21
|
+
|
22
|
+
def column_for_attribute(attr)
|
23
|
+
model.column_for_attribute(attr)
|
24
|
+
end
|
25
|
+
|
26
|
+
def has_attribute?(attr_name)
|
27
|
+
attr_name = attr_name.to_s
|
28
|
+
attr_name = self.class.attribute_aliases[attr_name] || attr_name
|
29
|
+
@attributes.key?(attr_name)
|
30
|
+
end
|
31
|
+
|
32
|
+
def attributes_with_values
|
33
|
+
attributes.select { |_, v| v.present? }
|
34
|
+
end
|
35
|
+
|
36
|
+
module ClassMethods
|
37
|
+
def _has_attribute?(name)
|
38
|
+
attribute_types.key?(name.to_s)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/formed/base.rb
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "formed/relation/delegation"
|
4
|
+
require "formed/associations"
|
5
|
+
require "formed/core"
|
6
|
+
require "formed/inheritance"
|
7
|
+
require "formed/reflection"
|
8
|
+
require "formed/relation"
|
9
|
+
require "formed/attributes"
|
10
|
+
require "formed/nested_attributes"
|
11
|
+
require "formed/association_relation"
|
12
|
+
require "formed/associations/association"
|
13
|
+
require "formed/associations/singular_association"
|
14
|
+
require "formed/associations/collection_association"
|
15
|
+
require "formed/associations/foreign_association"
|
16
|
+
require "formed/associations/collection_proxy"
|
17
|
+
require "formed/associations/builder"
|
18
|
+
require "formed/associations/builder/association"
|
19
|
+
require "formed/associations/builder/singular_association"
|
20
|
+
require "formed/associations/builder/collection_association"
|
21
|
+
require "formed/associations/builder/has_one"
|
22
|
+
require "formed/associations/builder/has_many"
|
23
|
+
require "formed/associations/has_many_association"
|
24
|
+
require "formed/associations/has_one_association"
|
25
|
+
|
26
|
+
require "formed/acts_like_model"
|
27
|
+
require "formed/from_model"
|
28
|
+
require "formed/from_params"
|
29
|
+
|
30
|
+
module Formed
|
31
|
+
RESTRICTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass)
|
32
|
+
|
33
|
+
class FormedError < StandardError
|
34
|
+
end
|
35
|
+
|
36
|
+
class AssociationTypeMismatch < FormedError
|
37
|
+
end
|
38
|
+
|
39
|
+
class Base
|
40
|
+
extend Relation::Delegation::DelegateCache
|
41
|
+
|
42
|
+
include Formed::Associations
|
43
|
+
include ActiveModel::Model
|
44
|
+
include ActiveModel::Validations
|
45
|
+
include ActiveModel::Attributes
|
46
|
+
include ActiveModel::AttributeAssignment
|
47
|
+
include ActiveModel::AttributeMethods
|
48
|
+
include ActiveModel::Callbacks
|
49
|
+
include ActiveModel::Dirty
|
50
|
+
include Formed::Attributes
|
51
|
+
|
52
|
+
include Formed::Core
|
53
|
+
include Formed::Inheritance
|
54
|
+
include Formed::Reflection
|
55
|
+
include Formed::NestedAttributes
|
56
|
+
|
57
|
+
include Formed::ActsLikeModel
|
58
|
+
include Formed::FromParams
|
59
|
+
include FromModel
|
60
|
+
|
61
|
+
def init_internals
|
62
|
+
@marked_for_destruction = false
|
63
|
+
@association_cache = {}
|
64
|
+
klass = self.class
|
65
|
+
|
66
|
+
@strict_loading = false
|
67
|
+
@strict_loading_mode = :all
|
68
|
+
|
69
|
+
klass.define_attribute_methods
|
70
|
+
end
|
71
|
+
|
72
|
+
def initialize_internals_callback; end
|
73
|
+
|
74
|
+
def initialize(attributes = nil)
|
75
|
+
@new_record = true
|
76
|
+
@attributes = self.class._default_attributes.deep_dup
|
77
|
+
|
78
|
+
init_internals
|
79
|
+
initialize_internals_callback
|
80
|
+
|
81
|
+
assign_attributes(attributes) if attributes
|
82
|
+
|
83
|
+
yield self if block_given?
|
84
|
+
_run_initialize_callbacks
|
85
|
+
end
|
86
|
+
|
87
|
+
class_attribute :inheritance_column, default: :type
|
88
|
+
|
89
|
+
define_model_callbacks :initialize, only: [:after]
|
90
|
+
define_model_callbacks :validation, only: %i[before after]
|
91
|
+
|
92
|
+
attribute :id, :integer
|
93
|
+
attribute :_destroy, :boolean
|
94
|
+
|
95
|
+
class_attribute :model
|
96
|
+
class_attribute :primary_key, default: "id"
|
97
|
+
class_attribute :model_name
|
98
|
+
class_attribute :default_ignored_attributes, default: %w[id created_at updated_at]
|
99
|
+
class_attribute :ignored_attributes, default: []
|
100
|
+
|
101
|
+
def with_context(contexts = {})
|
102
|
+
@context = OpenStruct.new(contexts)
|
103
|
+
_reflections.each do |_, reflection|
|
104
|
+
if (instance = public_send(reflection.name))
|
105
|
+
instance.with_context(@context)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
self
|
110
|
+
end
|
111
|
+
|
112
|
+
attr_reader :context
|
113
|
+
|
114
|
+
def inspect
|
115
|
+
# We check defined?(@attributes) not to issue warnings if the object is
|
116
|
+
# allocated but not initialized.
|
117
|
+
inspection = if defined?(@attributes) && @attributes
|
118
|
+
self.class.attribute_names.filter_map do |name|
|
119
|
+
"#{name}: #{_read_attribute(name).inspect}" if self.class.attribute_types.key?(name)
|
120
|
+
end.join(", ")
|
121
|
+
else
|
122
|
+
"not initialized"
|
123
|
+
end
|
124
|
+
|
125
|
+
"#<#{self.class} #{inspection}>"
|
126
|
+
end
|
127
|
+
|
128
|
+
def persisted?
|
129
|
+
id.present? && id.to_i.positive?
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.from_json(json)
|
133
|
+
params = JSON.parse(json)
|
134
|
+
from_params(params)
|
135
|
+
end
|
136
|
+
|
137
|
+
def valid?(options = {})
|
138
|
+
run_callbacks(:validation) do
|
139
|
+
options = {} if options.blank?
|
140
|
+
context = options[:context]
|
141
|
+
validations = [super(context)]
|
142
|
+
|
143
|
+
validations.all?
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def invalid?(options = {})
|
148
|
+
!valid?(options)
|
149
|
+
end
|
150
|
+
|
151
|
+
def to_key
|
152
|
+
[id]
|
153
|
+
end
|
154
|
+
|
155
|
+
def self.model_name
|
156
|
+
if model.is_a?(Symbol)
|
157
|
+
ActiveModel::Name.new(self, nil, model.to_s)
|
158
|
+
elsif model.present?
|
159
|
+
ActiveModel::Name.new(self, nil, model.model_name.name.split("::").last)
|
160
|
+
else
|
161
|
+
name = self.name.demodulize.delete_suffix("Form")
|
162
|
+
name = self.name if name.blank?
|
163
|
+
ActiveModel::Name.new(self, nil, name)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def model_name
|
168
|
+
self.class.model_name
|
169
|
+
end
|
170
|
+
|
171
|
+
def new_record?
|
172
|
+
!persisted?
|
173
|
+
end
|
174
|
+
|
175
|
+
def marked_for_destruction?
|
176
|
+
attributes["_destroy"]
|
177
|
+
end
|
178
|
+
|
179
|
+
def destroy?
|
180
|
+
_destroy
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
data/lib/formed/core.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formed
|
4
|
+
module Core
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def inherited(child_class) # :nodoc:
|
9
|
+
# initialize cache at class definition for thread safety
|
10
|
+
unless child_class.base_class?
|
11
|
+
klass = self
|
12
|
+
klass = klass.superclass until klass.base_class?
|
13
|
+
end
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize_generated_modules # :nodoc:
|
18
|
+
generated_association_methods
|
19
|
+
end
|
20
|
+
|
21
|
+
def generated_association_methods # :nodoc:
|
22
|
+
@generated_association_methods ||= begin
|
23
|
+
mod = const_set(:GeneratedAssociationMethods, Module.new)
|
24
|
+
private_constant :GeneratedAssociationMethods
|
25
|
+
include mod
|
26
|
+
|
27
|
+
mod
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns columns which shouldn't be exposed while calling +#inspect+.
|
32
|
+
def filter_attributes
|
33
|
+
if defined?(@filter_attributes)
|
34
|
+
@filter_attributes
|
35
|
+
else
|
36
|
+
superclass.filter_attributes
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Specifies columns which shouldn't be exposed while calling +#inspect+.
|
41
|
+
def filter_attributes=(filter_attributes)
|
42
|
+
@inspection_filter = nil
|
43
|
+
@filter_attributes = filter_attributes
|
44
|
+
end
|
45
|
+
|
46
|
+
def inspection_filter # :nodoc:
|
47
|
+
if defined?(@filter_attributes)
|
48
|
+
@inspection_filter ||= begin
|
49
|
+
mask = InspectionMask.new(ActiveSupport::ParameterFilter::FILTERED)
|
50
|
+
ActiveSupport::ParameterFilter.new(@filter_attributes, mask: mask)
|
51
|
+
end
|
52
|
+
else
|
53
|
+
superclass.inspection_filter
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns a string like 'Post(id:integer, title:string, body:text)'
|
58
|
+
def inspect # :nodoc:
|
59
|
+
if self == Formed::Base
|
60
|
+
super
|
61
|
+
else
|
62
|
+
attr_list = attribute_types.map { |name, type| "#{name}: #{type.type}" } * ", "
|
63
|
+
"#{super}(#{attr_list})"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Override the default class equality method to provide support for decorated models.
|
68
|
+
def ===(object) # :nodoc:
|
69
|
+
object.is_a?(self)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formed
|
4
|
+
module FromModel
|
5
|
+
class FromModelAssignment
|
6
|
+
def self.call(instance, record)
|
7
|
+
record.attributes.each do |k, v|
|
8
|
+
instance.public_send("#{k}=", v) if instance.attributes.key?(k)
|
9
|
+
end
|
10
|
+
record._reflections.each do |attr, _record_reflection|
|
11
|
+
next unless (form_reflection = instance._reflections[attr])
|
12
|
+
|
13
|
+
case form_reflection.macro
|
14
|
+
when :has_one
|
15
|
+
instance.send("build_#{attr}").from_model(record.public_send(attr))
|
16
|
+
when :has_many
|
17
|
+
record.public_send(attr).each do |associated_record|
|
18
|
+
instance.public_send(attr).build.from_model(associated_record)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
instance.id = record.id
|
24
|
+
instance.map_model(record)
|
25
|
+
instance
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
extend ActiveSupport::Concern
|
30
|
+
|
31
|
+
def from_model(model)
|
32
|
+
FromModelAssignment.call(self, model)
|
33
|
+
end
|
34
|
+
|
35
|
+
module ClassMethods
|
36
|
+
def from_model(model)
|
37
|
+
new.from_model(model)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formed
|
4
|
+
module FromParams
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
class FromParamsAssignment
|
8
|
+
def self.call(instance, attributes_hash)
|
9
|
+
attributes_hash.each do |k, v|
|
10
|
+
if instance.attributes.key?(k.to_s)
|
11
|
+
instance.public_send("#{k}=", v)
|
12
|
+
elsif instance.respond_to?("#{k}=")
|
13
|
+
instance.public_send("#{k}=", v)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
instance
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def from_params(params, additional_params = {})
|
22
|
+
attributes_hash = params.merge(additional_params)
|
23
|
+
|
24
|
+
FromParamsAssignment.call(self, attributes_hash)
|
25
|
+
end
|
26
|
+
|
27
|
+
module ClassMethods
|
28
|
+
def from_params(params, additional_params = {})
|
29
|
+
new.from_params(params, additional_params)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formed
|
4
|
+
module Inheritance
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
class_attribute :store_full_class_name, instance_writer: false, default: true
|
9
|
+
|
10
|
+
set_base_class
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
# Determines if one of the attributes passed in is the inheritance column,
|
15
|
+
# and if the inheritance column is attr accessible, it initializes an
|
16
|
+
# instance of the given subclass instead of the base class.
|
17
|
+
def new(attributes = nil, &block)
|
18
|
+
if abstract_class? || self == Formed
|
19
|
+
raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated."
|
20
|
+
end
|
21
|
+
|
22
|
+
if _has_attribute?(inheritance_column)
|
23
|
+
subclass = subclass_from_attributes(attributes)
|
24
|
+
|
25
|
+
if subclass.nil? && (scope_attributes = current_scope&.scope_for_create)
|
26
|
+
subclass = subclass_from_attributes(scope_attributes)
|
27
|
+
end
|
28
|
+
|
29
|
+
subclass = subclass_from_attributes(column_defaults) if subclass.nil? && base_class?
|
30
|
+
end
|
31
|
+
|
32
|
+
if subclass && subclass != self
|
33
|
+
subclass.new(attributes, &block)
|
34
|
+
else
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the class descending directly from ActiveRecord::Base, or
|
40
|
+
# an abstract class, if any, in the inheritance hierarchy.
|
41
|
+
#
|
42
|
+
# If A extends ActiveRecord::Base, A.base_class will return A. If B descends from A
|
43
|
+
# through some arbitrarily deep hierarchy, B.base_class will return A.
|
44
|
+
#
|
45
|
+
# If B < A and C < B and if A is an abstract_class then both B.base_class
|
46
|
+
# and C.base_class would return B as the answer since A is an abstract_class.
|
47
|
+
attr_reader :base_class
|
48
|
+
|
49
|
+
# Returns whether the class is a base class.
|
50
|
+
# See #base_class for more information.
|
51
|
+
def base_class?
|
52
|
+
base_class == self
|
53
|
+
end
|
54
|
+
|
55
|
+
attr_accessor :abstract_class
|
56
|
+
|
57
|
+
def abstract_class?
|
58
|
+
defined?(@abstract_class) && @abstract_class == true
|
59
|
+
end
|
60
|
+
|
61
|
+
def primary_abstract_class; end
|
62
|
+
|
63
|
+
def inherited(subclass)
|
64
|
+
subclass.set_base_class
|
65
|
+
subclass.instance_variable_set(:@_type_candidates_cache, Concurrent::Map.new)
|
66
|
+
super
|
67
|
+
end
|
68
|
+
|
69
|
+
def dup # :nodoc:
|
70
|
+
# `initialize_dup` / `initialize_copy` don't work when defined
|
71
|
+
# in the `singleton_class`.
|
72
|
+
other = super
|
73
|
+
other.set_base_class
|
74
|
+
other
|
75
|
+
end
|
76
|
+
|
77
|
+
def initialize_clone(other) # :nodoc:
|
78
|
+
super
|
79
|
+
set_base_class
|
80
|
+
end
|
81
|
+
|
82
|
+
protected
|
83
|
+
|
84
|
+
# Returns the class type of the record using the current module as a prefix. So descendants of
|
85
|
+
# MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
|
86
|
+
def compute_type(type_name)
|
87
|
+
if type_name.start_with?("::")
|
88
|
+
# If the type is prefixed with a scope operator then we assume that
|
89
|
+
# the type_name is an absolute reference.
|
90
|
+
type_name.constantize
|
91
|
+
else
|
92
|
+
|
93
|
+
type_candidate = @_type_candidates_cache[type_name]
|
94
|
+
if type_candidate && (type_constant = type_candidate.safe_constantize)
|
95
|
+
return type_constant
|
96
|
+
end
|
97
|
+
|
98
|
+
# Build a list of candidates to search for
|
99
|
+
candidates = []
|
100
|
+
name.scan(/::|$/) { candidates.unshift "#{::Regexp.last_match.pre_match}::#{type_name}" }
|
101
|
+
candidates << type_name
|
102
|
+
form_candidates = []
|
103
|
+
candidates.each do |candidate|
|
104
|
+
next if candidate.end_with?("Form")
|
105
|
+
|
106
|
+
form_candidates << "#{candidate}Form"
|
107
|
+
end
|
108
|
+
|
109
|
+
candidates += form_candidates
|
110
|
+
|
111
|
+
candidates.each do |candidate|
|
112
|
+
constant = candidate.safe_constantize
|
113
|
+
if candidate == constant.to_s
|
114
|
+
@_type_candidates_cache[type_name] = candidate
|
115
|
+
return constant
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
raise NameError.new("uninitialized constant #{candidates.first}", candidates.first)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def set_base_class # :nodoc:
|
124
|
+
@base_class = if self == Formed::Base
|
125
|
+
self
|
126
|
+
else
|
127
|
+
unless self < Formed::Base
|
128
|
+
raise FormedError, "#{name} doesn't belong in a hierarchy descending from Formed"
|
129
|
+
end
|
130
|
+
|
131
|
+
if superclass == Formed || superclass.abstract_class?
|
132
|
+
self
|
133
|
+
else
|
134
|
+
superclass.base_class
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
# Detect the subclass from the inheritance column of attrs. If the inheritance column value
|
142
|
+
# is not self or a valid subclass, raises ActiveRecord::SubclassNotFound
|
143
|
+
def subclass_from_attributes(attrs)
|
144
|
+
attrs = attrs.to_h if attrs.respond_to?(:permitted?)
|
145
|
+
return unless attrs.is_a?(Hash)
|
146
|
+
|
147
|
+
subclass_name = attrs[inheritance_column] || attrs[inheritance_column.to_sym]
|
148
|
+
|
149
|
+
return unless subclass_name.present?
|
150
|
+
|
151
|
+
find_sti_class(subclass_name)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def initialize_dup(other)
|
156
|
+
super
|
157
|
+
ensure_proper_type
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
def initialize_internals_callback
|
163
|
+
super
|
164
|
+
ensure_proper_type
|
165
|
+
end
|
166
|
+
|
167
|
+
# Sets the attribute used for single table inheritance to this class name if this is not the
|
168
|
+
# ActiveRecord::Base descendant.
|
169
|
+
# Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to
|
170
|
+
# do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself.
|
171
|
+
# No such attribute would be set for objects of the Message class in that example.
|
172
|
+
def ensure_proper_type
|
173
|
+
klass = self.class
|
174
|
+
return unless klass.finder_needs_type_condition?
|
175
|
+
|
176
|
+
_write_attribute(klass.inheritance_column, klass.sti_name)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|