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