active_record_compose 0.6.3 → 0.7.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: 6a3d2a1c6e4aac0bc387a6e0d9ba2917dafeacec4d46b2ad617d54e45ff9786f
4
- data.tar.gz: d71b88c9b069eca62e49d2e5ff25c447a5e70e741d29b76f572c5a21b4994a4b
3
+ metadata.gz: ab0bb2d4ff8813ad8fb9a4830c2b1d3887aaa29702f11561c9c8624719a5082f
4
+ data.tar.gz: a3fcfb870aaf5a0825a218556423cc1af16644b41ec22f0b4d656cb42cd33790
5
5
  SHA512:
6
- metadata.gz: 3579fdc36270eaaa2f9f253b247aabc5bc4aa6547df5511e81158ebbf3277b37dbbaa3e4d4376fff5c9ac0066cebbf0207db993cd20b4c8b159a6f3069ae24f1
7
- data.tar.gz: 4f2e98a4158a5c19c13f5bc5d83276c7071ece19b2c969d73c24cce47231c9f272fb32620d5a31748e2bd060d72733bcd28be3d4d3eb6c1e9c8ded1bb9db0340
6
+ metadata.gz: d5a1ea42bd83a986437faac38b2543ee48aff7f2003afa6267a7adcd325db1bbb0506f4f1f25e2aaa13b733df7956ebd820627821f19101fc36dd4f046a51c43
7
+ data.tar.gz: 4a9cc98fddbcf1068720387207469cb527b96e536a9f109a6174b5a439daa383f54a1625e916c731d01883cf360c6e01281858a238796b5cb64b92665c2405c3
data/CHANGELOG.md CHANGED
@@ -1,6 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.6.2] - 2025-01-31
3
+ ## [0.7.0] - 2025-02-12
4
+
5
+ - rename ActiveRecordCompose::InnerModel to ActiveRecordCompose::WrappedModel
6
+ - rename ActiveRecordCompose::InnerModelCollection to ActiveRecordCompose::ComposedCollection
7
+ - A new callback control flag, `persisted_flag_callback_control`, has been defined.
8
+ Currently, the default value is false, which does not change the existing behavior, but it will be deprecated in the future.
9
+ When the flag is set to true, the behavior will be almost the same as the callback sequence in ActiveRecord.
10
+ (https://github.com/hamajyotan/active_record_compose/issues/11)
11
+
12
+ ## [0.6.3] - 2025-01-31
4
13
 
5
14
  - fix: type error in `ActiveRecordCompose::Model` subclass definitions.
6
15
  - fixed type errors in subclass callback definitions, etc.
data/README.md CHANGED
@@ -286,13 +286,17 @@ end
286
286
 
287
287
  ### Callback ordering by `#save`, `#create` and `#update`.
288
288
 
289
- Sometimes, multiple AR objects are passed to the models in the arguments.
290
- It is not strictly possible to distinguish between create and update operations, regardless of the state of `#persisted?`.
291
- Therefore, control measures such as separating callbacks with `after_create` and `after_update` based on the `#persisted?` of AR objects are left to the discretion of the user,
292
- rather than being determined by the state of the AR objects themselves.
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
+
292
+ When `persisted_flag_callback_control` is set to false,
293
+ the execution of `#create`, `#update`, or `#save` determines which callbacks will be triggered.
294
+ Currently, the default value is `false`, but it will no longer be supported in the future.
293
295
 
294
296
  ```ruby
295
297
  class ComposedModel < ActiveRecordCompose::Model
298
+ self.persisted_flag_callback_control = false # Currently defaults to false, but will no longer be supported in the future.
299
+
296
300
  # ...
297
301
 
298
302
  before_save { puts 'before_save called!' }
@@ -324,6 +328,66 @@ model.update
324
328
  # after_save called!
325
329
  ```
326
330
 
331
+ When `persisted_flag_callback_control` is set to `true`, it behaves almost like callback control in ActiveRecord.
332
+ This behavior will be the default in the future.
333
+
334
+ ```ruby
335
+ class ComposedModel < ActiveRecordCompose::Model
336
+ self.persisted_flag_callback_control = true # In the future, true will be the default and false will no longer be supported.
337
+
338
+ # ...
339
+
340
+ before_save { puts 'before_save called!' }
341
+ before_create { puts 'before_create called!' }
342
+ before_update { puts 'before_update called!' }
343
+ after_save { puts 'after_save called!' }
344
+ after_create { puts 'after_create called!' }
345
+ after_update { puts 'after_update called!' }
346
+
347
+ def persisted?
348
+ # Override and return a boolish value depending on the state of the inner model.
349
+ # For example, it could be transferred to the primary model to be manipulated.
350
+ #
351
+ # # ex.)
352
+ # def persisted? = the_model.persisted?
353
+ #
354
+ true
355
+ end
356
+ end
357
+ ```
358
+
359
+ ```ruby
360
+ # when `model.persisted?` returns `true`
361
+
362
+ model = ComposedModel.new
363
+
364
+ model.save # or `model.update` (the same callbacks will be triggered in all cases).
365
+
366
+ # before_save called!
367
+ # before_update called! # when persisted? is false, before_create hook is fired here instead.
368
+ # after_update called! # when persisted? is false, after_create hook is fired here instead.
369
+ # after_save called!
370
+ ```
371
+
372
+ ```ruby
373
+ # when `model.persisted?` returns `false`
374
+
375
+ model = ComposedModel.new
376
+
377
+ model.save # or `model.update` (the same callbacks will be triggered in all cases).
378
+
379
+ # before_save called!
380
+ # before_create called!
381
+ # after_create called!
382
+ # after_save called!
383
+ ```
384
+
385
+ When `persisted_flag_callback_control` is `true`, `#create` is not supported.
386
+
387
+ ```ruby
388
+ model.create # => raises RuntimeError
389
+ ```
390
+
327
391
  ## Links
328
392
 
329
393
  - [Smart way to update multiple models simultaneously in Rails](https://dev.to/hamajyotan/smart-way-to-update-multiple-models-simultaneously-in-rails-51b6)
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_record_compose/inner_model'
3
+ require 'active_record_compose/wrapped_model'
4
4
 
5
5
  module ActiveRecordCompose
6
- using InnerModel::PackagePrivate
6
+ using WrappedModel::PackagePrivate
7
7
 
8
- class InnerModelCollection
8
+ class ComposedCollection
9
9
  include Enumerable
10
10
 
11
11
  def initialize(owner)
@@ -79,30 +79,25 @@ module ActiveRecordCompose
79
79
  attr_reader :owner, :models
80
80
 
81
81
  def wrap(model, destroy: false, if: nil)
82
- if model.is_a?(ActiveRecordCompose::InnerModel)
83
- # @type var model: ActiveRecordCompose::InnerModel
84
- model
85
- else
86
- if destroy.is_a?(Symbol)
87
- method = destroy
88
- destroy = -> { owner.__send__(method) }
89
- end
90
- if_option = binding.local_variable_get(:if)
91
- if if_option.is_a?(Symbol)
92
- method = if_option
93
- if_option = -> { owner.__send__(method) }
94
- end
95
- ActiveRecordCompose::InnerModel.new(model, destroy:, if: if_option)
82
+ if destroy.is_a?(Symbol)
83
+ method = destroy
84
+ destroy = -> { owner.__send__(method) }
96
85
  end
86
+ if_option = binding.local_variable_get(:if)
87
+ if if_option.is_a?(Symbol)
88
+ method = if_option
89
+ if_option = -> { owner.__send__(method) }
90
+ end
91
+ ActiveRecordCompose::WrappedModel.new(model, destroy:, if: if_option)
97
92
  end
98
93
 
99
94
  # @private
100
95
  module PackagePrivate
101
- refine InnerModelCollection do
96
+ refine ComposedCollection do
102
97
  # Returns array of wrapped model instance.
103
98
  #
104
99
  # @private
105
- # @return [Array[InnerModel] array of wrapped model instance.
100
+ # @return [Array[WrappedModel]] array of wrapped model instance.
106
101
  def __wrapped_models = models.reject { _1.ignore? }.select { _1.__raw_model }
107
102
  end
108
103
  end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_record_compose/composed_collection'
3
4
  require 'active_record_compose/delegate_attribute'
4
- require 'active_record_compose/inner_model_collection'
5
5
  require 'active_record_compose/transaction_support'
6
6
 
7
7
  module ActiveRecordCompose
8
- using InnerModelCollection::PackagePrivate
8
+ using ComposedCollection::PackagePrivate
9
9
 
10
10
  class Model
11
11
  include ActiveModel::Model
@@ -15,6 +15,46 @@ module ActiveRecordCompose
15
15
  include ActiveRecordCompose::DelegateAttribute
16
16
  include ActiveRecordCompose::TransactionSupport
17
17
 
18
+ # This flag controls the callback sequence for models.
19
+ # The current default value is `false`, but support for `false` is planned to be discontinued in the future.
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: false
57
+
18
58
  define_model_callbacks :save
19
59
  define_model_callbacks :create
20
60
  define_model_callbacks :update
@@ -35,7 +75,11 @@ module ActiveRecordCompose
35
75
  return false if invalid?
36
76
 
37
77
  with_transaction_returning_status do
38
- run_callbacks(:save) { save_models(bang: false) }
78
+ if self.class.persisted_flag_callback_control
79
+ with_callbacks { save_models(bang: false) }
80
+ else
81
+ run_callbacks(:save) { save_models(bang: false) }
82
+ end
39
83
  rescue ActiveRecord::RecordInvalid
40
84
  false
41
85
  end
@@ -50,7 +94,11 @@ module ActiveRecordCompose
50
94
  valid? || raise_validation_error
51
95
 
52
96
  with_transaction_returning_status do
53
- run_callbacks(:save) { save_models(bang: true) }
97
+ if self.class.persisted_flag_callback_control
98
+ with_callbacks { save_models(bang: true) }
99
+ else
100
+ run_callbacks(:save) { save_models(bang: true) }
101
+ end
54
102
  end || raise_on_save_error
55
103
  end
56
104
 
@@ -80,11 +128,15 @@ module ActiveRecordCompose
80
128
  # # after_save called!
81
129
  #
82
130
  def create(attributes = {})
131
+ if self.class.persisted_flag_callback_control
132
+ raise '`#create` cannot be called. The context for creation or update is determined by the `#persisted` flag.'
133
+ end
134
+
83
135
  assign_attributes(attributes)
84
136
  return false if invalid?
85
137
 
86
138
  with_transaction_returning_status do
87
- run_callbacks(:save) { run_callbacks(:create) { save_models(bang: false) } }
139
+ with_callbacks(context: :create) { save_models(bang: false) }
88
140
  rescue ActiveRecord::RecordInvalid
89
141
  false
90
142
  end
@@ -93,11 +145,15 @@ module ActiveRecordCompose
93
145
  # Behavior is same to `#create`, but raises an exception prematurely on failure.
94
146
  #
95
147
  def create!(attributes = {})
148
+ if self.class.persisted_flag_callback_control
149
+ raise '`#create` cannot be called. The context for creation or update is determined by the `#persisted` flag.'
150
+ end
151
+
96
152
  assign_attributes(attributes)
97
153
  valid? || raise_validation_error
98
154
 
99
155
  with_transaction_returning_status do
100
- run_callbacks(:save) { run_callbacks(:create) { save_models(bang: true) } }
156
+ with_callbacks(context: :create) { save_models(bang: true) }
101
157
  end || raise_on_save_error
102
158
  end
103
159
 
@@ -131,7 +187,11 @@ module ActiveRecordCompose
131
187
  return false if invalid?
132
188
 
133
189
  with_transaction_returning_status do
134
- run_callbacks(:save) { run_callbacks(:update) { save_models(bang: false) } }
190
+ if self.class.persisted_flag_callback_control
191
+ with_callbacks { save_models(bang: false) }
192
+ else
193
+ with_callbacks(context: :update) { save_models(bang: false) }
194
+ end
135
195
  rescue ActiveRecord::RecordInvalid
136
196
  false
137
197
  end
@@ -144,22 +204,29 @@ module ActiveRecordCompose
144
204
  valid? || raise_validation_error
145
205
 
146
206
  with_transaction_returning_status do
147
- run_callbacks(:save) { run_callbacks(:update) { save_models(bang: true) } }
207
+ if self.class.persisted_flag_callback_control
208
+ with_callbacks { save_models(bang: true) }
209
+ else
210
+ with_callbacks(context: :update) { save_models(bang: true) }
211
+ end
148
212
  end || raise_on_save_error
149
213
  end
150
214
 
151
215
  private
152
216
 
153
- def models = @__models ||= ActiveRecordCompose::InnerModelCollection.new(self)
217
+ def models = @__models ||= ActiveRecordCompose::ComposedCollection.new(self)
154
218
 
155
219
  def validate_models
156
- wms = models.__wrapped_models
157
- wms.select { _1.invalid? }.each { errors.merge!(_1) }
220
+ models.__wrapped_models.select { _1.invalid? }.each { errors.merge!(_1) }
221
+ end
222
+
223
+ def with_callbacks(context: nil, &block)
224
+ context ||= persisted? ? :update : :create
225
+ run_callbacks(:save) { run_callbacks(context, &block) }
158
226
  end
159
227
 
160
228
  def save_models(bang:)
161
- wms = models.__wrapped_models
162
- wms.all? { bang ? _1.save! : _1.save }
229
+ models.__wrapped_models.all? { bang ? _1.save! : _1.save }
163
230
  end
164
231
 
165
232
  def raise_validation_error = raise ActiveRecord::RecordInvalid, self
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordCompose
4
- VERSION = '0.6.3'
4
+ VERSION = '0.7.0'
5
5
  end
@@ -3,7 +3,7 @@
3
3
  require 'active_support/core_ext/object'
4
4
 
5
5
  module ActiveRecordCompose
6
- class InnerModel
6
+ class WrappedModel
7
7
  # @param model [Object] the model instance.
8
8
  # @param destroy [Boolean] given true, destroy model.
9
9
  # @param destroy [Proc] when proc returning true, destroy model.
@@ -17,7 +17,7 @@ module ActiveRecordCompose
17
17
  delegate :errors, to: :model
18
18
 
19
19
  # Determines whether to save or delete the target object.
20
- # Depends on the `destroy` value of the InnerModel object initialization option.
20
+ # Depends on the `destroy` value of the WrappedModel object initialization option.
21
21
  #
22
22
  # On the other hand, there are values `mark_for_destruction` and `marked_for_destruction?` in ActiveRecord.
23
23
  # However, these values are not substituted here.
@@ -118,7 +118,7 @@ module ActiveRecordCompose
118
118
 
119
119
  # @private
120
120
  module PackagePrivate
121
- refine InnerModel do
121
+ refine WrappedModel do
122
122
  # @private
123
123
  # Returns a model instance of raw, but it should
124
124
  # be noted that application developers are not expected to use this interface.
@@ -1,4 +1,22 @@
1
1
  module ActiveRecordCompose
2
+ class ComposedCollection
3
+ def initialize: (Model) -> void
4
+
5
+ private
6
+ attr_reader owner: Model
7
+ attr_reader models: Array[WrappedModel]
8
+ def wrap: (ar_like, ?destroy: (bool | Symbol | destroy_context_type), ?if: (nil | Symbol | condition_type)) -> WrappedModel
9
+
10
+ module PackagePrivate
11
+ def __wrapped_models: () -> Array[WrappedModel]
12
+
13
+ private
14
+ def models: () -> Array[WrappedModel]
15
+ end
16
+
17
+ include PackagePrivate
18
+ end
19
+
2
20
  module DelegateAttribute : ActiveModel::Attributes
3
21
  extend ActiveSupport::Concern
4
22
 
@@ -12,25 +30,31 @@ module ActiveRecordCompose
12
30
  end
13
31
  end
14
32
 
15
- class InnerModelCollection
16
- def initialize: (Model) -> void
33
+ class Model
34
+ include DelegateAttribute
35
+ extend DelegateAttribute::ClassMethods
36
+ include TransactionSupport
37
+ extend TransactionSupport::ClassMethods
17
38
 
18
- private
19
- attr_reader owner: Model
20
- attr_reader models: Array[InnerModel]
21
- def wrap: (ar_like | InnerModel, ?destroy: (bool | Symbol | destroy_context_type), ?if: (nil | Symbol | condition_type)) -> InnerModel
39
+ @__models: ComposedCollection
40
+ end
22
41
 
23
- module PackagePrivate
24
- def __wrapped_models: () -> Array[InnerModel]
42
+ module TransactionSupport
43
+ include ActiveRecord::Transactions
44
+
45
+ def id: -> untyped
46
+
47
+ module ClassMethods
48
+ def connection: -> ActiveRecord::ConnectionAdapters::AbstractAdapter
49
+ def lease_connection: -> ActiveRecord::ConnectionAdapters::AbstractAdapter
50
+ def with_connection: [T] () { () -> T } -> T
25
51
 
26
52
  private
27
- def models: () -> Array[InnerModel]
53
+ def ar_class: -> singleton(ActiveRecord::Base)
28
54
  end
29
-
30
- include PackagePrivate
31
55
  end
32
56
 
33
- class InnerModel
57
+ class WrappedModel
34
58
  def initialize: (ar_like, ?destroy: (bool | destroy_context_type), ?if: (nil | condition_type)) -> void
35
59
  def destroy_context?: -> bool
36
60
  def ignore?: -> bool
@@ -55,28 +79,4 @@ module ActiveRecordCompose
55
79
 
56
80
  include PackagePrivate
57
81
  end
58
-
59
- class Model
60
- include DelegateAttribute
61
- extend DelegateAttribute::ClassMethods
62
- include TransactionSupport
63
- extend TransactionSupport::ClassMethods
64
-
65
- @__models: InnerModelCollection
66
- end
67
-
68
- module TransactionSupport
69
- include ActiveRecord::Transactions
70
-
71
- def id: -> untyped
72
-
73
- module ClassMethods
74
- def connection: -> ActiveRecord::ConnectionAdapters::AbstractAdapter
75
- def lease_connection: -> ActiveRecord::ConnectionAdapters::AbstractAdapter
76
- def with_connection: [T] () { () -> T } -> T
77
-
78
- private
79
- def ar_class: -> singleton(ActiveRecord::Base)
80
- end
81
- end
82
82
  end
@@ -34,15 +34,15 @@ module ActiveRecordCompose
34
34
  type destroy_context_type = ((^() -> boolish) | (^(ar_like) -> boolish))
35
35
  type condition_type = ((^() -> boolish) | (^(ar_like) -> boolish))
36
36
 
37
- class InnerModelCollection
37
+ class ComposedCollection
38
38
  include ::Enumerable[ar_like]
39
39
 
40
- def each: () { (ar_like) -> void } -> InnerModelCollection | () -> Enumerator[ar_like, self]
40
+ def each: () { (ar_like) -> void } -> ComposedCollection | () -> Enumerator[ar_like, self]
41
41
  def <<: (ar_like) -> self
42
42
  def push: (ar_like, ?destroy: (bool | Symbol | destroy_context_type), ?if: (nil | Symbol | condition_type)) -> self
43
43
  def empty?: -> bool
44
44
  def clear: -> self
45
- def delete: (ar_like) -> InnerModelCollection?
45
+ def delete: (ar_like) -> ComposedCollection?
46
46
  end
47
47
 
48
48
  class Model
@@ -69,6 +69,9 @@ module ActiveRecordCompose
69
69
  def self.after_commit: (*callback[instance], ?if: condition[instance], ?unless: condition[instance], **untyped) ?{ () [self: instance] -> void } -> void
70
70
  def self.after_rollback: (*callback[instance], ?if: condition[instance], ?unless: condition[instance], **untyped) ?{ () [self: instance] -> void } -> void
71
71
 
72
+ def self.persisted_flag_callback_control: () -> boolish
73
+ def self.persisted_flag_callback_control=: (boolish) -> untyped
74
+
72
75
  def self.delegate_attribute: (*untyped methods, to: untyped, ?allow_nil: untyped?, ?private: untyped?) -> untyped
73
76
  def self.connection: -> ActiveRecord::ConnectionAdapters::AbstractAdapter
74
77
  def self.lease_connection: -> ActiveRecord::ConnectionAdapters::AbstractAdapter
@@ -84,7 +87,8 @@ module ActiveRecordCompose
84
87
  def id: -> untyped
85
88
 
86
89
  private
87
- def models: -> InnerModelCollection
90
+ def models: -> ComposedCollection
91
+ def with_callbacks: (?context: (nil | :create | :update)) { () -> bool } -> bool
88
92
  def validate_models: -> void
89
93
  def save_models: (bang: bool) -> bool
90
94
  def raise_validation_error: -> bot
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.6.3
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - hamajyotan
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-01-31 00:00:00.000000000 Z
10
+ date: 2025-02-12 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activerecord
@@ -37,12 +37,12 @@ files:
37
37
  - LICENSE.txt
38
38
  - README.md
39
39
  - lib/active_record_compose.rb
40
+ - lib/active_record_compose/composed_collection.rb
40
41
  - lib/active_record_compose/delegate_attribute.rb
41
- - lib/active_record_compose/inner_model.rb
42
- - lib/active_record_compose/inner_model_collection.rb
43
42
  - lib/active_record_compose/model.rb
44
43
  - lib/active_record_compose/transaction_support.rb
45
44
  - lib/active_record_compose/version.rb
45
+ - lib/active_record_compose/wrapped_model.rb
46
46
  - sig/_internal/package_private.rbs
47
47
  - sig/active_record_compose.rbs
48
48
  homepage: https://github.com/hamajyotan/active_record_compose
@@ -52,7 +52,7 @@ metadata:
52
52
  homepage_uri: https://github.com/hamajyotan/active_record_compose
53
53
  source_code_uri: https://github.com/hamajyotan/active_record_compose
54
54
  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.6.3
55
+ documentation_uri: https://www.rubydoc.info/gems/active_record_compose/0.7.0
56
56
  rubygems_mfa_required: 'true'
57
57
  rdoc_options: []
58
58
  require_paths: