active_record_compose 0.8.0 → 0.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 690ecf032eb0a4c59c97394702c838142a4d519145f72e58f9a381d956cad5ce
4
- data.tar.gz: 7133a2f5defa0d1c97dac41b207936512f6d5375e8f534ea77dc6c13c3f61dde
3
+ metadata.gz: c78fcb982e3f8f36a5497483ae835eaba9724d72342efc0866ca596c25fa45a8
4
+ data.tar.gz: 515f08b1def287d06c1d8436a0c3cb466ef75016094e4778b0e9ba992c95cc30
5
5
  SHA512:
6
- metadata.gz: 2680382210cf6eb2f8371cca7e656f8b00211361f00bf9aed7d2ee4f006a816eee6298a917e48c67d5b4f6149545f2ebb3e67bd5905946279c9e203e9e378d82
7
- data.tar.gz: 63aa5b0a8abcbcd59eb81aadadf4c8207622988ad417b09d2e8f977615ed8726233ae8f257f8d8847e16e38f400955e0bda1d9340f9c7733f06ea8d76fa4854a
6
+ metadata.gz: 1976cef02cc6dbf8d9d543b339254136733c580f116f2ad38c1239fd7a18bdf9a2bde27d7a22be4f041c0eb2576f1bbc97d6e18cfd5859dae52fb2a90624ab69
7
+ data.tar.gz: 71130efd7792fa18dc33903d12bd6eb7ac17ac59bff95ceeedb2200ce128c3f34fdc7229866f8b5fbeea68f1136bb84dd35a17e2e0167b5990d2b26723022ba3
data/.rubocop.yml CHANGED
@@ -24,6 +24,9 @@ Style/NumericPredicate:
24
24
  Style/OptionalBooleanParameter:
25
25
  Enabled: false
26
26
 
27
+ Style/ParallelAssignment:
28
+ Enabled: false
29
+
27
30
  Style/StringLiterals:
28
31
  Enabled: true
29
32
 
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.10.0] - 2025-04-07
4
+
5
+ - avoid twice validation. As a side effect, save must accept argument `#save(**options)`.
6
+ In line with this, the model to be put into models must be
7
+ at least responsive to `model.valid? && model.save(validate: false)`, not `model.save()` (no arguments).
8
+ - supports context as the first argument of `#valid?`, for example `model.valid(:custom_context)`.
9
+ At the same time, it accepts `options[:context]` in `#save(**options)`, such as `model.save(context: :custom_context)`.
10
+ However, this is a convenience support to unify the interface, not a positive one.
11
+
12
+ ## [0.9.0] - 2025-03-16
13
+
14
+ - removed `persisted_flag_callback_control` support.
15
+ - Omitted `:private` option from `delegate_attribute` because, assuming the
16
+ behavior and use cases of `ActiveModel::Attributes.attribute`, making it private is unnecessary.
17
+ - Added the URL for the sample application to the README
18
+
3
19
  ## [0.8.0] - 2025-02-22
4
20
 
5
21
  - changed `persisted_flag_callback_control` default from `false` to `true`.
data/README.md CHANGED
@@ -16,6 +16,7 @@ activemodel (activerecord) form object pattern. it embraces multiple AR models a
16
16
  - [Advanced Usage](#advanced-usage)
17
17
  - [`destroy` option](#destroy-option)
18
18
  - [Callback ordering by `#persisted?`](#callback-ordering-by-persisted)
19
+ - [`#save` with custom context option](#save-with-custom-context-option)
19
20
  - [Links](#links)
20
21
  - [Development](#development)
21
22
  - [Contributing](#contributing)
@@ -286,18 +287,11 @@ end
286
287
 
287
288
  ### Callback ordering by `#persisted?`
288
289
 
289
- The behavior of `(before|after|around)_create` and `(before|after|around)_update` hooks depends on
290
- the state of the `persisted_flag_callback_control` setting.
291
- default value is `true`, behavior when set to `false` will be removed in the next release.
292
-
293
- When `persisted_flag_callback_control` is set to `true`, it behaves almost like callback control in ActiveRecord.
294
- Depending on the evaluation result of `#persisted?`,
290
+ The behavior of `(before|after|around)_create` and `(before|after|around)_update` hooks depending on the evaluation result of `#persisted?`,
295
291
  either the create-related callbacks or the update-related callbacks will be triggered.
296
292
 
297
293
  ```ruby
298
294
  class ComposedModel < ActiveRecordCompose::Model
299
- self.persisted_flag_callback_control = true # In the future, true will be the default and false will no longer be supported.
300
-
301
295
  # ...
302
296
 
303
297
  before_save { puts 'before_save called!' }
@@ -345,48 +339,65 @@ model.save # or `model.update` (the same callbacks will be triggered in all case
345
339
  # after_save called!
346
340
  ```
347
341
 
348
- When `persisted_flag_callback_control` is set to false,
349
- the execution of `#create`, `#update`, or `#save` determines which callbacks will be triggered.
350
- This behavior will no longer be supported in the next release.
342
+ ### `#save` with custom context option
351
343
 
352
- ```ruby
353
- class ComposedModel < ActiveRecordCompose::Model
354
- self.persisted_flag_callback_control = false # Currently defaults to false, but will no longer be supported in the future.
344
+ The interface remains consistent with standard ActiveModel and ActiveRecord models, so the :context option works with #save.
355
345
 
356
- # ...
346
+ ```ruby
347
+ composed_model.valid?(:custom_context)
357
348
 
358
- before_save { puts 'before_save called!' }
359
- before_create { puts 'before_create called!' }
360
- before_update { puts 'before_update called!' }
361
- after_save { puts 'after_save called!' }
362
- after_create { puts 'after_create called!' }
363
- after_update { puts 'after_update called!' }
364
- end
349
+ composed_model.save(context: :custom_context)
365
350
  ```
366
351
 
352
+ However, this may not be ideal from a design perspective.
353
+ If your application requires complex context-specific validations, consider separating models by context.
354
+
367
355
  ```ruby
368
- model = ComposedModel.new
356
+ class Account < ActiveRecord::Base
357
+ validates :name, presence: true
358
+ validates :email, presence: true
359
+ validates :email, format: { with: /\.edu\z/ }, on: :education
360
+ end
369
361
 
370
- model.save
371
- # before_save called!
372
- # after_save called!
362
+ class Registration < ActiveRecordCompose::Model
363
+ def initialize(attributes = {})
364
+ models.push(@account = Account.new)
365
+ super(attributes)
366
+ end
373
367
 
374
- model.create
375
- # before_save called!
376
- # before_create called!
377
- # after_create called!
378
- # after_save called!
368
+ attribute :accept, :boolean
369
+ validates :accept, presence: true, on: :education
379
370
 
380
- model.update
381
- # before_save called!
382
- # before_update called!
383
- # after_update called!
384
- # after_save called!
371
+ delegate_attribute :name, :email, to: :account
372
+
373
+ private
374
+
375
+ attr_reader :account
376
+ end
377
+ ```
378
+ ```ruby
379
+ r = Registration.new(name: 'foo', email: 'example@example.com', accept: false)
380
+ r.valid?
381
+ => true
382
+
383
+ r.valid?(:education)
384
+ => false
385
+ r.errors.map { [_1.attribute, _1.type] }
386
+ => [[:email, :invalid], [:accept, :blank]]
387
+
388
+ r.email = 'example@example.edu'
389
+ r.accept = true
390
+
391
+ r.valid?(:education)
392
+ => true
393
+ r.save(context: :education)
394
+ => true
385
395
  ```
386
396
 
387
397
  ## Links
388
398
 
389
399
  - [Smart way to update multiple models simultaneously in Rails](https://dev.to/hamajyotan/smart-way-to-update-multiple-models-simultaneously-in-rails-51b6)
400
+ - [Sample application as an example](https://github.com/hamajyotan/active_record_compose-example)
390
401
 
391
402
  ## Development
392
403
 
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordCompose
4
+ module Callbacks
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ define_model_callbacks :save
9
+ define_model_callbacks :create
10
+ define_model_callbacks :update
11
+ end
12
+
13
+ private
14
+
15
+ def with_callbacks(&block) = run_callbacks(:save) { run_callbacks(callback_context, &block) }
16
+
17
+ def callback_context = persisted? ? :update : :create
18
+ end
19
+ end
@@ -45,7 +45,7 @@ module ActiveRecordCompose
45
45
  module ClassMethods
46
46
  # Defines the reader and writer for the specified attribute.
47
47
  #
48
- def delegate_attribute(*attributes, to:, allow_nil: nil, private: nil)
48
+ def delegate_attribute(*attributes, to:, allow_nil: nil)
49
49
  delegates = attributes.flat_map do |attribute|
50
50
  reader = attribute.to_s
51
51
  writer = "#{attribute}="
@@ -53,7 +53,7 @@ module ActiveRecordCompose
53
53
  [reader, writer]
54
54
  end
55
55
 
56
- delegate(*delegates, to:, allow_nil:, private:)
56
+ delegate(*delegates, to:, allow_nil:)
57
57
  self.delegated_attributes = delegated_attributes.to_a + attributes.map { _1.to_s }
58
58
  end
59
59
  end
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_record_compose/callbacks'
3
4
  require 'active_record_compose/composed_collection'
4
5
  require 'active_record_compose/delegate_attribute'
5
6
  require 'active_record_compose/transaction_support'
7
+ require 'active_record_compose/validations'
6
8
 
7
9
  module ActiveRecordCompose
8
10
  using ComposedCollection::PackagePrivate
@@ -12,52 +14,10 @@ module ActiveRecordCompose
12
14
  include ActiveModel::Validations::Callbacks
13
15
  include ActiveModel::Attributes
14
16
 
17
+ include ActiveRecordCompose::Callbacks
15
18
  include ActiveRecordCompose::DelegateAttribute
16
19
  include ActiveRecordCompose::TransactionSupport
17
-
18
- # This flag controls the callback sequence for models.
19
- # The current default value is `true`, behavior when set to `false` will be removed in the next release.
20
- #
21
- # When `persisted_flag_callback_control` is set to `true`,
22
- # the occurrence of callbacks depends on the evaluation result of `#persisted?`.
23
- # Additionally, the definition of `#persisted?` itself can be appropriately overridden in subclasses.
24
- #
25
- # if `#persisted?` returns `false`:
26
- # * before_save
27
- # * before_create
28
- # * after_create
29
- # * after_save
30
- #
31
- # if `#persisted?` returns `true`:
32
- # * before_save
33
- # * before_update
34
- # * after_update
35
- # * after_save
36
- #
37
- # On the other hand, when `persisted_flag_callback_control` is set to `false`,
38
- # the invoked methods during saving operations vary depending on the method used.
39
- #
40
- # when performing `#save` or `#save!`:
41
- # * before_save
42
- # * after_save
43
- #
44
- # when performing `#update` or `#update!`:
45
- # * before_save
46
- # * before_update
47
- # * after_update
48
- # * after_save
49
- #
50
- # when performing `#create` or `#create!`:
51
- # * before_save
52
- # * before_create
53
- # * after_create
54
- # * after_save
55
- #
56
- class_attribute :persisted_flag_callback_control, instance_accessor: false, default: true
57
-
58
- define_model_callbacks :save
59
- define_model_callbacks :create
60
- define_model_callbacks :update
20
+ prepend ActiveRecordCompose::Validations
61
21
 
62
22
  validate :validate_models
63
23
 
@@ -70,23 +30,16 @@ module ActiveRecordCompose
70
30
  #
71
31
  # The save is performed within a single transaction.
72
32
  #
33
+ # Only the `:validate` option takes effect as it is required internally.
34
+ # However, we do not recommend explicitly specifying `validate: false` to skip validation.
35
+ # Additionally, the `:context` option is not accepted.
36
+ # The need for such a value indicates that operations from multiple contexts are being processed.
37
+ # If the contexts differ, we recommend separating them into different model definitions.
38
+ #
73
39
  # @return [Boolean] returns true on success, false on failure.
74
- def save
75
- return false if invalid?
76
-
40
+ def save(**options)
77
41
  with_transaction_returning_status do
78
- if self.class.persisted_flag_callback_control
79
- with_callbacks { save_models(bang: false) }
80
- else
81
- # steep:ignore:start
82
- deprecator.warn(
83
- 'The behavior with `persisted_flag_callback_control` set to `false` will be removed in 0.9.0. ' \
84
- 'Use `self.persisted_flag_callback_control = true` set to `true`. ' \
85
- '(Alternatively, exclude statements that set `false`)',
86
- )
87
- # steep:ignore:end
88
- run_callbacks(:save) { save_models(bang: false) }
89
- end
42
+ with_callbacks { save_models(**options, bang: false) }
90
43
  rescue ActiveRecord::RecordInvalid
91
44
  false
92
45
  end
@@ -97,165 +50,35 @@ module ActiveRecordCompose
97
50
  #
98
51
  # Saving, like `#save`, is performed within a single transaction.
99
52
  #
100
- def save!
101
- valid? || raise_validation_error
102
-
103
- with_transaction_returning_status do
104
- if self.class.persisted_flag_callback_control
105
- with_callbacks { save_models(bang: true) }
106
- else
107
- # steep:ignore:start
108
- deprecator.warn(
109
- 'The behavior with `persisted_flag_callback_control` set to `false` will be removed in 0.9.0. ' \
110
- 'Use `self.persisted_flag_callback_control = true` set to `true`. ' \
111
- '(Alternatively, exclude statements that set `false`)',
112
- )
113
- # steep:ignore:end
114
- run_callbacks(:save) { save_models(bang: true) }
115
- end
116
- end || raise_on_save_error
117
- end
118
-
119
- # Behavior is same to `#save`, but `before_create` and `after_create` hooks fires.
120
- #
121
- # class ComposedModel < ActiveRecordCompose::Model
122
- # # ...
123
- #
124
- # before_save { puts 'before_save called!' }
125
- # before_create { puts 'before_create called!' }
126
- # before_update { puts 'before_update called!' }
127
- # after_save { puts 'after_save called!' }
128
- # after_create { puts 'after_create called!' }
129
- # after_update { puts 'after_update called!' }
130
- # end
53
+ # Only the `:validate` option takes effect as it is required internally.
54
+ # However, we do not recommend explicitly specifying `validate: false` to skip validation.
55
+ # Additionally, the `:context` option is not accepted.
56
+ # The need for such a value indicates that operations from multiple contexts are being processed.
57
+ # If the contexts differ, we recommend separating them into different model definitions.
131
58
  #
132
- # model = ComposedModel.new
133
- #
134
- # model.save
135
- # # before_save called!
136
- # # after_save called!
137
- #
138
- # model.create
139
- # # before_save called!
140
- # # before_create called!
141
- # # after_create called!
142
- # # after_save called!
143
- #
144
- # @deprecated
145
- def create(attributes = {})
146
- if self.class.persisted_flag_callback_control
147
- raise '`#create` cannot be called. The context for creation or update is determined by the `#persisted` flag.'
148
- end
149
-
150
- # steep:ignore:start
151
- deprecator.warn(
152
- 'The behavior with `persisted_flag_callback_control` set to `false` will be removed in 0.9.0. ' \
153
- 'Use `self.persisted_flag_callback_control = true` set to `true`. ' \
154
- '(Alternatively, exclude statements that set `false`)',
155
- )
156
- # steep:ignore:end
157
-
158
- assign_attributes(attributes)
159
- return false if invalid?
160
-
59
+ def save!(**options)
161
60
  with_transaction_returning_status do
162
- with_callbacks(context: :create) { save_models(bang: false) }
163
- rescue ActiveRecord::RecordInvalid
164
- false
165
- end
166
- end
167
-
168
- # Behavior is same to `#create`, but raises an exception prematurely on failure.
169
- #
170
- # @deprecated
171
- def create!(attributes = {})
172
- if self.class.persisted_flag_callback_control
173
- raise '`#create` cannot be called. The context for creation or update is determined by the `#persisted` flag.'
174
- end
175
-
176
- # steep:ignore:start
177
- deprecator.warn(
178
- 'The behavior with `persisted_flag_callback_control` set to `false` will be removed in 0.9.0. ' \
179
- 'Use `self.persisted_flag_callback_control = true` set to `true`. ' \
180
- '(Alternatively, exclude statements that set `false`)',
181
- )
182
- # steep:ignore:end
183
-
184
- assign_attributes(attributes)
185
- valid? || raise_validation_error
186
-
187
- with_transaction_returning_status do
188
- with_callbacks(context: :create) { save_models(bang: true) }
61
+ with_callbacks { save_models(**options, bang: true) }
189
62
  end || raise_on_save_error
190
63
  end
191
64
 
192
- # Behavior is same to `#save`, but `before_update` and `after_update` hooks fires.
193
- #
194
- # class ComposedModel < ActiveRecordCompose::Model
195
- # # ...
196
- #
197
- # before_save { puts 'before_save called!' }
198
- # before_create { puts 'before_create called!' }
199
- # before_update { puts 'before_update called!' }
200
- # after_save { puts 'after_save called!' }
201
- # after_create { puts 'after_create called!' }
202
- # after_update { puts 'after_update called!' }
203
- # end
204
- #
205
- # model = ComposedModel.new
206
- #
207
- # model.save
208
- # # before_save called!
209
- # # after_save called!
210
- #
211
- # model.update
212
- # # before_save called!
213
- # # before_update called!
214
- # # after_update called!
215
- # # after_save called!
65
+ # Assign attributes and save.
216
66
  #
67
+ # @return [Boolean] returns true on success, false on failure.
217
68
  def update(attributes = {})
218
- assign_attributes(attributes)
219
- return false if invalid?
220
-
221
69
  with_transaction_returning_status do
222
- if self.class.persisted_flag_callback_control
223
- with_callbacks { save_models(bang: false) }
224
- else
225
- # steep:ignore:start
226
- deprecator.warn(
227
- 'The behavior with `persisted_flag_callback_control` set to `false` will be removed in 0.9.0. ' \
228
- 'Use `self.persisted_flag_callback_control = true` set to `true`. ' \
229
- '(Alternatively, exclude statements that set `false`)',
230
- )
231
- # steep:ignore:end
232
- with_callbacks(context: :update) { save_models(bang: false) }
233
- end
234
- rescue ActiveRecord::RecordInvalid
235
- false
70
+ assign_attributes(attributes)
71
+ save
236
72
  end
237
73
  end
238
74
 
239
75
  # Behavior is same to `#update`, but raises an exception prematurely on failure.
240
76
  #
241
77
  def update!(attributes = {})
242
- assign_attributes(attributes)
243
- valid? || raise_validation_error
244
-
245
78
  with_transaction_returning_status do
246
- if self.class.persisted_flag_callback_control
247
- with_callbacks { save_models(bang: true) }
248
- else
249
- # steep:ignore:start
250
- deprecator.warn(
251
- 'The behavior with `persisted_flag_callback_control` set to `false` will be removed in 0.9.0. ' \
252
- 'Use `self.persisted_flag_callback_control = true` set to `true`. ' \
253
- '(Alternatively, exclude statements that set `false`)',
254
- )
255
- # steep:ignore:end
256
- with_callbacks(context: :update) { save_models(bang: true) }
257
- end
258
- end || raise_on_save_error
79
+ assign_attributes(attributes)
80
+ save!
81
+ end
259
82
  end
260
83
 
261
84
  # Returns true if model is persisted.
@@ -271,34 +94,12 @@ module ActiveRecordCompose
271
94
 
272
95
  def models = @__models ||= ActiveRecordCompose::ComposedCollection.new(self)
273
96
 
274
- def validate_models
275
- models.__wrapped_models.lazy.select { _1.invalid? }.each { errors.merge!(_1) }
276
- end
277
-
278
- def with_callbacks(context: nil, &block)
279
- run_callbacks(:save) { run_callbacks(callback_context(context:), &block) }
280
- end
281
-
282
- def callback_context(context: nil)
283
- context || (persisted? ? :update : :create)
284
- end
285
-
286
- def save_models(bang:)
287
- models.__wrapped_models.all? { bang ? _1.save! : _1.save }
97
+ def save_models(bang:, **options)
98
+ models.__wrapped_models.all? { bang ? _1.save!(**options, validate: false) : _1.save(**options, validate: false) }
288
99
  end
289
100
 
290
- def raise_validation_error = raise ActiveRecord::RecordInvalid, self
291
-
292
101
  def raise_on_save_error = raise ActiveRecord::RecordNotSaved.new(raise_on_save_error_message, self)
293
102
 
294
103
  def raise_on_save_error_message = 'Failed to save the model.'
295
-
296
- def deprecator
297
- if ActiveRecord.respond_to?(:deprecator)
298
- ActiveRecord.deprecator # steep:ignore
299
- else # for rails 7.0.x or lower
300
- ActiveSupport::Deprecation
301
- end
302
- end
303
104
  end
304
105
  end
@@ -18,7 +18,7 @@ module ActiveRecordCompose
18
18
 
19
19
  def with_connection(&) = ar_class.with_connection(&) # steep:ignore
20
20
 
21
- def composite_primary_key? = false
21
+ def composite_primary_key? = false # steep:ignore
22
22
 
23
23
  private
24
24
 
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordCompose
4
+ using ComposedCollection::PackagePrivate
5
+
6
+ module Validations
7
+ def save(**options)
8
+ perform_validations(options) ? super : false
9
+ end
10
+
11
+ def save!(**options)
12
+ perform_validations(options) ? super : raise_validation_error
13
+ end
14
+
15
+ def valid?(context = nil) = context_for_override_validation.with_override(context) { super }
16
+
17
+ private
18
+
19
+ def validate_models
20
+ context = override_validation_context
21
+ models.__wrapped_models.lazy.select { _1.invalid?(context) }.each { errors.merge!(_1) }
22
+ end
23
+
24
+ def perform_validations(options)
25
+ options[:validate] == false || valid?(options[:context])
26
+ end
27
+
28
+ def raise_validation_error = raise ActiveRecord::RecordInvalid, self
29
+
30
+ def context_for_override_validation
31
+ @context_for_override_validation ||= OverrideValidationContext.new
32
+ end
33
+
34
+ def override_validation_context = context_for_override_validation.context
35
+
36
+ class OverrideValidationContext
37
+ attr_reader :context
38
+
39
+ def with_override(context)
40
+ @context, original = context, @context
41
+ yield
42
+ ensure
43
+ @context = original # steep:ignore
44
+ end
45
+ end
46
+ private_constant :OverrideValidationContext
47
+ end
48
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordCompose
4
- VERSION = '0.8.0'
4
+ VERSION = '0.10.0'
5
5
  end
@@ -60,7 +60,7 @@ module ActiveRecordCompose
60
60
  # Whether save or destroy is executed depends on the value of `#destroy_context?`.
61
61
  #
62
62
  # @return [Boolean] returns true on success, false on failure.
63
- def save
63
+ def save(**options)
64
64
  # While errors caused by the type check are avoided,
65
65
  # it is important to note that an error can still occur
66
66
  # if `#destroy_context?` returns true but ar_like does not implement `#destroy`.
@@ -70,14 +70,14 @@ module ActiveRecordCompose
70
70
  m.destroy
71
71
  else
72
72
  # @type var m: ActiveRecordCompose::_ARLike
73
- m.save
73
+ m.save(**options)
74
74
  end
75
75
  end
76
76
 
77
77
  # Execute save or destroy. Unlike #save, an exception is raises on failure.
78
78
  # Whether save or destroy is executed depends on the value of `#destroy_context?`.
79
79
  #
80
- def save!
80
+ def save!(**options)
81
81
  # While errors caused by the type check are avoided,
82
82
  # it is important to note that an error can still occur
83
83
  # if `#destroy_context?` returns true but ar_like does not implement `#destroy`.
@@ -87,15 +87,15 @@ module ActiveRecordCompose
87
87
  m.destroy!
88
88
  else
89
89
  # @type var model: ActiveRecordCompose::_ARLike
90
- m.save!
90
+ m.save!(**options)
91
91
  end
92
92
  end
93
93
 
94
94
  # @return [Boolean]
95
- def invalid? = destroy_context? ? false : model.invalid?
95
+ def invalid?(context = nil) = !valid?(context)
96
96
 
97
97
  # @return [Boolean]
98
- def valid? = !invalid?
98
+ def valid?(context = nil) = destroy_context? || model.valid?(context)
99
99
 
100
100
  # Returns true if equivalent.
101
101
  #
@@ -1,4 +1,15 @@
1
1
  module ActiveRecordCompose
2
+ module Callbacks
3
+ include ActiveModel::Model
4
+ include ActiveModel::Validations::Callbacks
5
+ extend ActiveSupport::Concern
6
+ extend ActiveModel::Callbacks
7
+
8
+ private
9
+ def with_callbacks: { () -> bool } -> bool
10
+ def callback_context: -> (:create | :update)
11
+ end
12
+
2
13
  class ComposedCollection
3
14
  def initialize: (Model) -> void
4
15
 
@@ -24,7 +35,7 @@ module ActiveRecordCompose
24
35
  def delegated_attributes: () -> Array[String]
25
36
 
26
37
  module ClassMethods : Module
27
- def delegate_attribute: (*untyped methods, to: untyped, ?allow_nil: untyped?, ?private: untyped?) -> untyped
38
+ def delegate_attribute: (*untyped methods, to: untyped, ?allow_nil: untyped?) -> untyped
28
39
  def delegated_attributes: () -> Array[String]
29
40
  def delegated_attributes=: (Array[String]) -> untyped
30
41
  end
@@ -35,8 +46,13 @@ module ActiveRecordCompose
35
46
  extend DelegateAttribute::ClassMethods
36
47
  include TransactionSupport
37
48
  extend TransactionSupport::ClassMethods
49
+ include ActiveRecordCompose::Callbacks
38
50
 
39
51
  @__models: ComposedCollection
52
+
53
+ private
54
+ def validate_models: -> void
55
+ def override_validation_context: -> validation_context
40
56
  end
41
57
 
42
58
  module TransactionSupport
@@ -54,14 +70,37 @@ module ActiveRecordCompose
54
70
  end
55
71
  end
56
72
 
73
+ module Validations : Model
74
+ def save: (**untyped options) -> bool
75
+ def save!: (**untyped options) -> untyped
76
+ def valid?: (?validation_context context) -> bool
77
+
78
+ @context_for_override_validation: OverrideValidationContext
79
+
80
+ private
81
+
82
+ def perform_validations: (::Hash[untyped, untyped]) -> bool
83
+ def raise_validation_error: -> bot
84
+ def context_for_override_validation: -> OverrideValidationContext
85
+ def override_validation_context: -> validation_context
86
+
87
+ class OverrideValidationContext
88
+ @context: validation_context
89
+
90
+ attr_reader context: validation_context
91
+
92
+ def with_override: [T] (validation_context) { () -> T } -> T
93
+ end
94
+ end
95
+
57
96
  class WrappedModel
58
97
  def initialize: (ar_like, ?destroy: (bool | destroy_context_type), ?if: (nil | condition_type)) -> void
59
98
  def destroy_context?: -> bool
60
99
  def ignore?: -> bool
61
- def save: -> bool
62
- def save!: -> untyped
63
- def invalid?: -> bool
64
- def valid?: -> bool
100
+ def save: (**untyped options) -> bool
101
+ def save!: (**untyped options) -> untyped
102
+ def invalid?: (?validation_context context) -> bool
103
+ def valid?: (?validation_context context) -> bool
65
104
  def is_a?: (untyped) -> bool
66
105
  def ==: (untyped) -> bool
67
106
 
@@ -5,27 +5,29 @@ module ActiveRecordCompose
5
5
  VERSION: String
6
6
 
7
7
  interface _ARLike
8
- def save: -> bool
9
- def save!: -> untyped
10
- def invalid?: -> bool
11
- def valid?: -> bool
8
+ def save: (**untyped options) -> bool
9
+ def save!: (**untyped options) -> untyped
10
+ def invalid?: (?validation_context context) -> bool
11
+ def valid?: (?validation_context context) -> bool
12
12
  def errors: -> untyped
13
13
  def is_a?: (untyped) -> bool
14
14
  def ==: (untyped) -> bool
15
15
  end
16
16
  interface _ARLikeWithDestroy
17
- def save: -> bool
18
- def save!: -> untyped
17
+ def save: (**untyped options) -> bool
18
+ def save!: (**untyped options) -> untyped
19
19
  def destroy: -> bool
20
20
  def destroy!: -> untyped
21
- def invalid?: -> bool
22
- def valid?: -> bool
21
+ def invalid?: (?validation_context context) -> bool
22
+ def valid?: (?validation_context context) -> bool
23
23
  def errors: -> untyped
24
24
  def is_a?: (untyped) -> bool
25
25
  def ==: (untyped) -> bool
26
26
  end
27
27
  type ar_like = (_ARLike | _ARLikeWithDestroy)
28
28
 
29
+ type validation_context = nil | Symbol | Array[Symbol]
30
+
29
31
  type condition[T] = Symbol | ^(T) [self: T] -> boolish
30
32
  type callback[T] = Symbol | ^(T) [self: T] -> void
31
33
  type around_callback[T] = Symbol | ^(T, Proc) [self: T] -> void
@@ -69,30 +71,21 @@ module ActiveRecordCompose
69
71
  def self.after_commit: (*callback[instance], ?if: condition[instance], ?unless: condition[instance], **untyped) ?{ () [self: instance] -> void } -> void
70
72
  def self.after_rollback: (*callback[instance], ?if: condition[instance], ?unless: condition[instance], **untyped) ?{ () [self: instance] -> void } -> void
71
73
 
72
- def self.persisted_flag_callback_control: () -> boolish
73
- def self.persisted_flag_callback_control=: (boolish) -> untyped
74
-
75
- def self.delegate_attribute: (*untyped methods, to: untyped, ?allow_nil: untyped?, ?private: untyped?) -> untyped
74
+ def self.delegate_attribute: (*untyped methods, to: untyped, ?allow_nil: untyped?) -> untyped
76
75
  def self.connection: -> ActiveRecord::ConnectionAdapters::AbstractAdapter
77
76
  def self.lease_connection: -> ActiveRecord::ConnectionAdapters::AbstractAdapter
78
77
  def self.with_connection: [T] () { () -> T } -> T
79
78
 
80
79
  def initialize: (?Hash[attribute_name, untyped]) -> void
81
- def save: -> bool
82
- def save!: -> untyped
83
- def create: (?Hash[attribute_name, untyped]) -> bool
84
- def create!: (?Hash[attribute_name, untyped]) -> untyped
80
+ def save: (**untyped options) -> bool
81
+ def save!: (**untyped options) -> untyped
85
82
  def update: (?Hash[attribute_name, untyped]) -> bool
86
83
  def update!: (?Hash[attribute_name, untyped]) -> untyped
87
84
  def id: -> untyped
88
85
 
89
86
  private
90
87
  def models: -> ComposedCollection
91
- def with_callbacks: (?context: (nil | :create | :update)) { () -> bool } -> bool
92
- def callback_context: (?context: (nil | :create | :update)) -> (:create | :update)
93
- def validate_models: -> void
94
- def save_models: (bang: bool) -> bool
95
- def raise_validation_error: -> bot
88
+ def save_models: (bang: bool, **untyped options) -> bool
96
89
  def raise_on_save_error: -> bot
97
90
  def raise_on_save_error_message: -> String
98
91
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_record_compose
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - hamajyotan
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-02-22 00:00:00.000000000 Z
10
+ date: 2025-04-07 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activerecord
@@ -37,10 +37,12 @@ files:
37
37
  - LICENSE.txt
38
38
  - README.md
39
39
  - lib/active_record_compose.rb
40
+ - lib/active_record_compose/callbacks.rb
40
41
  - lib/active_record_compose/composed_collection.rb
41
42
  - lib/active_record_compose/delegate_attribute.rb
42
43
  - lib/active_record_compose/model.rb
43
44
  - lib/active_record_compose/transaction_support.rb
45
+ - lib/active_record_compose/validations.rb
44
46
  - lib/active_record_compose/version.rb
45
47
  - lib/active_record_compose/wrapped_model.rb
46
48
  - sig/_internal/package_private.rbs
@@ -52,7 +54,7 @@ metadata:
52
54
  homepage_uri: https://github.com/hamajyotan/active_record_compose
53
55
  source_code_uri: https://github.com/hamajyotan/active_record_compose
54
56
  changelog_uri: https://github.com/hamajyotan/active_record_compose/blob/main/CHANGELOG.md
55
- documentation_uri: https://www.rubydoc.info/gems/active_record_compose/0.8.0
57
+ documentation_uri: https://www.rubydoc.info/gems/active_record_compose/0.10.0
56
58
  rubygems_mfa_required: 'true'
57
59
  rdoc_options: []
58
60
  require_paths: