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,204 @@
|
|
|
1
|
+
require 'granite/form/model/attributes/reflections/base'
|
|
2
|
+
require 'granite/form/model/attributes/reflections/attribute'
|
|
3
|
+
require 'granite/form/model/attributes/reflections/collection'
|
|
4
|
+
require 'granite/form/model/attributes/reflections/dictionary'
|
|
5
|
+
|
|
6
|
+
require 'granite/form/model/attributes/base'
|
|
7
|
+
require 'granite/form/model/attributes/attribute'
|
|
8
|
+
require 'granite/form/model/attributes/collection'
|
|
9
|
+
require 'granite/form/model/attributes/dictionary'
|
|
10
|
+
|
|
11
|
+
module Granite
|
|
12
|
+
module Form
|
|
13
|
+
module Model
|
|
14
|
+
module Attributes
|
|
15
|
+
extend ActiveSupport::Concern
|
|
16
|
+
|
|
17
|
+
included do
|
|
18
|
+
class_attribute :_attributes, :_attribute_aliases, :_sanitize, instance_reader: false, instance_writer: false
|
|
19
|
+
self._attributes = {}
|
|
20
|
+
self._attribute_aliases = {}
|
|
21
|
+
self._sanitize = true
|
|
22
|
+
|
|
23
|
+
delegate :attribute_names, :has_attribute?, to: 'self.class'
|
|
24
|
+
|
|
25
|
+
%w[attribute collection dictionary].each do |kind|
|
|
26
|
+
define_singleton_method kind do |*args, &block|
|
|
27
|
+
add_attribute("Granite::Form::Model::Attributes::Reflections::#{kind.camelize}".constantize, *args, &block)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module ClassMethods
|
|
33
|
+
def add_attribute(reflection_class, *args, &block)
|
|
34
|
+
reflection = reflection_class.build(self, generated_attributes_methods, *args, &block)
|
|
35
|
+
self._attributes = _attributes.merge(reflection.name => reflection)
|
|
36
|
+
should_define_dirty = (dirty? && reflection_class != Granite::Form::Model::Attributes::Reflections::Base)
|
|
37
|
+
define_dirty(reflection.name, generated_attributes_methods) if should_define_dirty
|
|
38
|
+
reflection
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def alias_attribute(alias_name, attribute_name)
|
|
42
|
+
reflection = reflect_on_attribute(attribute_name)
|
|
43
|
+
raise ArgumentError, "Unable to alias undefined attribute `#{attribute_name}` on #{self}" unless reflection
|
|
44
|
+
raise ArgumentError, "Unable to alias base attribute `#{attribute_name}`" if reflection.class == Granite::Form::Model::Attributes::Reflections::Base
|
|
45
|
+
reflection.class.generate_methods alias_name, generated_attributes_methods
|
|
46
|
+
self._attribute_aliases = _attribute_aliases.merge(alias_name.to_s => reflection.name)
|
|
47
|
+
define_dirty alias_name, generated_attributes_methods if dirty?
|
|
48
|
+
reflection
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def reflect_on_attribute(name)
|
|
52
|
+
name = name.to_s
|
|
53
|
+
_attributes[_attribute_aliases[name] || name]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def has_attribute?(name) # rubocop:disable Naming/PredicateName
|
|
57
|
+
name = name.to_s
|
|
58
|
+
_attributes.key?(_attribute_aliases[name] || name)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def attribute_names(include_associations = true)
|
|
62
|
+
if include_associations
|
|
63
|
+
_attributes.keys
|
|
64
|
+
else
|
|
65
|
+
_attributes.map do |name, attribute|
|
|
66
|
+
name unless attribute.class == Granite::Form::Model::Attributes::Reflections::Base
|
|
67
|
+
end.compact
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def inspect
|
|
72
|
+
"#{original_inspect}(#{attributes_for_inspect.presence || 'no attributes'})"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def dirty?
|
|
76
|
+
false
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def with_sanitize(value)
|
|
80
|
+
previous_sanitize = _sanitize
|
|
81
|
+
self._sanitize = value
|
|
82
|
+
yield
|
|
83
|
+
ensure
|
|
84
|
+
self._sanitize = previous_sanitize
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def original_inspect
|
|
90
|
+
Object.method(:inspect).unbind.bind(self).call
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def attributes_for_inspect
|
|
94
|
+
attribute_names(false).map do |name|
|
|
95
|
+
prefix = respond_to?(:_primary_name) && _primary_name == name ? '*' : ''
|
|
96
|
+
"#{prefix}#{_attributes[name].inspect_reflection}"
|
|
97
|
+
end.join(', ')
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def generated_attributes_methods
|
|
101
|
+
@generated_attributes_methods ||=
|
|
102
|
+
const_set(:GeneratedAttributesMethods, Module.new)
|
|
103
|
+
.tap { |proxy| include proxy }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def inverted_attribute_aliases
|
|
107
|
+
@inverted_attribute_aliases ||=
|
|
108
|
+
_attribute_aliases.each.with_object({}) do |(alias_name, attribute_name), result|
|
|
109
|
+
(result[attribute_name] ||= []).push(alias_name)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def initialize(attrs = {})
|
|
115
|
+
assign_attributes attrs
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def ==(other)
|
|
119
|
+
super || other.instance_of?(self.class) && other.attributes(false) == attributes(false)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
alias_method :eql?, :==
|
|
123
|
+
|
|
124
|
+
def attribute(name)
|
|
125
|
+
reflection = self.class.reflect_on_attribute(name)
|
|
126
|
+
return unless reflection
|
|
127
|
+
initial_value = @initial_attributes.to_h.fetch(reflection.name, Granite::Form::UNDEFINED)
|
|
128
|
+
@_attributes ||= {}
|
|
129
|
+
@_attributes[reflection.name] ||= reflection.build_attribute(self, initial_value)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def write_attribute(name, value)
|
|
133
|
+
attribute(name).write(value)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
alias_method :[]=, :write_attribute
|
|
137
|
+
|
|
138
|
+
def read_attribute(name)
|
|
139
|
+
attribute(name).read
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
alias_method :[], :read_attribute
|
|
143
|
+
|
|
144
|
+
def read_attribute_before_type_cast(name)
|
|
145
|
+
attribute(name).read_before_type_cast
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def attribute_came_from_user?(name)
|
|
149
|
+
attribute(name).came_from_user?
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def attribute_present?(name)
|
|
153
|
+
attribute(name).value_present?
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def attributes(include_associations = true)
|
|
157
|
+
Hash[attribute_names(include_associations).map { |name| [name, read_attribute(name)] }]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def update(attrs)
|
|
161
|
+
assign_attributes(attrs)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
alias_method :update_attributes, :update
|
|
165
|
+
|
|
166
|
+
def assign_attributes(attrs)
|
|
167
|
+
attrs.each do |name, value|
|
|
168
|
+
name = name.to_s
|
|
169
|
+
sanitize_value = self.class._sanitize && name == self.class.primary_name
|
|
170
|
+
|
|
171
|
+
if respond_to?("#{name}=") && !sanitize_value
|
|
172
|
+
public_send("#{name}=", value)
|
|
173
|
+
else
|
|
174
|
+
logger.info("Ignoring #{sanitize_value ? 'primary' : 'undefined'} `#{name}` attribute value for #{self} during mass-assignment")
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
alias_method :attributes=, :assign_attributes
|
|
180
|
+
|
|
181
|
+
def inspect
|
|
182
|
+
"#<#{self.class.send(:original_inspect)} #{attributes_for_inspect.presence || '(no attributes)'}>"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def initialize_copy(_)
|
|
186
|
+
@initial_attributes = Hash[attribute_names.map do |name|
|
|
187
|
+
[name, read_attribute_before_type_cast(name)]
|
|
188
|
+
end]
|
|
189
|
+
@_attributes = nil
|
|
190
|
+
super
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
private
|
|
194
|
+
|
|
195
|
+
def attributes_for_inspect
|
|
196
|
+
attribute_names(false).map do |name|
|
|
197
|
+
prefix = self.class.primary_name == name ? '*' : ''
|
|
198
|
+
"#{prefix}#{attribute(name).inspect_attribute}"
|
|
199
|
+
end.join(', ')
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module Granite
|
|
2
|
+
module Form
|
|
3
|
+
module Model
|
|
4
|
+
# == Callbacks for Granite::Form::Model lifecycle
|
|
5
|
+
#
|
|
6
|
+
# Provides ActiveModel callbacks support for lifecycle
|
|
7
|
+
# actions.
|
|
8
|
+
#
|
|
9
|
+
# class Book
|
|
10
|
+
# include Granite::Form::Model
|
|
11
|
+
#
|
|
12
|
+
# attribute :id, Integer
|
|
13
|
+
# attribute :title, String
|
|
14
|
+
#
|
|
15
|
+
# define_save do
|
|
16
|
+
# REDIS.set(id, attributes.to_json)
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# define_destroy do
|
|
20
|
+
# REDIS.del(instance.id)
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# after_initialize :setup_id
|
|
24
|
+
# before_save :do_something
|
|
25
|
+
# around_update do |&block|
|
|
26
|
+
# ...
|
|
27
|
+
# block.call
|
|
28
|
+
# ...
|
|
29
|
+
# end
|
|
30
|
+
# after_destroy { ... }
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
module Callbacks
|
|
34
|
+
extend ActiveSupport::Concern
|
|
35
|
+
|
|
36
|
+
included do
|
|
37
|
+
extend ActiveModel::Callbacks
|
|
38
|
+
|
|
39
|
+
include ActiveModel::Validations::Callbacks
|
|
40
|
+
include Lifecycle
|
|
41
|
+
prepend PrependMethods
|
|
42
|
+
|
|
43
|
+
define_model_callbacks :initialize, only: :after
|
|
44
|
+
define_model_callbacks :save, :create, :update, :destroy
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
module PrependMethods
|
|
48
|
+
def initialize(*_)
|
|
49
|
+
super
|
|
50
|
+
run_callbacks :initialize
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def save_object(&block)
|
|
54
|
+
run_callbacks(:save) { super(&block) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def create_object(&block)
|
|
58
|
+
run_callbacks(:create) { super(&block) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def update_object(&block)
|
|
62
|
+
run_callbacks(:update) { super(&block) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def destroy_object(&block)
|
|
66
|
+
run_callbacks(:destroy) { super(&block) }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Granite
|
|
2
|
+
module Form
|
|
3
|
+
module Model
|
|
4
|
+
module Conventions
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
attr_reader :embedder
|
|
9
|
+
|
|
10
|
+
delegate :logger, to: Granite::Form
|
|
11
|
+
self.include_root_in_json = Granite::Form.include_root_in_json
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def persisted?
|
|
15
|
+
false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def new_record?
|
|
19
|
+
!persisted?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
alias_method :new_object?, :new_record?
|
|
23
|
+
|
|
24
|
+
module ClassMethods
|
|
25
|
+
def i18n_scope
|
|
26
|
+
Granite::Form.i18n_scope
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_ary
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def primary_name
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
module Granite
|
|
2
|
+
module Form
|
|
3
|
+
module Model
|
|
4
|
+
module Dirty
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
::Module.class_eval do
|
|
8
|
+
alias_method :unconcerned_append_features, :append_features
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
DIRTY_CLONE = ActiveModel::Dirty.clone
|
|
12
|
+
DIRTY_CLONE.class_eval do
|
|
13
|
+
def self.append_features(base)
|
|
14
|
+
unconcerned_append_features(base)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.included(_base); end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
include DIRTY_CLONE
|
|
21
|
+
|
|
22
|
+
included do
|
|
23
|
+
attribute_names(false).each do |name|
|
|
24
|
+
define_dirty name, generated_attributes_methods
|
|
25
|
+
end
|
|
26
|
+
_attribute_aliases.each_key do |name|
|
|
27
|
+
define_dirty name, generated_attributes_methods
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
if !method_defined?(:set_attribute_was) && !private_method_defined?(:set_attribute_was)
|
|
32
|
+
private def set_attribute_was(attr, old_value)
|
|
33
|
+
changed_attributes[attr] = old_value
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
unless method_defined?(:clear_changes_information)
|
|
38
|
+
if method_defined?(:reset_changes)
|
|
39
|
+
def clear_changes_information
|
|
40
|
+
reset_changes
|
|
41
|
+
end
|
|
42
|
+
else
|
|
43
|
+
def clear_changes_information
|
|
44
|
+
@previously_changed = nil
|
|
45
|
+
@changed_attributes = nil
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
unless method_defined?(:_read_attribute)
|
|
51
|
+
def _read_attribute(attr)
|
|
52
|
+
__send__(attr)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
module ClassMethods
|
|
57
|
+
def define_dirty(method, target = self)
|
|
58
|
+
reflection = reflect_on_attribute(method)
|
|
59
|
+
name = reflection ? reflection.name : method
|
|
60
|
+
|
|
61
|
+
%w[changed? change will_change! was
|
|
62
|
+
previously_changed? previous_change].each do |suffix|
|
|
63
|
+
target.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
64
|
+
def #{method}_#{suffix}
|
|
65
|
+
attribute_#{suffix} '#{name}'
|
|
66
|
+
end
|
|
67
|
+
RUBY
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
target.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
71
|
+
def restore_#{method}!
|
|
72
|
+
restore_attribute! '#{name}'
|
|
73
|
+
end
|
|
74
|
+
RUBY
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def dirty?
|
|
78
|
+
true
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
module Granite
|
|
2
|
+
module Form
|
|
3
|
+
module Model
|
|
4
|
+
# == Lifecycle methods for Granite::Form::Model
|
|
5
|
+
#
|
|
6
|
+
# Provides methods +save+ and +destroy+ and its bang variants.
|
|
7
|
+
# Also, patches +create+ and +update_attributes+ methods by adding
|
|
8
|
+
# save at the end.
|
|
9
|
+
#
|
|
10
|
+
# You can define save or destroy performers with <tt>define_<action></tt>
|
|
11
|
+
# methods. Create and update performers might be defined instead of
|
|
12
|
+
# save performer:
|
|
13
|
+
#
|
|
14
|
+
# class Book
|
|
15
|
+
# include Granite::Form::Model
|
|
16
|
+
# include Granite::Form::Model::Lifecycle
|
|
17
|
+
#
|
|
18
|
+
# attribute :id, Integer
|
|
19
|
+
# attribute :title, String
|
|
20
|
+
#
|
|
21
|
+
# define_save do # executes in the instance scope
|
|
22
|
+
# REDIS.set(id, attributes.to_json)
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# define_destroy do
|
|
26
|
+
# REDIS.del(id)
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# class Author
|
|
31
|
+
# include Granite::Form::Model
|
|
32
|
+
# include Granite::Form::Model::Lifecycle
|
|
33
|
+
#
|
|
34
|
+
# attribute :id, Integer
|
|
35
|
+
# attribute :name, String
|
|
36
|
+
#
|
|
37
|
+
# define_create do # will be called on create only
|
|
38
|
+
# REDIS.sadd('author_ids', id)
|
|
39
|
+
# REDIS.set(id, attributes.to_json)
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# define_update do # will be called on update only
|
|
43
|
+
# REDIS.set(id, attributes.to_json)
|
|
44
|
+
# end
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
# In case of undefined performer Granite::Form::UnsavableObject
|
|
48
|
+
# or Granite::Form::UndestroyableObject will be raised respectively.
|
|
49
|
+
#
|
|
50
|
+
# If performers was not defined in model, they cat be passed as
|
|
51
|
+
# blocks to `save`, `update` and `destroy` methods:
|
|
52
|
+
#
|
|
53
|
+
# authos.save { REDIS.set(id, attributes.to_json) }
|
|
54
|
+
# authos.update { REDIS.set(id, attributes.to_json) }
|
|
55
|
+
# authos.destroy { REDIS.del(id) }
|
|
56
|
+
#
|
|
57
|
+
# Save and destroy processes acts almost the save way as
|
|
58
|
+
# ActiveRecord's (with +persisted?+ and +destroyed?+ methods
|
|
59
|
+
# affecting).
|
|
60
|
+
#
|
|
61
|
+
module Lifecycle
|
|
62
|
+
extend ActiveSupport::Concern
|
|
63
|
+
|
|
64
|
+
included do
|
|
65
|
+
include Persistence
|
|
66
|
+
|
|
67
|
+
class_attribute(*%i[save create update destroy].map { |action| "_#{action}_performer" })
|
|
68
|
+
private(*%i[save create update destroy].map { |action| "_#{action}_performer=" })
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
module ClassMethods
|
|
72
|
+
# <tt>define_<action></tt> methods define performers for lifecycle
|
|
73
|
+
# actions. Every action block must return boolean result, which
|
|
74
|
+
# would mean the action success. If action performed unsuccessfully
|
|
75
|
+
# Granite::Form::ObjectNotSaved or Granite::Form::ObjectNotDestroyed will
|
|
76
|
+
# be raised respectively in case of bang methods using.
|
|
77
|
+
#
|
|
78
|
+
# class Author
|
|
79
|
+
# define_create { true }
|
|
80
|
+
# end
|
|
81
|
+
#
|
|
82
|
+
# Author.new.save # => true
|
|
83
|
+
# Author.new.save! # => true
|
|
84
|
+
#
|
|
85
|
+
# class Author
|
|
86
|
+
# define_create { false }
|
|
87
|
+
# end
|
|
88
|
+
#
|
|
89
|
+
# Author.new.save # => false
|
|
90
|
+
# Author.new.save! # => Granite::Form::ObjectNotSaved
|
|
91
|
+
#
|
|
92
|
+
# Also performers blocks are executed in the instance context, but
|
|
93
|
+
# instance also passed as argument
|
|
94
|
+
#
|
|
95
|
+
# define_update do |instance|
|
|
96
|
+
# instance.attributes.to_json
|
|
97
|
+
# end
|
|
98
|
+
#
|
|
99
|
+
# +define_create+ and +define_update+ performers has higher priority
|
|
100
|
+
# than +define_save+.
|
|
101
|
+
#
|
|
102
|
+
# class Author
|
|
103
|
+
# define_update { ... }
|
|
104
|
+
# define_save { ... }
|
|
105
|
+
# end
|
|
106
|
+
#
|
|
107
|
+
# author = Author.create # using define_save performer
|
|
108
|
+
# author.update_attributes(...) # using define_update performer
|
|
109
|
+
#
|
|
110
|
+
%i[save create update destroy].each do |action|
|
|
111
|
+
define_method "define_#{action}" do |&block|
|
|
112
|
+
send("_#{action}_performer=", block)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Initializes new instance with attributes passed and calls +save+
|
|
117
|
+
# on it. Returns instance in any case.
|
|
118
|
+
#
|
|
119
|
+
def create(*args)
|
|
120
|
+
new(*args).tap(&:save)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Initializes new instance with attributes passed and calls +save!+
|
|
124
|
+
# on it. Returns instance in case of success and raises Granite::Form::ValidationError
|
|
125
|
+
# or Granite::Form::ObjectNotSaved in case of validation or saving fail respectively.
|
|
126
|
+
#
|
|
127
|
+
def create!(*args)
|
|
128
|
+
new(*args).tap(&:save!)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# <tt>define_<action></tt> on instance level works the same
|
|
133
|
+
# way as class <tt>define_<action></tt> methods, but defines
|
|
134
|
+
# performers for instance only
|
|
135
|
+
#
|
|
136
|
+
# user.define_save do
|
|
137
|
+
# REDIS.set(id, attributes.to_json)
|
|
138
|
+
# end
|
|
139
|
+
# user.save! # => will use instance-level performer
|
|
140
|
+
#
|
|
141
|
+
%i[save create update destroy].each do |action|
|
|
142
|
+
define_method "define_#{action}" do |&block|
|
|
143
|
+
send("_#{action}_performer=", block)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Assigns passed attributes and calls +save+
|
|
148
|
+
# Returns true or false in case of successful or unsuccessful
|
|
149
|
+
# saving respectively.
|
|
150
|
+
#
|
|
151
|
+
# author.update(name: 'Donald')
|
|
152
|
+
#
|
|
153
|
+
# If update performer is not defined with `define_update`
|
|
154
|
+
# or `define_save`, it raises Granite::Form::UnsavableObject.
|
|
155
|
+
# Also save performer block might be passed instead of in-class
|
|
156
|
+
# performer definition:
|
|
157
|
+
#
|
|
158
|
+
# author.update(name: 'Donald') { REDIS.set(id, attributes.to_json) }
|
|
159
|
+
#
|
|
160
|
+
def update(attributes, &block)
|
|
161
|
+
assign_attributes(attributes) && save(&block)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
alias_method :update_attributes, :update
|
|
165
|
+
|
|
166
|
+
# Assigns passed attributes and calls +save!+
|
|
167
|
+
# Returns true in case of success and raises Granite::Form::ValidationError
|
|
168
|
+
# or Granite::Form::ObjectNotSaved in case of validation or
|
|
169
|
+
# saving fail respectively.
|
|
170
|
+
#
|
|
171
|
+
# author.update!(name: 'Donald')
|
|
172
|
+
#
|
|
173
|
+
# If update performer is not defined with `define_update`
|
|
174
|
+
# or `define_save`, it raises Granite::Form::UnsavableObject.
|
|
175
|
+
# Also save performer block might be passed instead of in-class
|
|
176
|
+
# performer definition:
|
|
177
|
+
#
|
|
178
|
+
# author.update!(name: 'Donald') { REDIS.set(id, attributes.to_json) }
|
|
179
|
+
#
|
|
180
|
+
def update!(attributes, &block)
|
|
181
|
+
assign_attributes(attributes) && save!(&block)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
alias_method :update_attributes!, :update!
|
|
185
|
+
|
|
186
|
+
# # Saves object by calling save performer defined with +define_save+,
|
|
187
|
+
# +define_create+ or +define_update+ methods.
|
|
188
|
+
# Returns true or false in case of successful
|
|
189
|
+
# or unsuccessful saving respectively. Changes +persisted?+ to true
|
|
190
|
+
#
|
|
191
|
+
# author.save
|
|
192
|
+
#
|
|
193
|
+
# If save performer is not defined with `define_update` or
|
|
194
|
+
# `define_create` or `define_save`, it raises Granite::Form::UnsavableObject.
|
|
195
|
+
# Also save performer block might be passed instead of in-class
|
|
196
|
+
# performer definition:
|
|
197
|
+
#
|
|
198
|
+
# author.save { REDIS.set(id, attributes.to_json) }
|
|
199
|
+
#
|
|
200
|
+
def save(_options = {}, &block)
|
|
201
|
+
raise Granite::Form::UnsavableObject unless block || savable?
|
|
202
|
+
valid? && save_object(&block)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Saves object by calling save performer defined with +define_save+,
|
|
206
|
+
# +define_create+ or +define_update+ methods.
|
|
207
|
+
# Returns true in case of success and raises Granite::Form::ValidationError
|
|
208
|
+
# or Granite::Form::ObjectNotSaved in case of validation or
|
|
209
|
+
# saving fail respectively. Changes +persisted?+ to true
|
|
210
|
+
#
|
|
211
|
+
# author.save!
|
|
212
|
+
#
|
|
213
|
+
# If save performer is not defined with `define_update` or
|
|
214
|
+
# `define_create` or `define_save`, it raises Granite::Form::UnsavableObject.
|
|
215
|
+
# Also save performer block might be passed instead of in-class
|
|
216
|
+
# performer definition:
|
|
217
|
+
#
|
|
218
|
+
# author.save! { REDIS.set(id, attributes.to_json) }
|
|
219
|
+
#
|
|
220
|
+
def save!(_options = {}, &block)
|
|
221
|
+
raise Granite::Form::UnsavableObject unless block || savable?
|
|
222
|
+
validate!
|
|
223
|
+
save_object(&block) or raise Granite::Form::ObjectNotSaved
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Destroys object by calling the destroy performer.
|
|
227
|
+
# Returns instance in any case. Changes +persisted?+
|
|
228
|
+
# to false and +destroyed?+ to true in case of success.
|
|
229
|
+
#
|
|
230
|
+
# author.destroy
|
|
231
|
+
#
|
|
232
|
+
# If destroy performer is not defined with `define_destroy`,
|
|
233
|
+
# it raises Granite::Form::UndestroyableObject.
|
|
234
|
+
# Also destroy performer block might be passed instead of in-class
|
|
235
|
+
# performer definition:
|
|
236
|
+
#
|
|
237
|
+
# author.destroy { REDIS.del(id) }
|
|
238
|
+
#
|
|
239
|
+
def destroy(&block)
|
|
240
|
+
raise Granite::Form::UndestroyableObject unless block || destroyable?
|
|
241
|
+
destroy_object(&block)
|
|
242
|
+
self
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Destroys object by calling the destroy performer.
|
|
246
|
+
# In case of success returns instance and changes +persisted?+
|
|
247
|
+
# to false and +destroyed?+ to true.
|
|
248
|
+
# Raises Granite::Form::ObjectNotDestroyed in case of fail.
|
|
249
|
+
#
|
|
250
|
+
# author.destroy!
|
|
251
|
+
#
|
|
252
|
+
# If destroy performer is not defined with `define_destroy`,
|
|
253
|
+
# it raises Granite::Form::UndestroyableObject.
|
|
254
|
+
# Also destroy performer block might be passed instead of in-class
|
|
255
|
+
# performer definition:
|
|
256
|
+
#
|
|
257
|
+
# author.destroy! { REDIS.del(id) }
|
|
258
|
+
#
|
|
259
|
+
def destroy!(&block)
|
|
260
|
+
raise Granite::Form::UndestroyableObject unless block || destroyable?
|
|
261
|
+
destroy_object(&block) or raise Granite::Form::ObjectNotDestroyed
|
|
262
|
+
self
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
private
|
|
266
|
+
|
|
267
|
+
def savable?
|
|
268
|
+
!!((persisted? ? _update_performer : _create_performer) || _save_performer)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def save_object(&block)
|
|
272
|
+
apply_association_changes! if respond_to?(:apply_association_changes!)
|
|
273
|
+
result = persisted? ? update_object(&block) : create_object(&block)
|
|
274
|
+
mark_persisted! if result
|
|
275
|
+
result
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def create_object(&block)
|
|
279
|
+
performer = block || _create_performer || _save_performer
|
|
280
|
+
!!performer_exec(&performer)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def update_object(&block)
|
|
284
|
+
performer = block || _update_performer || _save_performer
|
|
285
|
+
!!performer_exec(&performer)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def destroyable?
|
|
289
|
+
!!_destroy_performer
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def destroy_object(&block)
|
|
293
|
+
performer = block || _destroy_performer
|
|
294
|
+
result = !!performer_exec(&performer)
|
|
295
|
+
mark_destroyed! if result
|
|
296
|
+
result
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def performer_exec(&block)
|
|
300
|
+
if block.arity == 1
|
|
301
|
+
yield(self)
|
|
302
|
+
else
|
|
303
|
+
instance_exec(&block)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|