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,42 @@
|
|
|
1
|
+
module Granite
|
|
2
|
+
module Form
|
|
3
|
+
class Config
|
|
4
|
+
include Singleton
|
|
5
|
+
|
|
6
|
+
attr_accessor :include_root_in_json, :i18n_scope, :logger, :primary_attribute, :base_class, :base_concern,
|
|
7
|
+
:_normalizers, :_typecasters
|
|
8
|
+
|
|
9
|
+
def self.delegated
|
|
10
|
+
public_instance_methods - superclass.public_instance_methods - Singleton.public_instance_methods
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@include_root_in_json = false
|
|
15
|
+
@i18n_scope = :granite
|
|
16
|
+
@logger = Logger.new(STDERR)
|
|
17
|
+
@primary_attribute = :id
|
|
18
|
+
@_normalizers = {}
|
|
19
|
+
@_typecasters = {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def normalizer(name, &block)
|
|
23
|
+
if block
|
|
24
|
+
_normalizers[name.to_sym] = block
|
|
25
|
+
else
|
|
26
|
+
_normalizers[name.to_sym] or raise NormalizerMissing, name
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def typecaster(*classes, &block)
|
|
31
|
+
classes = classes.flatten
|
|
32
|
+
if block
|
|
33
|
+
_typecasters[classes.first.to_s.camelize] = block
|
|
34
|
+
else
|
|
35
|
+
_typecasters[classes.detect do |klass|
|
|
36
|
+
_typecasters[klass.to_s.camelize]
|
|
37
|
+
end.to_s.camelize] or raise TypecasterMissing, classes
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
module Granite
|
|
2
|
+
module Form
|
|
3
|
+
class Error < StandardError
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
class NotFound < Error
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Backported from active_model 5
|
|
10
|
+
class ValidationError < Error
|
|
11
|
+
attr_reader :model
|
|
12
|
+
|
|
13
|
+
def initialize(model)
|
|
14
|
+
@model = model
|
|
15
|
+
errors = @model.errors.full_messages.join(', ')
|
|
16
|
+
super(I18n.t(:"#{@model.class.i18n_scope}.errors.messages.model_invalid", errors: errors, default: :'errors.messages.model_invalid'))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class UnsavableObject < Error
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class UndestroyableObject < Error
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class ObjectNotSaved < Error
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class ObjectNotDestroyed < Error
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class AssociationChangesNotApplied < Error
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class AssociationTypeMismatch < Error
|
|
36
|
+
def initialize(expected, got)
|
|
37
|
+
super "Expected `#{expected}` (##{expected.object_id}), but got `#{got}` (##{got.object_id})"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class ObjectNotFound < Error
|
|
42
|
+
def initialize(object, association_name, record_id)
|
|
43
|
+
message = "Couldn't find #{object.class.reflect_on_association(association_name).klass.name}" \
|
|
44
|
+
"with #{object.respond_to?(:_primary_name) ? object._primary_name : 'id'} = #{record_id} for #{object.inspect}"
|
|
45
|
+
super message
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class TooManyObjects < Error
|
|
50
|
+
def initialize(limit, actual_size)
|
|
51
|
+
super "Maximum #{limit} objects are allowed. Got #{actual_size} objects instead."
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class UndefinedPrimaryAttribute < Error
|
|
56
|
+
def initialize(klass, association_name)
|
|
57
|
+
super <<-MESSAGE
|
|
58
|
+
Undefined primary attribute for `#{association_name}` in #{klass}.
|
|
59
|
+
It is required for embeds_many nested attributes proper operation.
|
|
60
|
+
You can define this association as:
|
|
61
|
+
|
|
62
|
+
embeds_many :#{association_name} do
|
|
63
|
+
primary :attribute_name
|
|
64
|
+
end
|
|
65
|
+
MESSAGE
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class NormalizerMissing < NoMethodError
|
|
70
|
+
def initialize(name)
|
|
71
|
+
super <<-MESSAGE
|
|
72
|
+
Could not find normalizer `:#{name}`
|
|
73
|
+
You can define it with:
|
|
74
|
+
|
|
75
|
+
Granite::Form.normalizer(:#{name}) do |value, options|
|
|
76
|
+
# do some staff with value and options
|
|
77
|
+
end
|
|
78
|
+
MESSAGE
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class TypecasterMissing < NoMethodError
|
|
83
|
+
def initialize(*classes)
|
|
84
|
+
classes = classes.flatten
|
|
85
|
+
super <<-MESSAGE
|
|
86
|
+
Could not find typecaster for #{classes}
|
|
87
|
+
You can define it with:
|
|
88
|
+
|
|
89
|
+
Granite::Form.typecaster('#{classes.first}') do |value|
|
|
90
|
+
# do some staff with value and options
|
|
91
|
+
end
|
|
92
|
+
MESSAGE
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
class PersistenceAdapterMissing < NoMethodError
|
|
97
|
+
def initialize(data_source)
|
|
98
|
+
super <<-MESSAGE
|
|
99
|
+
Could not find persistence adapter for #{data_source}
|
|
100
|
+
You can define it with:
|
|
101
|
+
|
|
102
|
+
class #{data_source}
|
|
103
|
+
def self.granite_persistence_adapter
|
|
104
|
+
#{data_source}GraniteFormPersistenceAdapter
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
MESSAGE
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
class Boolean; end unless defined?(Boolean)
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require 'uuidtools'
|
|
5
|
+
rescue LoadError
|
|
6
|
+
nil
|
|
7
|
+
else
|
|
8
|
+
module Granite
|
|
9
|
+
module Form
|
|
10
|
+
class UUID < UUIDTools::UUID
|
|
11
|
+
def as_json(*_)
|
|
12
|
+
to_s
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_param
|
|
16
|
+
to_s
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.parse_string(value)
|
|
20
|
+
return nil if value.length.zero?
|
|
21
|
+
if value.length == 36
|
|
22
|
+
parse value
|
|
23
|
+
elsif value.length == 32
|
|
24
|
+
parse_hexdigest value
|
|
25
|
+
else
|
|
26
|
+
parse_raw value
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def inspect
|
|
31
|
+
"#<Granite::Form::UUID:#{self}>"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
module Granite
|
|
2
|
+
module Form
|
|
3
|
+
module Model
|
|
4
|
+
module Associations
|
|
5
|
+
class Base
|
|
6
|
+
attr_accessor :owner, :reflection
|
|
7
|
+
delegate :macro, :collection?, to: :reflection
|
|
8
|
+
|
|
9
|
+
def initialize(owner, reflection)
|
|
10
|
+
@owner = owner
|
|
11
|
+
@reflection = reflection
|
|
12
|
+
@evar_loaded = owner.persisted?
|
|
13
|
+
reset
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def reset
|
|
17
|
+
@loaded = false
|
|
18
|
+
@target = nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def evar_loaded?
|
|
22
|
+
!!@evar_loaded
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def loaded?
|
|
26
|
+
!!@loaded
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def loaded!
|
|
30
|
+
@evar_loaded = true
|
|
31
|
+
@loaded = true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def target
|
|
35
|
+
return @target if loaded?
|
|
36
|
+
self.target = load_target
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def reload
|
|
40
|
+
reset
|
|
41
|
+
target
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def apply_changes!
|
|
45
|
+
apply_changes or raise Granite::Form::AssociationChangesNotApplied
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def callback(name, object)
|
|
49
|
+
evaluator = reflection.options[name]
|
|
50
|
+
return true unless evaluator
|
|
51
|
+
|
|
52
|
+
if evaluator.is_a?(Proc)
|
|
53
|
+
if evaluator.arity == 1
|
|
54
|
+
owner.instance_exec(object, &evaluator)
|
|
55
|
+
else
|
|
56
|
+
evaluator.call(owner, object)
|
|
57
|
+
end
|
|
58
|
+
else
|
|
59
|
+
owner.send(evaluator, object)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def transaction
|
|
64
|
+
data = read_source.deep_dup
|
|
65
|
+
yield
|
|
66
|
+
rescue StandardError => e
|
|
67
|
+
write_source data
|
|
68
|
+
reload
|
|
69
|
+
raise e
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def inspect
|
|
73
|
+
"#<#{reflection.macro.to_s.camelize} #{target.inspect.truncate(50, omission: collection? ? '...]' : '...')}>"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def read_source
|
|
79
|
+
reflection.read_source owner
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def write_source(value)
|
|
83
|
+
reflection.write_source owner, value
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def target_for_inspect
|
|
87
|
+
if value.length > 50
|
|
88
|
+
"#{value[0..50]}...".inspect
|
|
89
|
+
else
|
|
90
|
+
value.inspect
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Granite
|
|
2
|
+
module Form
|
|
3
|
+
module Model
|
|
4
|
+
module Associations
|
|
5
|
+
module Collection
|
|
6
|
+
class Proxy
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
delegate :target, :save, :save!, :loaded?, :reload, :clear, :concat, to: :@association
|
|
10
|
+
delegate :each, :size, :length, :first, :last, :empty?, :many?, :==, :dup, to: :target
|
|
11
|
+
alias_method :<<, :concat
|
|
12
|
+
alias_method :push, :concat
|
|
13
|
+
|
|
14
|
+
def initialize(association)
|
|
15
|
+
@association = association
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_ary
|
|
19
|
+
dup
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
alias_method :to_a, :to_ary
|
|
23
|
+
|
|
24
|
+
def inspect
|
|
25
|
+
entries = target.take(10).map!(&:inspect)
|
|
26
|
+
entries[10] = '...' if target.size > 10
|
|
27
|
+
|
|
28
|
+
"#<#{self.class.name.demodulize} [#{entries.join(', ')}]>"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Granite
|
|
2
|
+
module Form
|
|
3
|
+
module Model
|
|
4
|
+
module Associations
|
|
5
|
+
class EmbedsAny < Base
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def build_object(attributes)
|
|
9
|
+
reflection.klass.new(attributes)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def embed_object(object)
|
|
13
|
+
object.instance_variable_set(:@embedder, owner)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
module Granite
|
|
2
|
+
module Form
|
|
3
|
+
module Model
|
|
4
|
+
module Associations
|
|
5
|
+
class EmbedsMany < EmbedsAny
|
|
6
|
+
def build(attributes = {})
|
|
7
|
+
push_object(build_object(attributes))
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def create(attributes = {})
|
|
11
|
+
build(attributes).tap(&:save)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create!(attributes = {})
|
|
15
|
+
build(attributes).tap(&:save!)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def destroyed
|
|
19
|
+
@destroyed ||= []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def apply_changes
|
|
23
|
+
result = target.map do |object|
|
|
24
|
+
object.destroyed? || object.marked_for_destruction? ? object.destroy : object.save
|
|
25
|
+
end.all?
|
|
26
|
+
@destroyed = target.select(&:destroyed?)
|
|
27
|
+
target.delete_if(&:destroyed?)
|
|
28
|
+
result
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def target=(objects)
|
|
32
|
+
objects.each { |object| setup_performers! object }
|
|
33
|
+
loaded!
|
|
34
|
+
@target = objects
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def load_target
|
|
38
|
+
source = read_source
|
|
39
|
+
source.present? ? reflection.klass.instantiate_collection(source) : default
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def default
|
|
43
|
+
unless evar_loaded?
|
|
44
|
+
default = Array.wrap(reflection.default(owner))
|
|
45
|
+
if default.present?
|
|
46
|
+
collection = if default.all? { |object| object.is_a?(reflection.klass) }
|
|
47
|
+
default
|
|
48
|
+
else
|
|
49
|
+
default.map do |attributes|
|
|
50
|
+
reflection.klass.with_sanitize(false) do
|
|
51
|
+
build_object(attributes)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
collection.map { |object| object.send(:clear_changes_information) } if reflection.klass.dirty?
|
|
56
|
+
collection
|
|
57
|
+
end
|
|
58
|
+
end || []
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def reset
|
|
62
|
+
super
|
|
63
|
+
@target = []
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def clear
|
|
67
|
+
begin
|
|
68
|
+
transaction { target.all?(&:destroy!) }
|
|
69
|
+
rescue Granite::Form::ObjectNotDestroyed
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
reload.empty?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def reader(force_reload = false)
|
|
76
|
+
reload if force_reload
|
|
77
|
+
@proxy ||= Collection::Embedded.new self
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def replace(objects)
|
|
81
|
+
transaction do
|
|
82
|
+
clear
|
|
83
|
+
append(objects) or raise Granite::Form::AssociationChangesNotApplied
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
alias_method :writer, :replace
|
|
88
|
+
|
|
89
|
+
def concat(*objects)
|
|
90
|
+
append objects.flatten
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def read_source
|
|
96
|
+
super || []
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def append(objects)
|
|
100
|
+
objects.each do |object|
|
|
101
|
+
raise AssociationTypeMismatch.new(reflection.klass, object.class) unless object && object.is_a?(reflection.klass)
|
|
102
|
+
push_object object
|
|
103
|
+
end
|
|
104
|
+
result = owner.persisted? ? apply_changes : true
|
|
105
|
+
result && target
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def push_object(object)
|
|
109
|
+
setup_performers! object
|
|
110
|
+
target[target.size] = object
|
|
111
|
+
object
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def setup_performers!(object)
|
|
115
|
+
embed_object(object)
|
|
116
|
+
callback(:before_add, object)
|
|
117
|
+
|
|
118
|
+
association = self
|
|
119
|
+
|
|
120
|
+
object.define_create do
|
|
121
|
+
source = association.send(:read_source)
|
|
122
|
+
index = association.target
|
|
123
|
+
.select { |one| one.persisted? || one.equal?(self) }
|
|
124
|
+
.index { |one| one.equal?(self) }
|
|
125
|
+
|
|
126
|
+
source.insert(index, attributes)
|
|
127
|
+
association.send(:write_source, source)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
object.define_update do
|
|
131
|
+
source = association.send(:read_source)
|
|
132
|
+
index = association.target.select(&:persisted?).index { |one| one.equal?(self) }
|
|
133
|
+
|
|
134
|
+
source[index] = attributes
|
|
135
|
+
association.send(:write_source, source)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
object.define_destroy do
|
|
139
|
+
source = association.send(:read_source)
|
|
140
|
+
index = association.target.select(&:persisted?).index { |one| one.equal?(self) }
|
|
141
|
+
|
|
142
|
+
source.delete_at(index) if index
|
|
143
|
+
association.send(:write_source, source)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
callback(:after_add, object)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
module Granite
|
|
2
|
+
module Form
|
|
3
|
+
module Model
|
|
4
|
+
module Associations
|
|
5
|
+
class EmbedsOne < EmbedsAny
|
|
6
|
+
attr_reader :destroyed
|
|
7
|
+
|
|
8
|
+
def build(attributes = {})
|
|
9
|
+
self.target = build_object(attributes)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create(attributes = {})
|
|
13
|
+
build(attributes).tap(&:save)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create!(attributes = {})
|
|
17
|
+
build(attributes).tap(&:save!)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def apply_changes
|
|
21
|
+
if target
|
|
22
|
+
if target.destroyed? || target.marked_for_destruction?
|
|
23
|
+
@destroyed = target
|
|
24
|
+
clear
|
|
25
|
+
else
|
|
26
|
+
target.save
|
|
27
|
+
end
|
|
28
|
+
else
|
|
29
|
+
true
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def target=(object)
|
|
34
|
+
if object
|
|
35
|
+
callback(:before_add, object)
|
|
36
|
+
setup_performers! object
|
|
37
|
+
end
|
|
38
|
+
loaded!
|
|
39
|
+
@target = object
|
|
40
|
+
callback(:after_add, object) if object
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def load_target
|
|
44
|
+
source = read_source
|
|
45
|
+
source ? reflection.klass.instantiate(source) : default
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def default
|
|
49
|
+
return if evar_loaded?
|
|
50
|
+
|
|
51
|
+
default = reflection.default(owner)
|
|
52
|
+
|
|
53
|
+
return unless default
|
|
54
|
+
|
|
55
|
+
object = if default.is_a?(reflection.klass)
|
|
56
|
+
default
|
|
57
|
+
else
|
|
58
|
+
reflection.klass.with_sanitize(false) do
|
|
59
|
+
build_object(default)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
object.send(:clear_changes_information) if reflection.klass.dirty?
|
|
63
|
+
object
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def clear
|
|
67
|
+
target.try(:destroy)
|
|
68
|
+
reload.nil?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def reader(force_reload = false)
|
|
72
|
+
reload if force_reload
|
|
73
|
+
target
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def replace(object)
|
|
77
|
+
if object
|
|
78
|
+
raise AssociationTypeMismatch.new(reflection.klass, object.class) unless object.is_a?(reflection.klass)
|
|
79
|
+
transaction do
|
|
80
|
+
clear
|
|
81
|
+
self.target = object
|
|
82
|
+
apply_changes! if owner.persisted?
|
|
83
|
+
end
|
|
84
|
+
else
|
|
85
|
+
clear
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
target
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
alias_method :writer, :replace
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def setup_performers!(object)
|
|
96
|
+
embed_object(object)
|
|
97
|
+
association = self
|
|
98
|
+
|
|
99
|
+
object.define_save do
|
|
100
|
+
association.send(:write_source, attributes)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
object.define_destroy do
|
|
104
|
+
association.send(:write_source, nil)
|
|
105
|
+
true
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|