active_record_compose 0.6.3 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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: