granite-form 0.1.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/.codeclimate.yml +13 -0
- data/.github/workflows/ci.yml +35 -0
- data/.github/workflows/main.yml +29 -0
- data/.gitignore +21 -0
- data/.rspec +2 -0
- data/.rubocop.yml +64 -0
- data/.rubocop_todo.yml +48 -0
- data/Appraisals +8 -0
- data/CHANGELOG.md +73 -0
- data/Gemfile +8 -0
- data/Guardfile +77 -0
- data/LICENSE +22 -0
- data/README.md +429 -0
- data/Rakefile +6 -0
- data/gemfiles/rails.4.2.gemfile +15 -0
- data/gemfiles/rails.5.0.gemfile +15 -0
- data/gemfiles/rails.5.1.gemfile +15 -0
- data/gemfiles/rails.5.2.gemfile +15 -0
- data/gemfiles/rails.6.0.gemfile +14 -0
- data/gemfiles/rails.6.1.gemfile +14 -0
- data/gemfiles/rails.7.0.gemfile +14 -0
- data/granite-form.gemspec +31 -0
- data/lib/granite/form/active_record/associations.rb +57 -0
- data/lib/granite/form/active_record/nested_attributes.rb +20 -0
- data/lib/granite/form/base.rb +15 -0
- data/lib/granite/form/config.rb +42 -0
- data/lib/granite/form/errors.rb +111 -0
- data/lib/granite/form/extensions.rb +36 -0
- data/lib/granite/form/model/associations/base.rb +97 -0
- data/lib/granite/form/model/associations/collection/embedded.rb +14 -0
- data/lib/granite/form/model/associations/collection/proxy.rb +35 -0
- data/lib/granite/form/model/associations/embeds_any.rb +19 -0
- data/lib/granite/form/model/associations/embeds_many.rb +152 -0
- data/lib/granite/form/model/associations/embeds_one.rb +112 -0
- data/lib/granite/form/model/associations/nested_attributes.rb +215 -0
- data/lib/granite/form/model/associations/persistence_adapters/active_record/referenced_proxy.rb +33 -0
- data/lib/granite/form/model/associations/persistence_adapters/active_record.rb +68 -0
- data/lib/granite/form/model/associations/persistence_adapters/base.rb +55 -0
- data/lib/granite/form/model/associations/references_any.rb +43 -0
- data/lib/granite/form/model/associations/references_many.rb +113 -0
- data/lib/granite/form/model/associations/references_one.rb +88 -0
- data/lib/granite/form/model/associations/reflections/base.rb +92 -0
- data/lib/granite/form/model/associations/reflections/embeds_any.rb +52 -0
- data/lib/granite/form/model/associations/reflections/embeds_many.rb +17 -0
- data/lib/granite/form/model/associations/reflections/embeds_one.rb +19 -0
- data/lib/granite/form/model/associations/reflections/references_any.rb +65 -0
- data/lib/granite/form/model/associations/reflections/references_many.rb +30 -0
- data/lib/granite/form/model/associations/reflections/references_one.rb +32 -0
- data/lib/granite/form/model/associations/reflections/singular.rb +37 -0
- data/lib/granite/form/model/associations/validations.rb +41 -0
- data/lib/granite/form/model/associations.rb +120 -0
- data/lib/granite/form/model/attributes/attribute.rb +75 -0
- data/lib/granite/form/model/attributes/base.rb +134 -0
- data/lib/granite/form/model/attributes/collection.rb +19 -0
- data/lib/granite/form/model/attributes/dictionary.rb +28 -0
- data/lib/granite/form/model/attributes/localized.rb +44 -0
- data/lib/granite/form/model/attributes/reference_many.rb +21 -0
- data/lib/granite/form/model/attributes/reference_one.rb +52 -0
- data/lib/granite/form/model/attributes/reflections/attribute.rb +61 -0
- data/lib/granite/form/model/attributes/reflections/base.rb +62 -0
- data/lib/granite/form/model/attributes/reflections/collection.rb +12 -0
- data/lib/granite/form/model/attributes/reflections/dictionary.rb +15 -0
- data/lib/granite/form/model/attributes/reflections/localized.rb +45 -0
- data/lib/granite/form/model/attributes/reflections/reference_many.rb +12 -0
- data/lib/granite/form/model/attributes/reflections/reference_one.rb +49 -0
- data/lib/granite/form/model/attributes/reflections/represents.rb +56 -0
- data/lib/granite/form/model/attributes/represents.rb +67 -0
- data/lib/granite/form/model/attributes.rb +204 -0
- data/lib/granite/form/model/callbacks.rb +72 -0
- data/lib/granite/form/model/conventions.rb +40 -0
- data/lib/granite/form/model/dirty.rb +84 -0
- data/lib/granite/form/model/lifecycle.rb +309 -0
- data/lib/granite/form/model/localization.rb +26 -0
- data/lib/granite/form/model/persistence.rb +59 -0
- data/lib/granite/form/model/primary.rb +59 -0
- data/lib/granite/form/model/representation.rb +101 -0
- data/lib/granite/form/model/scopes.rb +118 -0
- data/lib/granite/form/model/validations/associated.rb +22 -0
- data/lib/granite/form/model/validations/nested.rb +56 -0
- data/lib/granite/form/model/validations.rb +29 -0
- data/lib/granite/form/model.rb +33 -0
- data/lib/granite/form/railtie.rb +9 -0
- data/lib/granite/form/undefined_class.rb +11 -0
- data/lib/granite/form/version.rb +5 -0
- data/lib/granite/form.rb +163 -0
- data/spec/lib/granite/form/active_record/associations_spec.rb +211 -0
- data/spec/lib/granite/form/active_record/nested_attributes_spec.rb +15 -0
- data/spec/lib/granite/form/config_spec.rb +66 -0
- data/spec/lib/granite/form/model/associations/embeds_many_spec.rb +706 -0
- data/spec/lib/granite/form/model/associations/embeds_one_spec.rb +533 -0
- data/spec/lib/granite/form/model/associations/nested_attributes_spec.rb +119 -0
- data/spec/lib/granite/form/model/associations/persistence_adapters/active_record_spec.rb +58 -0
- data/spec/lib/granite/form/model/associations/references_many_spec.rb +572 -0
- data/spec/lib/granite/form/model/associations/references_one_spec.rb +445 -0
- data/spec/lib/granite/form/model/associations/reflections/embeds_any_spec.rb +42 -0
- data/spec/lib/granite/form/model/associations/reflections/embeds_many_spec.rb +145 -0
- data/spec/lib/granite/form/model/associations/reflections/embeds_one_spec.rb +117 -0
- data/spec/lib/granite/form/model/associations/reflections/references_many_spec.rb +303 -0
- data/spec/lib/granite/form/model/associations/reflections/references_one_spec.rb +287 -0
- data/spec/lib/granite/form/model/associations/validations_spec.rb +137 -0
- data/spec/lib/granite/form/model/associations_spec.rb +198 -0
- data/spec/lib/granite/form/model/attributes/attribute_spec.rb +186 -0
- data/spec/lib/granite/form/model/attributes/base_spec.rb +97 -0
- data/spec/lib/granite/form/model/attributes/collection_spec.rb +72 -0
- data/spec/lib/granite/form/model/attributes/dictionary_spec.rb +100 -0
- data/spec/lib/granite/form/model/attributes/localized_spec.rb +103 -0
- data/spec/lib/granite/form/model/attributes/reflections/attribute_spec.rb +72 -0
- data/spec/lib/granite/form/model/attributes/reflections/base_spec.rb +56 -0
- data/spec/lib/granite/form/model/attributes/reflections/collection_spec.rb +37 -0
- data/spec/lib/granite/form/model/attributes/reflections/dictionary_spec.rb +43 -0
- data/spec/lib/granite/form/model/attributes/reflections/localized_spec.rb +37 -0
- data/spec/lib/granite/form/model/attributes/reflections/represents_spec.rb +70 -0
- data/spec/lib/granite/form/model/attributes/represents_spec.rb +85 -0
- data/spec/lib/granite/form/model/attributes_spec.rb +350 -0
- data/spec/lib/granite/form/model/callbacks_spec.rb +337 -0
- data/spec/lib/granite/form/model/conventions_spec.rb +11 -0
- data/spec/lib/granite/form/model/dirty_spec.rb +84 -0
- data/spec/lib/granite/form/model/lifecycle_spec.rb +356 -0
- data/spec/lib/granite/form/model/persistence_spec.rb +46 -0
- data/spec/lib/granite/form/model/primary_spec.rb +84 -0
- data/spec/lib/granite/form/model/representation_spec.rb +139 -0
- data/spec/lib/granite/form/model/scopes_spec.rb +86 -0
- data/spec/lib/granite/form/model/typecasting_spec.rb +193 -0
- data/spec/lib/granite/form/model/validations/associated_spec.rb +102 -0
- data/spec/lib/granite/form/model/validations/nested_spec.rb +164 -0
- data/spec/lib/granite/form/model/validations_spec.rb +31 -0
- data/spec/lib/granite/form/model_spec.rb +10 -0
- data/spec/lib/granite/form_spec.rb +11 -0
- data/spec/shared/nested_attribute_examples.rb +332 -0
- data/spec/spec_helper.rb +50 -0
- data/spec/support/model_helpers.rb +10 -0
- data/spec/support/muffle_helper.rb +7 -0
- metadata +403 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
module Granite
|
|
2
|
+
module Form
|
|
3
|
+
module Model
|
|
4
|
+
module Associations
|
|
5
|
+
module NestedAttributes
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
DESTROY_ATTRIBUTE = '_destroy'.freeze
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
class_attribute :nested_attributes_options, instance_writer: false
|
|
12
|
+
self.nested_attributes_options = {}
|
|
13
|
+
|
|
14
|
+
extend NestedAttributesMethodsExtension
|
|
15
|
+
prepend PrependMethods
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module PrependMethods
|
|
19
|
+
def assign_attributes(attrs)
|
|
20
|
+
if self.class.nested_attributes_options.present?
|
|
21
|
+
attrs = attrs.to_unsafe_hash if attrs.respond_to?(:to_unsafe_hash)
|
|
22
|
+
attrs = attrs.stringify_keys
|
|
23
|
+
|
|
24
|
+
nested_attrs = self.class.nested_attributes_options.keys
|
|
25
|
+
.each_with_object({}) do |association_name, result|
|
|
26
|
+
name = "#{association_name}_attributes"
|
|
27
|
+
result[name] = attrs.delete(name) if attrs.key?(name)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
super(attrs.merge!(nested_attrs))
|
|
31
|
+
else
|
|
32
|
+
super(attrs)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
alias_method :attributes=, :assign_attributes
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class NestedAttributesMethods
|
|
40
|
+
REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == DESTROY_ATTRIBUTE || value.blank? } }
|
|
41
|
+
|
|
42
|
+
def self.accepts_nested_attributes_for(klass, *attr_names)
|
|
43
|
+
options = {allow_destroy: false, update_only: false}
|
|
44
|
+
options.update(attr_names.extract_options!)
|
|
45
|
+
options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only)
|
|
46
|
+
options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
|
|
47
|
+
|
|
48
|
+
NestedAttributesMethodsExtension.ensure_extended!(klass)
|
|
49
|
+
|
|
50
|
+
attr_names.each do |association_name|
|
|
51
|
+
reflection = klass.reflect_on_association(association_name)
|
|
52
|
+
raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?" unless reflection
|
|
53
|
+
klass.nested_attributes_options = klass.nested_attributes_options.merge(association_name.to_sym => options)
|
|
54
|
+
|
|
55
|
+
should_validate_nested = klass.respond_to?(:validates_nested) && !klass.validates_nested?(association_name)
|
|
56
|
+
klass.validates_nested(association_name) if should_validate_nested
|
|
57
|
+
|
|
58
|
+
type = (reflection.collection? ? :collection : :one_to_one)
|
|
59
|
+
klass.nested_attributes_methods_module.class_eval <<-METHOD, __FILE__, __LINE__ + 1
|
|
60
|
+
def #{association_name}_attributes=(attributes)
|
|
61
|
+
Granite::Form::Model::Associations::NestedAttributes::NestedAttributesMethods
|
|
62
|
+
.assign_nested_attributes_for_#{type}_association(self, :#{association_name}, attributes)
|
|
63
|
+
end
|
|
64
|
+
METHOD
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.assign_nested_attributes_for_one_to_one_association(object, association_name, attributes)
|
|
69
|
+
options = object.nested_attributes_options[association_name]
|
|
70
|
+
attributes = attributes.with_indifferent_access
|
|
71
|
+
|
|
72
|
+
association = object.association(association_name)
|
|
73
|
+
existing_record = association.target
|
|
74
|
+
primary_attribute_name = primary_name_for(association.reflection.klass)
|
|
75
|
+
if existing_record
|
|
76
|
+
primary_attribute = existing_record.attribute(primary_attribute_name)
|
|
77
|
+
primary_attribute_value = primary_attribute.typecast(attributes[primary_attribute_name]) if primary_attribute
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if existing_record && (!primary_attribute || options[:update_only] || existing_record.primary_attribute == primary_attribute_value)
|
|
81
|
+
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) unless call_reject_if(object, association_name, attributes)
|
|
82
|
+
elsif attributes[primary_attribute_name].present?
|
|
83
|
+
raise Granite::Form::ObjectNotFound.new(object, association_name, attributes[primary_attribute_name])
|
|
84
|
+
elsif !reject_new_object?(object, association_name, attributes, options)
|
|
85
|
+
assignable_attributes = attributes.except(*unassignable_keys(object))
|
|
86
|
+
|
|
87
|
+
if existing_record && !existing_record.persisted?
|
|
88
|
+
existing_record.assign_attributes(assignable_attributes)
|
|
89
|
+
else
|
|
90
|
+
association.build(assignable_attributes)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.assign_nested_attributes_for_collection_association(object, association_name, attributes_collection)
|
|
96
|
+
options = object.nested_attributes_options[association_name]
|
|
97
|
+
|
|
98
|
+
unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
|
|
99
|
+
raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
check_record_limit!(options[:limit], attributes_collection)
|
|
103
|
+
|
|
104
|
+
association = object.association(association_name)
|
|
105
|
+
primary_attribute_name = primary_name_for(association.reflection.klass)
|
|
106
|
+
|
|
107
|
+
raise Granite::Form::UndefinedPrimaryAttribute.new(object.class, association_name) unless primary_attribute_name
|
|
108
|
+
|
|
109
|
+
if attributes_collection.is_a? Hash
|
|
110
|
+
keys = attributes_collection.keys
|
|
111
|
+
attributes_collection = if keys.include?(primary_attribute_name) || keys.include?(primary_attribute_name.to_sym)
|
|
112
|
+
[attributes_collection]
|
|
113
|
+
else
|
|
114
|
+
attributes_collection.values
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
attributes_collection.each do |attributes|
|
|
119
|
+
attributes = attributes.with_indifferent_access
|
|
120
|
+
|
|
121
|
+
if attributes[primary_attribute_name].blank?
|
|
122
|
+
association.build(attributes.except(*unassignable_keys(object))) unless reject_new_object?(object, association_name, attributes, options)
|
|
123
|
+
else
|
|
124
|
+
existing_record = association.target.detect do |record|
|
|
125
|
+
primary_attribute_value = record.attribute(primary_attribute_name)
|
|
126
|
+
.typecast(attributes[primary_attribute_name])
|
|
127
|
+
record.primary_attribute == primary_attribute_value
|
|
128
|
+
end
|
|
129
|
+
if existing_record
|
|
130
|
+
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) unless call_reject_if(object, association_name, attributes)
|
|
131
|
+
elsif association.reflection.embedded?
|
|
132
|
+
unless reject_new_object?(object, association_name, attributes, options)
|
|
133
|
+
association.reflection.klass.with_sanitize(false) do
|
|
134
|
+
association.build(attributes.except(DESTROY_ATTRIBUTE))
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
else
|
|
138
|
+
raise Granite::Form::ObjectNotFound.new(object, association_name, attributes[primary_attribute_name])
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def self.check_record_limit!(limit, attributes_collection)
|
|
145
|
+
limit = case limit
|
|
146
|
+
when Symbol
|
|
147
|
+
send(limit)
|
|
148
|
+
when Proc
|
|
149
|
+
limit.call
|
|
150
|
+
else
|
|
151
|
+
limit
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
return unless limit && attributes_collection.size > limit
|
|
155
|
+
|
|
156
|
+
raise Granite::Form::TooManyObjects.new(limit, attributes_collection.size)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def self.assign_to_or_mark_for_destruction(object, attributes, allow_destroy)
|
|
160
|
+
object.assign_attributes(attributes.except(*unassignable_keys(object)))
|
|
161
|
+
object.mark_for_destruction if destroy_flag?(attributes) && allow_destroy
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def self.destroy_flag?(hash)
|
|
165
|
+
Granite::Form.typecaster(Boolean).call(hash[DESTROY_ATTRIBUTE])
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def self.reject_new_object?(object, association_name, attributes, options)
|
|
169
|
+
options[:update_only] || destroy_flag?(attributes) || call_reject_if(object, association_name, attributes)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def self.call_reject_if(object, association_name, attributes)
|
|
173
|
+
return false if destroy_flag?(attributes)
|
|
174
|
+
case callback = object.nested_attributes_options[association_name][:reject_if]
|
|
175
|
+
when Symbol
|
|
176
|
+
method(callback).arity.zero? ? send(callback) : send(callback, attributes)
|
|
177
|
+
when Proc
|
|
178
|
+
callback.call(attributes)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def self.unassignable_keys(object)
|
|
183
|
+
[primary_name_for(object.class), DESTROY_ATTRIBUTE].compact
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def self.primary_name_for(klass)
|
|
187
|
+
klass < Granite::Form::Model ? klass.primary_name : 'id'
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
module ClassMethods
|
|
192
|
+
def accepts_nested_attributes_for(*attr_names)
|
|
193
|
+
NestedAttributesMethods.accepts_nested_attributes_for self, *attr_names
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
module NestedAttributesMethodsExtension
|
|
198
|
+
def self.ensure_extended!(klass)
|
|
199
|
+
return if klass.singleton_class.ancestors.include?(self)
|
|
200
|
+
klass.extend(self)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def nested_attributes_methods_module
|
|
204
|
+
@nested_attributes_methods_module ||= begin
|
|
205
|
+
mod = const_set(:NestedAttributesMethods, Module.new)
|
|
206
|
+
include(mod)
|
|
207
|
+
mod
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
data/lib/granite/form/model/associations/persistence_adapters/active_record/referenced_proxy.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Granite
|
|
2
|
+
module Form
|
|
3
|
+
module Model
|
|
4
|
+
module Associations
|
|
5
|
+
module PersistenceAdapters
|
|
6
|
+
class ActiveRecord < Base
|
|
7
|
+
class ReferencedProxy < Granite::Form::Model::Associations::Collection::Proxy
|
|
8
|
+
# You can't create data directly through ActiveRecord::Relation
|
|
9
|
+
METHODS_EXCLUDED_FROM_DELEGATION = %w[build create create!].map(&:to_sym).freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :association
|
|
12
|
+
delegate :scope, to: :@association
|
|
13
|
+
|
|
14
|
+
def method_missing(method, *args, &block)
|
|
15
|
+
delegate_to_scope?(method) ? scope.send(method, *args, &block) : super
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def respond_to_missing?(method, include_private = false)
|
|
19
|
+
delegate_to_scope?(method) || super
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def delegate_to_scope?(method)
|
|
25
|
+
METHODS_EXCLUDED_FROM_DELEGATION.exclude?(method) && scope.respond_to?(method)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
require 'granite/form/model/associations/persistence_adapters/active_record/referenced_proxy'
|
|
2
|
+
|
|
3
|
+
module Granite
|
|
4
|
+
module Form
|
|
5
|
+
module Model
|
|
6
|
+
module Associations
|
|
7
|
+
module PersistenceAdapters
|
|
8
|
+
class ActiveRecord < Base
|
|
9
|
+
TYPES = {
|
|
10
|
+
integer: Integer,
|
|
11
|
+
float: Float,
|
|
12
|
+
decimal: BigDecimal,
|
|
13
|
+
datetime: Time,
|
|
14
|
+
timestamp: Time,
|
|
15
|
+
time: Time,
|
|
16
|
+
date: Date,
|
|
17
|
+
text: String,
|
|
18
|
+
string: String,
|
|
19
|
+
binary: String,
|
|
20
|
+
boolean: Boolean
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
alias_method :data_type, :data_source
|
|
24
|
+
|
|
25
|
+
def build(attributes)
|
|
26
|
+
data_source.new(attributes)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def persist(object, raise_error: false)
|
|
30
|
+
raise_error ? object.save! : object.save
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def scope(owner, source)
|
|
34
|
+
scope = data_source.unscoped
|
|
35
|
+
|
|
36
|
+
if scope_proc
|
|
37
|
+
scope = if scope_proc.arity.zero?
|
|
38
|
+
scope.instance_exec(&scope_proc)
|
|
39
|
+
else
|
|
40
|
+
scope.instance_exec(owner, &scope_proc)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
scope.where(primary_key => source)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def identify(object)
|
|
48
|
+
object[primary_key] if object
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def primary_key
|
|
52
|
+
@primary_key ||= :id
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def primary_key_type
|
|
56
|
+
column = data_source.columns_hash[primary_key.to_s]
|
|
57
|
+
TYPES[column.type]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def referenced_proxy(association)
|
|
61
|
+
ReferencedProxy.new(association)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module Granite
|
|
2
|
+
module Form
|
|
3
|
+
module Model
|
|
4
|
+
module Associations
|
|
5
|
+
module PersistenceAdapters
|
|
6
|
+
class Base
|
|
7
|
+
attr_reader :data_source, :primary_key, :scope_proc
|
|
8
|
+
|
|
9
|
+
def initialize(data_source, primary_key, scope_proc = nil)
|
|
10
|
+
@data_source = data_source
|
|
11
|
+
@primary_key = primary_key
|
|
12
|
+
@scope_proc = scope_proc
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def build(_attributes)
|
|
16
|
+
raise NotImplementedError, 'Should be implemented in inhereted adapter. Build new instance of data object by attributes'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def persist(_object, *)
|
|
20
|
+
raise NotImplementedError, 'Should be implemented in inhereted adapter. Build new instance of data object by attributes'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def scope(_owner, _source)
|
|
24
|
+
raise NotImplementedError, 'Should be implemented in inhereted adapter. Better to be Enumerable'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def find_one(owner, identificator)
|
|
28
|
+
scope(owner, identificator).first
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def find_all(owner, identificators)
|
|
32
|
+
scope(owner, identificators).to_a
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def identify(_object)
|
|
36
|
+
raise NotImplementedError, 'Should be implemented in inhereted adapter. Field to be used as primary_key for object'
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def data_type
|
|
40
|
+
raise NotImplementedError, 'Should be implemented in inhereted adapter. Type of data object for type_check'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def primary_key_type
|
|
44
|
+
raise NotImplementedError, 'Should be implemented in inhereted adapter. Ruby data type'
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def referenced_proxy
|
|
48
|
+
raise NotImplementedError, 'Should be implemented in inhereted adapter. Object to manage proxying of methods to scope.'
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module Granite
|
|
2
|
+
module Form
|
|
3
|
+
module Model
|
|
4
|
+
module Associations
|
|
5
|
+
class ReferencesAny < Base
|
|
6
|
+
def scope(source = read_source)
|
|
7
|
+
reflection.persistence_adapter.scope(owner, source)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def read_source
|
|
13
|
+
attribute.read_before_type_cast
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def write_source(value)
|
|
17
|
+
attribute.write_value value
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def attribute
|
|
21
|
+
@attribute ||= owner.attribute(reflection.reference_key)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def build_object(attributes)
|
|
25
|
+
reflection.persistence_adapter.build(attributes)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def persist_object(object, **options)
|
|
29
|
+
reflection.persistence_adapter.persist(object, **options)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def matches_type?(object)
|
|
33
|
+
object.is_a?(reflection.persistence_adapter.data_type)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def raise_type_mismatch(object)
|
|
37
|
+
raise AssociationTypeMismatch.new(reflection.persistence_adapter.data_type, object.class)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
module Granite
|
|
2
|
+
module Form
|
|
3
|
+
module Model
|
|
4
|
+
module Associations
|
|
5
|
+
class ReferencesMany < ReferencesAny
|
|
6
|
+
def build(attributes = {})
|
|
7
|
+
append([build_object(attributes)]).last
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def create(attributes = {})
|
|
11
|
+
object = build(attributes)
|
|
12
|
+
persist_object(object)
|
|
13
|
+
object
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create!(attributes = {})
|
|
17
|
+
object = build(attributes)
|
|
18
|
+
persist_object(object, raise_error: true)
|
|
19
|
+
object
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def apply_changes
|
|
23
|
+
target.all? do |object|
|
|
24
|
+
if object
|
|
25
|
+
if object.marked_for_destruction? && reflection.autosave?
|
|
26
|
+
object.destroy
|
|
27
|
+
elsif object.new_record? || (reflection.autosave? && object.changed?)
|
|
28
|
+
persist_object(object)
|
|
29
|
+
else
|
|
30
|
+
true
|
|
31
|
+
end
|
|
32
|
+
else
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def target=(object)
|
|
39
|
+
loaded!
|
|
40
|
+
@target = object.to_a
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def load_target
|
|
44
|
+
source = read_source
|
|
45
|
+
source.present? ? reflection.persistence_adapter.find_all(owner, source) : default
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def default
|
|
49
|
+
return [] if evar_loaded?
|
|
50
|
+
|
|
51
|
+
default = Array.wrap(reflection.default(owner))
|
|
52
|
+
|
|
53
|
+
return [] unless default
|
|
54
|
+
|
|
55
|
+
if default.all? { |object| object.is_a?(reflection.persistence_adapter.data_type) }
|
|
56
|
+
default
|
|
57
|
+
elsif default.all? { |object| object.is_a?(Hash) }
|
|
58
|
+
default.map { |attributes| build_object(attributes) }
|
|
59
|
+
else
|
|
60
|
+
reflection.persistence_adapter.find_all(owner, default)
|
|
61
|
+
end || []
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def reader(force_reload = false)
|
|
65
|
+
reload if force_reload
|
|
66
|
+
@proxy ||= reflection.persistence_adapter.referenced_proxy(self)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def replace(objects)
|
|
70
|
+
loaded!
|
|
71
|
+
transaction do
|
|
72
|
+
clear
|
|
73
|
+
append objects
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
alias_method :writer, :replace
|
|
78
|
+
|
|
79
|
+
def concat(*objects)
|
|
80
|
+
append objects.flatten
|
|
81
|
+
reader
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def clear
|
|
85
|
+
attribute.pollute do
|
|
86
|
+
write_source([])
|
|
87
|
+
end
|
|
88
|
+
reload.empty?
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def identify
|
|
92
|
+
target.map { |obj| reflection.persistence_adapter.identify(obj) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def append(objects)
|
|
98
|
+
attribute.pollute do
|
|
99
|
+
objects.each do |object|
|
|
100
|
+
next if target.include?(object)
|
|
101
|
+
raise_type_mismatch(object) unless matches_type?(object)
|
|
102
|
+
|
|
103
|
+
target.push(object)
|
|
104
|
+
write_source(identify)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
target
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
module Granite
|
|
2
|
+
module Form
|
|
3
|
+
module Model
|
|
4
|
+
module Associations
|
|
5
|
+
class ReferencesOne < ReferencesAny
|
|
6
|
+
def build(attributes = {})
|
|
7
|
+
replace(build_object(attributes))
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def create(attributes = {})
|
|
11
|
+
persist_object(build(attributes))
|
|
12
|
+
target
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def create!(attributes = {})
|
|
16
|
+
persist_object(build(attributes), raise_error: true)
|
|
17
|
+
target
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def apply_changes
|
|
21
|
+
if target
|
|
22
|
+
if target.marked_for_destruction? && reflection.autosave?
|
|
23
|
+
target.destroy
|
|
24
|
+
elsif target.new_record? || (reflection.autosave? && target.changed?)
|
|
25
|
+
persist_object(target)
|
|
26
|
+
else
|
|
27
|
+
true
|
|
28
|
+
end
|
|
29
|
+
else
|
|
30
|
+
true
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def target=(object)
|
|
35
|
+
loaded!
|
|
36
|
+
@target = object
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def load_target
|
|
40
|
+
source = read_source
|
|
41
|
+
source ? reflection.persistence_adapter.find_one(owner, source) : default
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def default
|
|
45
|
+
return if evar_loaded?
|
|
46
|
+
|
|
47
|
+
default = reflection.default(owner)
|
|
48
|
+
|
|
49
|
+
return unless default
|
|
50
|
+
|
|
51
|
+
case default
|
|
52
|
+
when reflection.persistence_adapter.data_type
|
|
53
|
+
default
|
|
54
|
+
when Hash
|
|
55
|
+
build_object(default)
|
|
56
|
+
else
|
|
57
|
+
reflection.persistence_adapter.find_one(owner, default)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def reader(force_reload = false)
|
|
62
|
+
reset if force_reload
|
|
63
|
+
target
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def replace(object)
|
|
67
|
+
raise_type_mismatch(object) unless object.nil? || matches_type?(object)
|
|
68
|
+
|
|
69
|
+
transaction do
|
|
70
|
+
attribute.pollute do
|
|
71
|
+
self.target = object
|
|
72
|
+
write_source identify
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
target
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
alias_method :writer, :replace
|
|
80
|
+
|
|
81
|
+
def identify
|
|
82
|
+
reflection.persistence_adapter.identify(target)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|