granite-form 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (134) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +13 -0
  3. data/.github/workflows/ci.yml +35 -0
  4. data/.github/workflows/main.yml +29 -0
  5. data/.gitignore +21 -0
  6. data/.rspec +2 -0
  7. data/.rubocop.yml +64 -0
  8. data/.rubocop_todo.yml +48 -0
  9. data/Appraisals +8 -0
  10. data/CHANGELOG.md +73 -0
  11. data/Gemfile +8 -0
  12. data/Guardfile +77 -0
  13. data/LICENSE +22 -0
  14. data/README.md +429 -0
  15. data/Rakefile +6 -0
  16. data/gemfiles/rails.4.2.gemfile +15 -0
  17. data/gemfiles/rails.5.0.gemfile +15 -0
  18. data/gemfiles/rails.5.1.gemfile +15 -0
  19. data/gemfiles/rails.5.2.gemfile +15 -0
  20. data/gemfiles/rails.6.0.gemfile +14 -0
  21. data/gemfiles/rails.6.1.gemfile +14 -0
  22. data/gemfiles/rails.7.0.gemfile +14 -0
  23. data/granite-form.gemspec +31 -0
  24. data/lib/granite/form/active_record/associations.rb +57 -0
  25. data/lib/granite/form/active_record/nested_attributes.rb +20 -0
  26. data/lib/granite/form/base.rb +15 -0
  27. data/lib/granite/form/config.rb +42 -0
  28. data/lib/granite/form/errors.rb +111 -0
  29. data/lib/granite/form/extensions.rb +36 -0
  30. data/lib/granite/form/model/associations/base.rb +97 -0
  31. data/lib/granite/form/model/associations/collection/embedded.rb +14 -0
  32. data/lib/granite/form/model/associations/collection/proxy.rb +35 -0
  33. data/lib/granite/form/model/associations/embeds_any.rb +19 -0
  34. data/lib/granite/form/model/associations/embeds_many.rb +152 -0
  35. data/lib/granite/form/model/associations/embeds_one.rb +112 -0
  36. data/lib/granite/form/model/associations/nested_attributes.rb +215 -0
  37. data/lib/granite/form/model/associations/persistence_adapters/active_record/referenced_proxy.rb +33 -0
  38. data/lib/granite/form/model/associations/persistence_adapters/active_record.rb +68 -0
  39. data/lib/granite/form/model/associations/persistence_adapters/base.rb +55 -0
  40. data/lib/granite/form/model/associations/references_any.rb +43 -0
  41. data/lib/granite/form/model/associations/references_many.rb +113 -0
  42. data/lib/granite/form/model/associations/references_one.rb +88 -0
  43. data/lib/granite/form/model/associations/reflections/base.rb +92 -0
  44. data/lib/granite/form/model/associations/reflections/embeds_any.rb +52 -0
  45. data/lib/granite/form/model/associations/reflections/embeds_many.rb +17 -0
  46. data/lib/granite/form/model/associations/reflections/embeds_one.rb +19 -0
  47. data/lib/granite/form/model/associations/reflections/references_any.rb +65 -0
  48. data/lib/granite/form/model/associations/reflections/references_many.rb +30 -0
  49. data/lib/granite/form/model/associations/reflections/references_one.rb +32 -0
  50. data/lib/granite/form/model/associations/reflections/singular.rb +37 -0
  51. data/lib/granite/form/model/associations/validations.rb +41 -0
  52. data/lib/granite/form/model/associations.rb +120 -0
  53. data/lib/granite/form/model/attributes/attribute.rb +75 -0
  54. data/lib/granite/form/model/attributes/base.rb +134 -0
  55. data/lib/granite/form/model/attributes/collection.rb +19 -0
  56. data/lib/granite/form/model/attributes/dictionary.rb +28 -0
  57. data/lib/granite/form/model/attributes/localized.rb +44 -0
  58. data/lib/granite/form/model/attributes/reference_many.rb +21 -0
  59. data/lib/granite/form/model/attributes/reference_one.rb +52 -0
  60. data/lib/granite/form/model/attributes/reflections/attribute.rb +61 -0
  61. data/lib/granite/form/model/attributes/reflections/base.rb +62 -0
  62. data/lib/granite/form/model/attributes/reflections/collection.rb +12 -0
  63. data/lib/granite/form/model/attributes/reflections/dictionary.rb +15 -0
  64. data/lib/granite/form/model/attributes/reflections/localized.rb +45 -0
  65. data/lib/granite/form/model/attributes/reflections/reference_many.rb +12 -0
  66. data/lib/granite/form/model/attributes/reflections/reference_one.rb +49 -0
  67. data/lib/granite/form/model/attributes/reflections/represents.rb +56 -0
  68. data/lib/granite/form/model/attributes/represents.rb +67 -0
  69. data/lib/granite/form/model/attributes.rb +204 -0
  70. data/lib/granite/form/model/callbacks.rb +72 -0
  71. data/lib/granite/form/model/conventions.rb +40 -0
  72. data/lib/granite/form/model/dirty.rb +84 -0
  73. data/lib/granite/form/model/lifecycle.rb +309 -0
  74. data/lib/granite/form/model/localization.rb +26 -0
  75. data/lib/granite/form/model/persistence.rb +59 -0
  76. data/lib/granite/form/model/primary.rb +59 -0
  77. data/lib/granite/form/model/representation.rb +101 -0
  78. data/lib/granite/form/model/scopes.rb +118 -0
  79. data/lib/granite/form/model/validations/associated.rb +22 -0
  80. data/lib/granite/form/model/validations/nested.rb +56 -0
  81. data/lib/granite/form/model/validations.rb +29 -0
  82. data/lib/granite/form/model.rb +33 -0
  83. data/lib/granite/form/railtie.rb +9 -0
  84. data/lib/granite/form/undefined_class.rb +11 -0
  85. data/lib/granite/form/version.rb +5 -0
  86. data/lib/granite/form.rb +163 -0
  87. data/spec/lib/granite/form/active_record/associations_spec.rb +211 -0
  88. data/spec/lib/granite/form/active_record/nested_attributes_spec.rb +15 -0
  89. data/spec/lib/granite/form/config_spec.rb +66 -0
  90. data/spec/lib/granite/form/model/associations/embeds_many_spec.rb +706 -0
  91. data/spec/lib/granite/form/model/associations/embeds_one_spec.rb +533 -0
  92. data/spec/lib/granite/form/model/associations/nested_attributes_spec.rb +119 -0
  93. data/spec/lib/granite/form/model/associations/persistence_adapters/active_record_spec.rb +58 -0
  94. data/spec/lib/granite/form/model/associations/references_many_spec.rb +572 -0
  95. data/spec/lib/granite/form/model/associations/references_one_spec.rb +445 -0
  96. data/spec/lib/granite/form/model/associations/reflections/embeds_any_spec.rb +42 -0
  97. data/spec/lib/granite/form/model/associations/reflections/embeds_many_spec.rb +145 -0
  98. data/spec/lib/granite/form/model/associations/reflections/embeds_one_spec.rb +117 -0
  99. data/spec/lib/granite/form/model/associations/reflections/references_many_spec.rb +303 -0
  100. data/spec/lib/granite/form/model/associations/reflections/references_one_spec.rb +287 -0
  101. data/spec/lib/granite/form/model/associations/validations_spec.rb +137 -0
  102. data/spec/lib/granite/form/model/associations_spec.rb +198 -0
  103. data/spec/lib/granite/form/model/attributes/attribute_spec.rb +186 -0
  104. data/spec/lib/granite/form/model/attributes/base_spec.rb +97 -0
  105. data/spec/lib/granite/form/model/attributes/collection_spec.rb +72 -0
  106. data/spec/lib/granite/form/model/attributes/dictionary_spec.rb +100 -0
  107. data/spec/lib/granite/form/model/attributes/localized_spec.rb +103 -0
  108. data/spec/lib/granite/form/model/attributes/reflections/attribute_spec.rb +72 -0
  109. data/spec/lib/granite/form/model/attributes/reflections/base_spec.rb +56 -0
  110. data/spec/lib/granite/form/model/attributes/reflections/collection_spec.rb +37 -0
  111. data/spec/lib/granite/form/model/attributes/reflections/dictionary_spec.rb +43 -0
  112. data/spec/lib/granite/form/model/attributes/reflections/localized_spec.rb +37 -0
  113. data/spec/lib/granite/form/model/attributes/reflections/represents_spec.rb +70 -0
  114. data/spec/lib/granite/form/model/attributes/represents_spec.rb +85 -0
  115. data/spec/lib/granite/form/model/attributes_spec.rb +350 -0
  116. data/spec/lib/granite/form/model/callbacks_spec.rb +337 -0
  117. data/spec/lib/granite/form/model/conventions_spec.rb +11 -0
  118. data/spec/lib/granite/form/model/dirty_spec.rb +84 -0
  119. data/spec/lib/granite/form/model/lifecycle_spec.rb +356 -0
  120. data/spec/lib/granite/form/model/persistence_spec.rb +46 -0
  121. data/spec/lib/granite/form/model/primary_spec.rb +84 -0
  122. data/spec/lib/granite/form/model/representation_spec.rb +139 -0
  123. data/spec/lib/granite/form/model/scopes_spec.rb +86 -0
  124. data/spec/lib/granite/form/model/typecasting_spec.rb +193 -0
  125. data/spec/lib/granite/form/model/validations/associated_spec.rb +102 -0
  126. data/spec/lib/granite/form/model/validations/nested_spec.rb +164 -0
  127. data/spec/lib/granite/form/model/validations_spec.rb +31 -0
  128. data/spec/lib/granite/form/model_spec.rb +10 -0
  129. data/spec/lib/granite/form_spec.rb +11 -0
  130. data/spec/shared/nested_attribute_examples.rb +332 -0
  131. data/spec/spec_helper.rb +50 -0
  132. data/spec/support/model_helpers.rb +10 -0
  133. data/spec/support/muffle_helper.rb +7 -0
  134. 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