active_record_compose 0.11.1 → 0.11.3

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.
@@ -1,107 +1,375 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_record_compose/callbacks'
4
- require 'active_record_compose/attribute_querying'
5
- require 'active_record_compose/composed_collection'
6
- require 'active_record_compose/delegate_attribute'
7
- require 'active_record_compose/transaction_support'
8
- require 'active_record_compose/validations'
3
+ require_relative "attributes"
4
+ require_relative "callbacks"
5
+ require_relative "composed_collection"
6
+ require_relative "persistence"
9
7
 
10
8
  module ActiveRecordCompose
11
- using ComposedCollection::PackagePrivate
12
-
9
+ # This is the core class of {ActiveRecordCompose}.
10
+ #
11
+ # By defining subclasses of this model, you can use ActiveRecordCompose functionality in your application.
12
+ # It has the basic functionality of `ActiveModel::Model` and `ActiveModel::Attributes`,
13
+ # and also provides aggregation of multiple models and atomic updates through transaction control.
14
+ # @example Example of model registration.
15
+ # class AccountRegistration < ActiveRecordCompose::Model
16
+ # def initialize(account = Account.new, attributes = {})
17
+ # @account = account
18
+ # @profile = @account.build_profile
19
+ # models << account << profile
20
+ # super(attributes)
21
+ # end
22
+ #
23
+ # attribute :register_confirmation, :boolean, default: false
24
+ # delegate_attribute :name, :email, to: :account
25
+ # delegate_attribute :firstname, :lastname, :age, to: :profile
26
+ #
27
+ # validates :register_confirmation, presence: true
28
+ #
29
+ # private
30
+ #
31
+ # attr_reader :account, :profile
32
+ # end
33
+ # @example Multiple model update once.
34
+ # registration = AccountRegistration.new
35
+ # registration.assign_attributes(
36
+ # name: "alice-in-wonderland",
37
+ # email: "alice@example.com",
38
+ # firstname: "Alice",
39
+ # lastname: "Smith",
40
+ # age: 24,
41
+ # register_confirmation: true
42
+ # )
43
+ #
44
+ # registration.save! # Register Account and Profile models at the same time.
45
+ # Account.count # => (0 ->) 1
46
+ # Profile.count # => (0 ->) 1
47
+ # @example Attribute delegation.
48
+ # account = Account.new
49
+ # account.name = "foo"
50
+ #
51
+ # registration = AccountRegistration.new(account)
52
+ # registration.name # => "foo" (delegated)
53
+ # registration.name? # => true (delegated attribute method + `?`)
54
+ #
55
+ # registration.name = "bar" # => updates account.name
56
+ # account.name # => "bar"
57
+ # account.name? # => true
58
+ #
59
+ # registration.attributes # => { "original_attribute" => "qux", "name" => "bar" }
60
+ # @example Aggregate errors on invalid.
61
+ # registration = AccountRegistration.new
62
+ #
63
+ # registration.name = "alice-in-wonderland"
64
+ # registration.firstname = "Alice"
65
+ # registration.age = 18
66
+ #
67
+ # registration.valid?
68
+ # #=> false
69
+ #
70
+ # # The error contents of the objects stored in models are aggregated.
71
+ # # For example, direct access to errors in Account#email.
72
+ # registration.errors[:email].to_a # Account#email
73
+ # #=> ["can't be blank"]
74
+ #
75
+ # # Of course, the validation defined for itself is also working.
76
+ # registration.errors[:register_confirmation].to_a
77
+ # #=> ["can't be blank"]
78
+ #
79
+ # registration.errors.to_a
80
+ # #=> ["Email can't be blank", "Lastname can't be blank", "Register confirmation can't be blank"]
13
81
  class Model
14
82
  include ActiveModel::Model
15
- include ActiveModel::Validations::Callbacks
16
- include ActiveModel::Attributes
17
83
 
18
- include ActiveRecordCompose::AttributeQuerying
84
+ include ActiveRecordCompose::Attributes
85
+ include ActiveRecordCompose::Persistence
19
86
  include ActiveRecordCompose::Callbacks
20
- include ActiveRecordCompose::DelegateAttribute
21
- include ActiveRecordCompose::TransactionSupport
22
- prepend ActiveRecordCompose::Validations
23
87
 
24
- validate :validate_models
88
+ begin
89
+ # @group Model Core
25
90
 
26
- def initialize(attributes = {})
27
- super
28
- end
91
+ # @!method self.delegate_attribute(*attributes, to:, allow_nil: false)
92
+ # Provides a method of attribute access to the encapsulated model.
93
+ #
94
+ # It provides a way to access the attributes of the model it encompasses,
95
+ # allowing transparent access as if it had those attributes itself.
96
+ #
97
+ # @param [Array<Symbol, String>] attributes
98
+ # attributes A variable-length list of attribute names to delegate.
99
+ # @param [Symbol, String] to
100
+ # The target object to which attributes are delegated (keyword argument).
101
+ # @param [Boolean] allow_nil
102
+ # allow_nil Whether to allow nil values. Defaults to false.
103
+ # @example Basic usage
104
+ # delegate_attribute :name, :email, to: :profile
105
+ # @example Allowing nil
106
+ # delegate_attribute :bio, to: :profile, allow_nil: true
107
+ # @see Module#delegate for similar behavior in ActiveSupport
29
108
 
30
- # Save the models that exist in models.
31
- # Returns false if any of the targets fail, true if all succeed.
32
- #
33
- # The save is performed within a single transaction.
34
- #
35
- # Only the `:validate` option takes effect as it is required internally.
36
- # However, we do not recommend explicitly specifying `validate: false` to skip validation.
37
- # Additionally, the `:context` option is not accepted.
38
- # The need for such a value indicates that operations from multiple contexts are being processed.
39
- # If the contexts differ, we recommend separating them into different model definitions.
40
- #
41
- # @return [Boolean] returns true on success, false on failure.
42
- def save(**options)
43
- with_transaction_returning_status do
44
- with_callbacks { save_models(**options, bang: false) }
45
- rescue ActiveRecord::RecordInvalid
46
- false
47
- end
48
- end
109
+ # @!method self.attribute_names
110
+ # Returns a array of attribute name.
111
+ # Attributes declared with {.delegate_attribute} are also merged.
112
+ #
113
+ # @see #attribute_names
114
+ # @return [Array<String>] array of attribute name.
49
115
 
50
- # Save the models that exist in models.
51
- # Unlike #save, an exception is raises on failure.
52
- #
53
- # Saving, like `#save`, is performed within a single transaction.
54
- #
55
- # Only the `:validate` option takes effect as it is required internally.
56
- # However, we do not recommend explicitly specifying `validate: false` to skip validation.
57
- # Additionally, the `:context` option is not accepted.
58
- # The need for such a value indicates that operations from multiple contexts are being processed.
59
- # If the contexts differ, we recommend separating them into different model definitions.
60
- #
61
- def save!(**options)
62
- with_transaction_returning_status do
63
- with_callbacks { save_models(**options, bang: true) }
64
- end || raise_on_save_error
65
- end
116
+ # @!method attribute_names
117
+ # Returns a array of attribute name.
118
+ # Attributes declared with {.delegate_attribute} are also merged.
119
+ #
120
+ # class Foo < ActiveRecordCompose::Base
121
+ # def initialize(attributes = {})
122
+ # @account = Account.new
123
+ # super
124
+ # end
125
+ #
126
+ # attribute :confirmation, :boolean, default: false # plain attribute
127
+ # delegate_attribute :name, to: :account # delegated attribute
128
+ #
129
+ # private
130
+ #
131
+ # attr_reader :account
132
+ # end
133
+ #
134
+ # Foo.attribute_names # Returns the merged state of plain and delegated attributes
135
+ # # => ["confirmation" ,"name"]
136
+ #
137
+ # foo = Foo.new
138
+ # foo.attribute_names # Similar behavior for instance method version
139
+ # # => ["confirmation", "name"]
140
+ #
141
+ # @see #attributes
142
+ # @return [Array<String>] array of attribute name.
66
143
 
67
- # Assign attributes and save.
68
- #
69
- # @return [Boolean] returns true on success, false on failure.
70
- def update(attributes = {})
71
- with_transaction_returning_status do
72
- assign_attributes(attributes)
73
- save
74
- end
75
- end
144
+ # @!method attributes
145
+ # Returns a hash with the attribute name as key and the attribute value as value.
146
+ # Attributes declared with {.delegate_attribute} are also merged.
147
+ #
148
+ # class Foo < ActiveRecordCompose::Base
149
+ # def initialize(attributes = {})
150
+ # @account = Account.new
151
+ # super
152
+ # end
153
+ #
154
+ # attribute :confirmation, :boolean, default: false # plain attribute
155
+ # delegate_attribute :name, to: :account # delegated attribute
156
+ #
157
+ # private
158
+ #
159
+ # attr_reader :account
160
+ # end
161
+ #
162
+ # foo = Foo.new
163
+ # foo.name = "Alice"
164
+ # foo.confirmation = true
165
+ #
166
+ # foo.attributes # Returns the merged state of plain and delegated attributes
167
+ # # => { "confirmation" => true, "name" => "Alice" }
168
+ #
169
+ # @return [Hash<String, Object>] hash with the attribute name as key and the attribute value as value.
76
170
 
77
- # Behavior is same to `#update`, but raises an exception prematurely on failure.
78
- #
79
- def update!(attributes = {})
80
- with_transaction_returning_status do
81
- assign_attributes(attributes)
82
- save!
83
- end
84
- end
171
+ # @!method persisted?
172
+ # Returns true if model is persisted.
173
+ #
174
+ # By overriding this definition, you can control the callbacks that are triggered when a save is made.
175
+ # For example, returning false will trigger before_create, around_create and after_create,
176
+ # and returning true will trigger {.before_update}, {.around_update} and {.after_update}.
177
+ #
178
+ # @return [Boolean] returns true if model is persisted.
179
+ # @example
180
+ # # A model where persistence is always false
181
+ # class Foo < ActiveRecordCompose::Model
182
+ # before_save { puts "before_save called" }
183
+ # before_create { puts "before_create called" }
184
+ # before_update { puts "before_update called" }
185
+ # after_update { puts "after_update called" }
186
+ # after_create { puts "after_create called" }
187
+ # after_save { puts "after_save called" }
188
+ #
189
+ # def persisted? = false
190
+ # end
191
+ #
192
+ # # A model where persistence is always true
193
+ # class Bar < Foo
194
+ # def persisted? = true
195
+ # end
196
+ #
197
+ # Foo.new.save!
198
+ # # before_save called
199
+ # # before_create called
200
+ # # after_create called
201
+ # # after_save called
202
+ #
203
+ # Bar.new.save!
204
+ # # before_save called
205
+ # # before_update called
206
+ # # after_update called
207
+ # # after_save called
85
208
 
86
- # Returns true if model is persisted.
87
- #
88
- # By overriding this definition, you can control the callbacks that are triggered when a save is made.
89
- # For example, returning false will trigger before_create, around_create and after_create,
90
- # and returning true will trigger before_update, around_update and after_update.
91
- #
92
- # @return [Boolean] returns true if model is persisted.
93
- def persisted? = super
209
+ # @endgroup
94
210
 
95
- private
211
+ # @group Validations
96
212
 
97
- def models = @__models ||= ActiveRecordCompose::ComposedCollection.new(self)
213
+ # @!method valid?(context = nil)
214
+ # Runs all the validations and returns the result as true or false.
215
+ # @param context Validation context.
216
+ # @return [Boolean] true on success, false on failure.
217
+
218
+ # @!method validate(context = nil)
219
+ # Alias for {#valid?}
220
+ # @see #valid? Validation context.
221
+ # @param context
222
+ # @return [Boolean] true on success, false on failure.
223
+
224
+ # @!method validate!(context = nil)
225
+ # @see #valid?
226
+ # Runs all the validations within the specified context.
227
+ # no errors are found, raises `ActiveRecord::RecordInvalid` otherwise.
228
+ # @param context Validation context.
229
+ # @raise ActiveRecord::RecordInvalid
230
+
231
+ # @!method errors
232
+ # Returns the `ActiveModel::Errors` object that holds all information about attribute error messages.
233
+ #
234
+ # The `ActiveModel::Base` implementation itself,
235
+ # but also aggregates error information for objects stored in {#models} when validation is performed.
236
+ #
237
+ # class Account < ActiveRecord::Base
238
+ # validates :name, :email, presence: true
239
+ # end
240
+ #
241
+ # class AccountRegistration < ActiveRecordCompose::Model
242
+ # def initialize(attributes = {})
243
+ # @account = Account.new
244
+ # super(attributes)
245
+ # models << account
246
+ # end
247
+ #
248
+ # attribute :confirmation, :boolean, default: false
249
+ # validates :confirmation, presence: true
250
+ #
251
+ # private
252
+ #
253
+ # attr_reader :account
254
+ # end
255
+ #
256
+ # registration = AccountRegistration
257
+ # registration.valid?
258
+ # #=> false
259
+ #
260
+ # # In addition to the model's own validation error information (`confirmation`), also aggregates
261
+ # # error information for objects stored in `account` (`name`, `email`) when validation is performed.
262
+ #
263
+ # registration.errors.map { _1.attribute } #=> [:name, :email, :confirmation]
264
+ #
265
+ # @return [ActiveModel::Errors]
266
+
267
+ # @endgroup
268
+
269
+ # @group Persistences
270
+
271
+ # @!method save(**options)
272
+ # Save the models that exist in models.
273
+ # Returns false if any of the targets fail, true if all succeed.
274
+ #
275
+ # The save is performed within a single transaction.
276
+ #
277
+ # Only the `:validate` option takes effect as it is required internally.
278
+ # However, we do not recommend explicitly specifying `validate: false` to skip validation.
279
+ # Additionally, the `:context` option is not accepted.
280
+ # The need for such a value indicates that operations from multiple contexts are being processed.
281
+ # If the contexts differ, we recommend separating them into different model definitions.
282
+ #
283
+ # @params [Hash] Optional parameters.
284
+ # @option options [Boolean] :validate Whether to run validations.
285
+ # This option is intended for internal use only.
286
+ # Users should avoid explicitly passing <tt>validate: false</tt>,
287
+ # as skipping validations can lead to unexpected behavior.
288
+ # @return [Boolean] returns true on success, false on failure.
98
289
 
99
- def save_models(bang:, **options)
100
- models.__wrapped_models.all? { bang ? _1.save!(**options, validate: false) : _1.save(**options, validate: false) }
290
+ # @!method save!(**options)
291
+ # Behavior is same to {#save}, but raises an exception prematurely on failure.
292
+ # @see #save
293
+ # @raise ActiveRecord::RecordInvalid
294
+ # @raise ActiveRecord::RecordNotSaved
295
+
296
+ # @!method update(attributes)
297
+ # Assign attributes and {#save}.
298
+ #
299
+ # @param [Hash<String, Object>] attributes
300
+ # new attributes.
301
+ # @see #save
302
+ # @return [Boolean] returns true on success, false on failure.
303
+
304
+ # @!method update!(attributes)
305
+ # Behavior is same to {#update}, but raises an exception prematurely on failure.
306
+ #
307
+ # @param [Hash<String, Object>] attributes
308
+ # new attributes.
309
+ # @see #save
310
+ # @see #update
311
+ # @raise ActiveRecord::RecordInvalid
312
+ # @raise ActiveRecord::RecordNotSaved
313
+
314
+ # @endgroup
315
+
316
+ # @group Callbacks
317
+
318
+ # @!method self.before_save(*args, &block)
319
+ # Registers a callback to be called before a model is saved.
320
+
321
+ # @!method self.around_save(*args, &block)
322
+ # Registers a callback to be called around the save of a model.
323
+
324
+ # @!method self.after_save(*args, &block)
325
+ # Registers a callback to be called after a model is saved.
326
+
327
+ # @!method self.before_create(*args, &block)
328
+ # Registers a callback to be called before a model is created.
329
+
330
+ # @!method self.around_create(*args, &block)
331
+ # Registers a callback to be called around the creation of a model.
332
+
333
+ # @!method self.after_create(*args, &block)
334
+ # Registers a callback to be called after a model is created.
335
+
336
+ # @!method self.before_update(*args, &block)
337
+ # Registers a callback to be called before a model is updated.
338
+
339
+ # @!method self.around_update(*args, &block)
340
+ # Registers a callback to be called around the update of a model.
341
+
342
+ # @!method self.after_update(*args, &block)
343
+ # Registers a callback to be called after a update is updated.
344
+
345
+ # @!method self.after_commit(*args, &block)
346
+ # Registers a block to be called after the transaction is fully committed.
347
+
348
+ # @!method self.after_rollback(*args, &block)
349
+ # Registers a block to be called after the transaction is rolled back.
350
+
351
+ # @endgroup
352
+ end
353
+
354
+ # @group Model Core
355
+
356
+ def initialize(attributes = {})
357
+ super
101
358
  end
102
359
 
103
- def raise_on_save_error = raise ActiveRecord::RecordNotSaved.new(raise_on_save_error_message, self)
360
+ private
361
+
362
+ # Returns a collection of model elements to encapsulate.
363
+ # @example Adding models
364
+ # models << inner_model_a << inner_model_b
365
+ # models.push(inner_model_c)
366
+ # @example `#push` can have `:destroy` `:if` options
367
+ # models.push(profile, destroy: :blank_profile?)
368
+ # models.push(profile, destroy: -> { blank_profile? })
369
+ # @return [ActiveRecordCompose::ComposedCollection]
370
+ #
371
+ def models = @__models ||= ActiveRecordCompose::ComposedCollection.new(self)
104
372
 
105
- def raise_on_save_error_message = 'Failed to save the model.'
373
+ # @endgroup
106
374
  end
107
375
  end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "composed_collection"
4
+ require_relative "transaction_support"
5
+
6
+ module ActiveRecordCompose
7
+ using ComposedCollection::PackagePrivate
8
+
9
+ # @private
10
+ module Persistence
11
+ extend ActiveSupport::Concern
12
+ include ActiveRecordCompose::TransactionSupport
13
+
14
+ # Save the models that exist in models.
15
+ # Returns false if any of the targets fail, true if all succeed.
16
+ #
17
+ # The save is performed within a single transaction.
18
+ #
19
+ # Only the `:validate` option takes effect as it is required internally.
20
+ # However, we do not recommend explicitly specifying `validate: false` to skip validation.
21
+ # Additionally, the `:context` option is not accepted.
22
+ # The need for such a value indicates that operations from multiple contexts are being processed.
23
+ # If the contexts differ, we recommend separating them into different model definitions.
24
+ #
25
+ # @return [Boolean] returns true on success, false on failure.
26
+ def save(**options)
27
+ with_transaction_returning_status do
28
+ with_callbacks { save_models(**options, bang: false) }
29
+ rescue ActiveRecord::RecordInvalid
30
+ false
31
+ end
32
+ end
33
+
34
+ # Save the models that exist in models.
35
+ # Unlike #save, an exception is raises on failure.
36
+ #
37
+ # Saving, like `#save`, is performed within a single transaction.
38
+ #
39
+ # Only the `:validate` option takes effect as it is required internally.
40
+ # However, we do not recommend explicitly specifying `validate: false` to skip validation.
41
+ # Additionally, the `:context` option is not accepted.
42
+ # The need for such a value indicates that operations from multiple contexts are being processed.
43
+ # If the contexts differ, we recommend separating them into different model definitions.
44
+ #
45
+ def save!(**options)
46
+ with_transaction_returning_status do
47
+ with_callbacks { save_models(**options, bang: true) }
48
+ end || raise_on_save_error
49
+ end
50
+
51
+ # Assign attributes and save.
52
+ #
53
+ # @return [Boolean] returns true on success, false on failure.
54
+ def update(attributes = {})
55
+ with_transaction_returning_status do
56
+ assign_attributes(attributes)
57
+ save
58
+ end
59
+ end
60
+
61
+ # Behavior is same to `#update`, but raises an exception prematurely on failure.
62
+ #
63
+ def update!(attributes = {})
64
+ with_transaction_returning_status do
65
+ assign_attributes(attributes)
66
+ save!
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def save_models(bang:, **options)
73
+ models.__wrapped_models.all? do |model|
74
+ if bang
75
+ model.save!(**options, validate: false)
76
+ else
77
+ model.save(**options, validate: false)
78
+ end
79
+ end
80
+ end
81
+
82
+ def raise_on_save_error = raise ActiveRecord::RecordNotSaved.new(raise_on_save_error_message, self)
83
+
84
+ def raise_on_save_error_message = "Failed to save the model."
85
+ end
86
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordCompose
4
+ # @private
4
5
  module TransactionSupport
5
6
  extend ActiveSupport::Concern
6
7
  include ActiveRecord::Transactions
@@ -1,9 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "composed_collection"
4
+
3
5
  module ActiveRecordCompose
4
6
  using ComposedCollection::PackagePrivate
5
7
 
8
+ # @private
6
9
  module Validations
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ validate :validate_models
14
+ end
15
+
7
16
  def save(**options)
8
17
  perform_validations(options) ? super : false
9
18
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordCompose
4
- VERSION = '0.11.1'
4
+ VERSION = "0.11.3"
5
5
  end
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/core_ext/object'
3
+ require "active_support/core_ext/object"
4
4
 
5
5
  module ActiveRecordCompose
6
+ # @private
6
7
  class WrappedModel
7
8
  # @param model [Object] the model instance.
8
- # @param destroy [Boolean] given true, destroy model.
9
- # @param destroy [Proc] when proc returning true, destroy model.
9
+ # @param destroy [Boolean, Proc, Symbol] Controls whether the model should be destroyed.
10
+ # - Boolean: if `true`, the model will be destroyed.
11
+ # - Proc: the model will be destroyed if the proc returns `true`.
10
12
  # @param if [Proc] evaluation result is false, it will not be included in the renewal.
11
13
  def initialize(model, destroy: false, if: nil)
12
14
  @model = model
@@ -1,9 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_record'
3
+ require "active_record"
4
4
 
5
- require_relative 'active_record_compose/version'
6
- require_relative 'active_record_compose/model'
5
+ require_relative "active_record_compose/version"
6
+ require_relative "active_record_compose/model"
7
7
 
8
+ # namespaces in gem `active_record_compose`.
9
+ #
10
+ # Most of the functionality resides in {ActiveRecordCompose::Model}.
11
+ #
8
12
  module ActiveRecordCompose
9
13
  end