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.
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