active_record_compose 0.6.1 → 0.6.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a87013307c9d2f0532ca4365765d94dc1423705c9d7df309af4f0c856d5803a6
4
- data.tar.gz: fc650023b00d26306a222800f16949ffdf65c096028e07ccd855bed5b16258f9
3
+ metadata.gz: 6a3d2a1c6e4aac0bc387a6e0d9ba2917dafeacec4d46b2ad617d54e45ff9786f
4
+ data.tar.gz: d71b88c9b069eca62e49d2e5ff25c447a5e70e741d29b76f572c5a21b4994a4b
5
5
  SHA512:
6
- metadata.gz: 95b6fdee6e5977ab27a7973aeb241a48f9dede2d05aa3685586b549db1f74dc5e4bf8df9e49e66fbe8e1bcf8be3f1dfaca4ff183dc67bd42771d10db64a371be
7
- data.tar.gz: 410b89dd9af2e0878aa6646996d2ad48b33297b1b932c1dfb2caf21b284e7a2cb2a2fd7559d55a135984484719b44304086ae280692188a5001d1cd2759e3dcb
6
+ metadata.gz: 3579fdc36270eaaa2f9f253b247aabc5bc4aa6547df5511e81158ebbf3277b37dbbaa3e4d4376fff5c9ac0066cebbf0207db993cd20b4c8b159a6f3069ae24f1
7
+ data.tar.gz: 4f2e98a4158a5c19c13f5bc5d83276c7071ece19b2c969d73c24cce47231c9f272fb32620d5a31748e2bd060d72733bcd28be3d4d3eb6c1e9c8ded1bb9db0340
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.6.2] - 2025-01-31
4
+
5
+ - fix: type error in `ActiveRecordCompose::Model` subclass definitions.
6
+ - fixed type errors in subclass callback definitions, etc.
7
+ - doc: more detailed gem desciption.
8
+ - rewrite readme.
9
+
10
+ ## [0.6.2] - 2025-01-04
11
+
12
+ - fix: `delegate_attribute` defined in a subclass had an unintended side effect on the superclass.
13
+ - support ruby 3.4.x
14
+ - refactor: remove some `steep:ignore` by private rbs.
15
+ - refactor: place definitions that you don't want to be used much in private rbs.
16
+ - make `DelegateAttribute` dependent on `ActiveModel::Attributes` since it will not change in practice.
17
+
3
18
  ## [0.6.1] - 2024-12-23
4
19
 
5
20
  - refactor: reorganize the overall structure of the test. Change from rspec to minitest
data/README.md CHANGED
@@ -1,9 +1,35 @@
1
1
  # ActiveRecordCompose
2
2
 
3
- activermodel (activerecord) form object pattern.
3
+ activemodel (activerecord) form object pattern. it embraces multiple AR models and provides a transparent interface as if they were a single model.
4
4
 
5
5
  ![CI](https://github.com/hamajyotan/active_record_compose/workflows/CI/badge.svg)
6
6
 
7
+ ## Table of Contents
8
+
9
+ - [Motivation](#motivation)
10
+ - [Installation](#installation)
11
+ - [Usage](#usage)
12
+ - [Basic usage](#basic-usage)
13
+ - [`delegate_attribute`](#delegate_attribute)
14
+ - [Promotion to model from AR-model errors](#promotion-to-model-from-ar-model-errors)
15
+ - [I18n](#i18n)
16
+ - [Advanced Usage](#advanced-usage)
17
+ - [`destroy` option](#destroy-option)
18
+ - [Callback ordering by `#save`, `#create` and `#update`](#callback-ordering-by-save-create-and-update)
19
+ - [Links](#links)
20
+ - [Development](#development)
21
+ - [Contributing](#contributing)
22
+ - [License](#license)
23
+ - [Code of Conduct](#code-of-conduct)
24
+
25
+ ## Motivation
26
+
27
+ `ActiveRecord::Base` is responsible for persisting data to the database, and by defining validations and callbacks, it allows you to structure your use cases effectively. This is a crucial component of Rails. However, when a specific model starts being updated by multiple different use cases, validations and callbacks may require conditions such as `on: :context` or `save(validate: false)`. As a result, the model needs to account for multiple dependent use cases, leading to increased complexity.
28
+
29
+ In such cases, `ActiveModel::Model` becomes useful. It provides the same interfaces as `ActiveRecord::Base`, such as `attribute` and `errors`, allowing it to be used similarly to an ActiveRecord model. Additionally, it enables you to define validations and callbacks within a limited context, preventing conditions related to multiple contexts from being embedded in `ActiveRecord::Base` validations and callbacks. This results in simpler, more maintainable code.
30
+
31
+ This gem is built on `ActiveModel::Model` and acts as a first-class model within the Rails context. It provides methods for performing batch and safe updates on 0..N encapsulated models, enables transparent attribute access, and facilitates access to error information.
32
+
7
33
  ## Installation
8
34
 
9
35
  To install `active_record_compose`, just put this line in your Gemfile:
@@ -20,149 +46,186 @@ $ bundle
20
46
 
21
47
  ## Usage
22
48
 
23
- ### ActiveRecordCompose::Model basic
49
+ ### Basic usage
24
50
 
25
- It wraps AR objects or equivalent models to provide unified operation.
26
- For example, the following cases are supported
51
+ (Below, it is assumed that there are two AR model definitions, `Account` and `Profile`, for the sake of explanation.)
52
+
53
+ ```ruby
54
+ class Account < ApplicationRecord
55
+ has_one :profile # can work without `autosave:true`
56
+ validates :name, :email, presence: true
57
+ end
27
58
 
28
- #### Context-specific callbacks.
59
+ class Profile < ApplicationRecord
60
+ belongs_to :account
61
+ validates :firstname, :lastname, :age, presence: true
62
+ end
63
+ ```
29
64
 
30
- A callback is useful to define some processing before or after a save in a particular model.
31
- However, if a callback is written directly in the AR model, it is necessary to consider the case where the model is updated in other contexts.
32
- In particular, if you frequently create with test data, previously unnecessary processing will be called at every point of creation.
33
- In addition to cost, the more complicated the callbacks you write, the more difficult it will be to create even a single test data.
34
- If the callbacks are written in a class that inherits from `ActiveRecordCompose::Model`, the AR model itself will not be polluted, and the context can be limited.
65
+ Here is an example of designing a model that updates both Account and Profile at the same time, using `ActiveRecordCompose::Model`.
35
66
 
36
67
  ```ruby
37
- class AccountRegistration < ActiveRecordCompose::Model
38
- def initialize(account = Account.new, attributes = {})
39
- @account = account
40
- super(attributes) # When overrides `#initialize`, be sure to call `super`.
68
+ class UserRegistration < ActiveRecordCompose::Model
69
+ def initialize
70
+ @account = Account.new
71
+ @profile = @account.build_profile
41
72
 
42
- # By including AR instance in models, AR instance itself is saved when this model is saved.
43
- models.push(account)
73
+ super() # Don't forget to call `super()`
74
+ # RuboCop's Lint/MissingSuper cop assists in addressing this.
75
+
76
+ models << account << profile
77
+ # Alternatively, it can also be written as follows:
78
+ # models.push(account)
79
+ # models.push(profile)
44
80
  end
45
81
 
46
- # By delegating these to the AR instance,
47
- # For example, this model itself can be given directly as an argument to form_with, and it will behave as if it were an instance of the model.
48
- delegate :id, :persisted?, to: :account
82
+ # Attribute declarations using ActiveModel::Attributes are supported.
83
+ attribute :terms_of_service, :boolean
49
84
 
50
- # Defines an attribute of the same name that delegates to account#name and account#email
51
- delegate_attribute :name, :email, to: :account
85
+ # You can provide validation definitions limited to UserRegistration.
86
+ # Instead of directly defining validations for Account or Profile, such
87
+ # as `on: :create` in the context, the model itself explains the context.
88
+ validates :terms_of_service, presence: true
89
+ validates :email, confirmation: true
90
+
91
+ # You can provide callback definitions limited to UserRegistration.
92
+ # For example, if this is written directly in the AR model, you need to consider
93
+ # callback control for data generation during tests and other situations.
94
+ after_commit :send_email_message
52
95
 
53
- # You can only define post-processing if you update through this model.
54
- # If this is written directly into the AR model, for example, it would be necessary to consider a callback control for each test data generation.
55
- after_commit :try_send_email_message
96
+ # UserRegistration behaves as if it has attributes like email, name, and age
97
+ # For example, `email` is delegated to `account.email`,
98
+ # and `email=` is delegated to `account.email=`.
99
+ delegate_attribute :name, :email, to: :account
100
+ delegate_attribute :firstname, :lastname, :age, to: :profile
56
101
 
57
102
  private
58
103
 
59
- attr_reader :account
104
+ attr_reader :account, :profile
60
105
 
61
- def try_send_email_message
106
+ def send_email_message
62
107
  SendEmailConfirmationJob.perform_later(account)
63
108
  end
64
109
  end
65
110
  ```
66
111
 
67
- #### Validation limited to a specific context.
68
-
69
- Validates are basically fired in all cases where the model is manipulated. To avoid this, use `on: :create`, etc. to make it work only in specific cases.
70
- and so on to work only in specific cases. This allows you to create context-sensitive validations for the same model operation.
71
- However, this is the first step in making the model more and more complex. You will have to go around with `update(context: :foo)`
72
- In some cases, you may have to go around with the context option, such as `update(context: :foo)` everywhere.
73
- By writing validates in a class that extends `ActiveRecordCompose::Model`, you can define context-specific validation without polluting the AR model itself.
112
+ The above model is used as follows.
74
113
 
75
114
  ```ruby
76
- class AccountRegistration < ActiveRecordCompose::Model
77
- def initialize(account = Account.new, attributes = {})
78
- @account = account
79
- super(attributes)
80
- models.push(account)
81
- end
115
+ registration = UserRegistration.new
116
+
117
+ # Atomically update Account and Profile.
118
+ registration.update!(
119
+ name: "foo",
120
+ email: "bar@example.com",
121
+ firstname: "taro",
122
+ lastname: "yamada",
123
+ age: 18,
124
+ email_confirmation: "bar@example.com",
125
+ terms_of_service: true,
126
+ )
127
+ ```
82
128
 
83
- delegate :id, :persisted?, to: :account
84
- delegate_attribute :name, :email, to: :account
129
+ By executing `save`, you can simultaneously update multiple models added to `models`. Furthermore, the save operation is performed within a database transaction, ensuring atomic processing.
85
130
 
86
- # Only if this model is used, also check the validity of the domain
87
- before_validation :require_valid_domain
131
+ ```ruby
132
+ user_registration.save # Atomically update Account and Profile.
133
+ # In case of failure, a false value is returned.
134
+ user_registration.save! # With the bang method,
135
+ # an exception is raised in case of failure.
136
+ ```
88
137
 
89
- private
138
+ ### `delegate_attribute`
90
139
 
91
- attr_reader :account
92
-
93
- # Validity of the domain part of the e-mail address is also checked only when registering an account.
94
- def require_valid_domain
95
- e = ValidEmail2::Address.new(email.to_s)
96
- unless e.valid?
97
- errors.add(:email, :invalid_format)
98
- return
99
- end
100
- unless e.valid_mx?
101
- errors.add(:email, :invalid_domain)
102
- end
103
- end
104
- end
105
- ```
140
+ In many cases, the composed models have attributes that need to be assigned before saving. `ActiveRecordCompose::Model` provides `delegate_attribute`, allowing transparent access to those attributes."
106
141
 
107
142
  ```ruby
108
- account = Account.new(name: 'new account', email: 'foo@example.com')
109
- account.valid? #=> true
143
+ # UserRegistration behaves as if it has attributes like email, name, and age
144
+ # For example, `email` is delegated to `account.email`,
145
+ # and `email=` is delegated to `account.email=`.
146
+ delegate_attribute :name, :email, to: :account
147
+ delegate_attribute :firstname, :lastname, :age, to: :profile
148
+ ```
149
+
150
+ Attributes defined with `.delegate_attribute` can be accessed through `#attributes` in the same way as the original attributes defined with `.attribute`.
110
151
 
111
- account_registration = AccountRegistration.new(name: 'new account', email: 'foo@example.com')
112
- account_registration.valid? #=> false
152
+ ```ruby
153
+ registration = UserRegistration.new
154
+ registration.name = "foo"
155
+ registration.terms_of_service = true
156
+
157
+ # Not only the email_confirmation defined with attribute,
158
+ # but also the attributes defined with delegate_attribute are included.
159
+ registration.attributes
160
+ # => {
161
+ # "terms_of_service" => true,
162
+ # "email" => nil,
163
+ # "name" => "foo",
164
+ # "age" => nil,
165
+ # "firstname" => nil,
166
+ # "lastname" => nil
167
+ # }
113
168
  ```
114
169
 
115
- #### updating multiple models at the same time.
170
+ ### Promotion to model from AR-model errors
116
171
 
117
- In an AR model, you can add, for example, `autosave: true` or `accepts_nested_attributes_for` to an association to update the related models at the same time.
118
- There are ways to update related models at the same time. The operation is safe because it is transactional.
119
- `ActiveRecordCompose::Model` has an internal array called models. By adding an AR object to this models array
120
- By adding an AR object to the models, the object stored in the models provides an atomic update operation via #save.
172
+ When saving a composed model with `#save`, models that are not valid with `#valid?` will obviously not be saved. As a result, the #errors information can be accessed from `ActiveRecordCompose::Model`.
121
173
 
122
174
  ```ruby
123
- class AccountRegistration < ActiveRecordCompose::Model
124
- def initialize(account = Account.new, profile = account.build_profile, attributes = {})
125
- @account = account
126
- @profile = profile
127
- super(attributes)
128
- models << account << profile
129
- end
175
+ user_registration = UserRegistration.new
176
+ user_registration.email = "foo@example.com"
177
+ user_registration.email_confirmation = "BAZ@example.com"
178
+ user_registration.age = 18
179
+ user_registration.terms_of_service = true
180
+
181
+ user_registration.save
182
+ #=> false
183
+
184
+ user_registration.errors.to_a
185
+ # => [
186
+ # "Name can't be blank",
187
+ # "Firstname can't be blank",
188
+ # "Lastname can't be blank",
189
+ # "Email confirmation doesn't match Email"
190
+ # ]
191
+ ```
130
192
 
131
- delegate :id, :persisted?, to: :account
132
- delegate_attribute :name, :email, to: :account
133
- delegate_attribute :firstname, :lastname, :age, to: :profile
193
+ ### I18n
134
194
 
135
- private
195
+ When the `#save!` operation raises an `ActiveRecord::RecordInvalid` exception, it is necessary to have pre-existing locale definitions in order to construct i18n information correctly.
196
+ The specific keys required are `activemodel.errors.messages.record_invalid` or `errors.messages.record_invalid`.
136
197
 
137
- attr_reader :account, :profile
138
- end
198
+ (Replace `en` as appropriate in the context.)
199
+
200
+ ```yaml
201
+ en:
202
+ activemodel:
203
+ errors:
204
+ messages:
205
+ record_invalid: 'Validation failed: %{errors}'
139
206
  ```
140
207
 
141
- ```ruby
142
- Account.count #=> 0
143
- Profile.count #=> 0
144
-
145
- account_registration =
146
- AccountRegistration.new(
147
- name: 'foo',
148
- email: 'foo@example.com',
149
- firstname: 'bar',
150
- lastname: 'baz',
151
- age: 36,
152
- )
153
- account_registration.save!
154
-
155
- Account.count #=> 1
156
- Profile.count #=> 1
208
+ Alternatively, the following definition is also acceptable:
209
+
210
+ ```yaml
211
+ en:
212
+ errors:
213
+ messages:
214
+ record_invalid: 'Validation failed: %{errors}'
157
215
  ```
158
216
 
159
- By adding to the `models` array while specifying `destroy: true`, you can perform a delete instead of a save on the model at `#save` time.
217
+ ## Advanced Usage
218
+
219
+ ### `destroy` option
220
+
221
+ By adding to the models array while specifying destroy: true, you can perform a delete instead of a save on the model at #save time.
160
222
 
161
223
  ```ruby
162
224
  class AccountResignation < ActiveRecordCompose::Model
163
225
  def initialize(account)
164
226
  @account = account
165
- @profile = account.profile # Suppose that Account has_one Profile.
227
+ @profile = account.profile || account.build_profile
228
+ super()
166
229
  models.push(account)
167
230
  models.push(profile, destroy: true)
168
231
  end
@@ -178,7 +241,6 @@ class AccountResignation < ActiveRecordCompose::Model
178
241
  end
179
242
  end
180
243
  ```
181
-
182
244
  ```ruby
183
245
  account = Account.last
184
246
 
@@ -197,63 +259,31 @@ Conditional destroy (or save) can be written like this.
197
259
 
198
260
  ```ruby
199
261
  class AccountRegistration < ActiveRecordCompose::Model
200
- def initialize(account, attributes = {})
262
+ def initialize(account)
201
263
  @account = account
202
264
  @profile = account.profile || account.build_profile
203
- super(attributes)
265
+ super()
204
266
  models.push(account)
205
- models.push(profile, destroy: :all_blank?) # destroy if all blank, otherwise save.
267
+
268
+ # destroy if all blank, otherwise save.
269
+ models.push(profile, destroy: :profile_field_is_blank?)
270
+ # Alternatively, it can also be written as follows:
271
+ # models.push(profile, destroy: -> { profile_field_is_blank? })
206
272
  end
207
273
 
208
- delegate_attribute :name, :email, to: :account
209
- delegate_attribute :firstname, :lastname, :age, to: :profile
274
+ delegate_attribute :email, to: :account
275
+ delegate_attribute :name, :age, to: :profile
210
276
 
211
277
  private
212
278
 
213
279
  attr_reader :account, :profile
214
280
 
215
- def all_blank? = firstname.blank && lastname.blank? && age.blank?
216
- end
217
- ```
218
-
219
- ### `delegate_attribute`
220
-
221
- It provides a macro description that expresses access to the attributes of the AR model through delegation.
222
-
223
- ```ruby
224
- class AccountRegistration < ActiveRecordCompose::Model
225
- def initialize(account, attributes = {})
226
- @account = account
227
- super(attributes)
228
- models.push(account)
281
+ def profile_field_is_blank?
282
+ firstname.blank? && lastname.blank? && age.blank?
229
283
  end
230
-
231
- attribute :original_attribute, :string, default: 'qux'
232
- delegate_attribute :name, to: :account
233
-
234
- private
235
-
236
- attr_reader :account
237
284
  end
238
285
  ```
239
286
 
240
- ```ruby
241
- account = Account.new
242
- account.name = 'foo'
243
-
244
- registration = AccountRegistration.new(account)
245
- registration.name #=> 'foo'
246
-
247
- registration.name = 'bar'
248
- account.name #=> 'bar'
249
- ```
250
-
251
- Overrides `#attributes`, merging attributes defined with `delegate_attribute` in addition to the original attributes.
252
-
253
- ```
254
- account.attributes #=> {'original_attribute' => 'qux', 'name' => 'bar'}
255
- ```
256
-
257
287
  ### Callback ordering by `#save`, `#create` and `#update`.
258
288
 
259
289
  Sometimes, multiple AR objects are passed to the models in the arguments.
@@ -294,29 +324,9 @@ model.update
294
324
  # after_save called!
295
325
  ```
296
326
 
297
- ### I18n
298
-
299
- When the `#save!` operation raises an `ActiveRecord::RecordInvalid` exception, it is necessary to have pre-existing locale definitions in order to construct i18n information correctly.
300
- The specific keys required are `activemodel.errors.messages.record_invalid` or `errors.messages.record_invalid`.
301
-
302
- (Replace `en` as appropriate in the context.)
303
-
304
- ```yaml
305
- en:
306
- activemodel:
307
- errors:
308
- messages:
309
- record_invalid: 'Validation failed: %{errors}'
310
- ```
311
-
312
- Alternatively, the following definition is also acceptable:
327
+ ## Links
313
328
 
314
- ```yaml
315
- en:
316
- errors:
317
- messages:
318
- record_invalid: 'Validation failed: %{errors}'
319
- ```
329
+ - [Smart way to update multiple models simultaneously in Rails](https://dev.to/hamajyotan/smart-way-to-update-multiple-models-simultaneously-in-rails-51b6)
320
330
 
321
331
  ## Development
322
332
 
@@ -335,3 +345,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
335
345
  ## Code of Conduct
336
346
 
337
347
  Everyone interacting in the ActiveRecord::Compose project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/hamajyotan/active_record_compose/blob/main/CODE_OF_CONDUCT.md).
348
+
@@ -38,7 +38,8 @@ module ActiveRecordCompose
38
38
  extend ActiveSupport::Concern
39
39
 
40
40
  included do
41
- class_attribute :delegated_attributes, instance_writer: false # steep:ignore
41
+ # @type self: Class
42
+ class_attribute :delegated_attributes, instance_writer: false
42
43
  end
43
44
 
44
45
  module ClassMethods
@@ -52,9 +53,8 @@ module ActiveRecordCompose
52
53
  [reader, writer]
53
54
  end
54
55
 
55
- delegate(*delegates, to:, allow_nil:, private:) # steep:ignore
56
- delegated_attributes = (self.delegated_attributes ||= []) # steep:ignore
57
- attributes.each { delegated_attributes.push(_1.to_s) }
56
+ delegate(*delegates, to:, allow_nil:, private:)
57
+ self.delegated_attributes = delegated_attributes.to_a + attributes.map { _1.to_s }
58
58
  end
59
59
  end
60
60
 
@@ -63,12 +63,7 @@ module ActiveRecordCompose
63
63
  #
64
64
  # @return [Hash] hash with the attribute name as key and the attribute value as value.
65
65
  def attributes
66
- attrs = defined?(super) ? super : {} # steep:ignore
67
- delegates = delegated_attributes # steep:ignore
68
-
69
- # @type var attrs: Hash[String, untyped]
70
- # @type var delegates: Array[String]
71
- attrs.merge(delegates.to_h { [_1, public_send(_1)] })
66
+ super.merge(delegated_attributes.to_h { [_1, public_send(_1)] })
72
67
  end
73
68
  end
74
69
  end
@@ -60,12 +60,36 @@ 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 = destroy_context? ? model.destroy : model.save
63
+ def save
64
+ # While errors caused by the type check are avoided,
65
+ # it is important to note that an error can still occur
66
+ # if `#destroy_context?` returns true but ar_like does not implement `#destroy`.
67
+ m = model
68
+ if destroy_context?
69
+ # @type var m: ActiveRecordCompose::_ARLikeWithDestroy
70
+ m.destroy
71
+ else
72
+ # @type var m: ActiveRecordCompose::_ARLike
73
+ m.save
74
+ end
75
+ end
64
76
 
65
77
  # Execute save or destroy. Unlike #save, an exception is raises on failure.
66
78
  # Whether save or destroy is executed depends on the value of `#destroy_context?`.
67
79
  #
68
- def save! = destroy_context? ? model.destroy! : model.save!
80
+ def save!
81
+ # While errors caused by the type check are avoided,
82
+ # it is important to note that an error can still occur
83
+ # if `#destroy_context?` returns true but ar_like does not implement `#destroy`.
84
+ m = model
85
+ if destroy_context?
86
+ # @type var m: ActiveRecordCompose::_ARLikeWithDestroy
87
+ m.destroy!
88
+ else
89
+ # @type var model: ActiveRecordCompose::_ARLike
90
+ m.save!
91
+ end
92
+ end
69
93
 
70
94
  # @return [Boolean]
71
95
  def invalid? = destroy_context? ? false : model.invalid?
@@ -93,7 +117,6 @@ module ActiveRecordCompose
93
117
  attr_reader :destroy_context_type, :if_option
94
118
 
95
119
  # @private
96
- # steep:ignore:start
97
120
  module PackagePrivate
98
121
  refine InnerModel do
99
122
  # @private
@@ -104,6 +127,5 @@ module ActiveRecordCompose
104
127
  def __raw_model = model
105
128
  end
106
129
  end
107
- # steep:ignore:end
108
130
  end
109
131
  end
@@ -3,7 +3,7 @@
3
3
  require 'active_record_compose/inner_model'
4
4
 
5
5
  module ActiveRecordCompose
6
- using InnerModel::PackagePrivate # steep:ignore
6
+ using InnerModel::PackagePrivate
7
7
 
8
8
  class InnerModelCollection
9
9
  include Enumerable
@@ -21,7 +21,7 @@ module ActiveRecordCompose
21
21
  def each
22
22
  return enum_for(:each) unless block_given?
23
23
 
24
- models.each { yield _1.__raw_model } # steep:ignore
24
+ models.each { yield _1.__raw_model }
25
25
  self
26
26
  end
27
27
 
@@ -79,7 +79,7 @@ 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) # steep:ignore
82
+ if model.is_a?(ActiveRecordCompose::InnerModel)
83
83
  # @type var model: ActiveRecordCompose::InnerModel
84
84
  model
85
85
  else
@@ -97,7 +97,6 @@ module ActiveRecordCompose
97
97
  end
98
98
 
99
99
  # @private
100
- # steep:ignore:start
101
100
  module PackagePrivate
102
101
  refine InnerModelCollection do
103
102
  # Returns array of wrapped model instance.
@@ -107,6 +106,5 @@ module ActiveRecordCompose
107
106
  def __wrapped_models = models.reject { _1.ignore? }.select { _1.__raw_model }
108
107
  end
109
108
  end
110
- # steep:ignore:end
111
109
  end
112
110
  end
@@ -5,7 +5,7 @@ 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 # steep:ignore
8
+ using InnerModelCollection::PackagePrivate
9
9
 
10
10
  class Model
11
11
  include ActiveModel::Model
@@ -153,12 +153,12 @@ module ActiveRecordCompose
153
153
  def models = @__models ||= ActiveRecordCompose::InnerModelCollection.new(self)
154
154
 
155
155
  def validate_models
156
- wms = models.__wrapped_models # steep:ignore
156
+ wms = models.__wrapped_models
157
157
  wms.select { _1.invalid? }.each { errors.merge!(_1) }
158
158
  end
159
159
 
160
160
  def save_models(bang:)
161
- wms = models.__wrapped_models # steep:ignore
161
+ wms = models.__wrapped_models
162
162
  wms.all? { bang ? _1.save! : _1.save }
163
163
  end
164
164
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordCompose
4
- VERSION = '0.6.1'
4
+ VERSION = '0.6.3'
5
5
  end
@@ -0,0 +1,82 @@
1
+ module ActiveRecordCompose
2
+ module DelegateAttribute : ActiveModel::Attributes
3
+ extend ActiveSupport::Concern
4
+
5
+ def attributes: -> Hash[String, untyped]
6
+ def delegated_attributes: () -> Array[String]
7
+
8
+ module ClassMethods : Module
9
+ def delegate_attribute: (*untyped methods, to: untyped, ?allow_nil: untyped?, ?private: untyped?) -> untyped
10
+ def delegated_attributes: () -> Array[String]
11
+ def delegated_attributes=: (Array[String]) -> untyped
12
+ end
13
+ end
14
+
15
+ class InnerModelCollection
16
+ def initialize: (Model) -> void
17
+
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
22
+
23
+ module PackagePrivate
24
+ def __wrapped_models: () -> Array[InnerModel]
25
+
26
+ private
27
+ def models: () -> Array[InnerModel]
28
+ end
29
+
30
+ include PackagePrivate
31
+ end
32
+
33
+ class InnerModel
34
+ def initialize: (ar_like, ?destroy: (bool | destroy_context_type), ?if: (nil | condition_type)) -> void
35
+ def destroy_context?: -> bool
36
+ def ignore?: -> bool
37
+ def save: -> bool
38
+ def save!: -> untyped
39
+ def invalid?: -> bool
40
+ def valid?: -> bool
41
+ def is_a?: (untyped) -> bool
42
+ def ==: (untyped) -> bool
43
+
44
+ private
45
+ attr_reader model: ar_like
46
+ attr_reader destroy_context_type: (bool | destroy_context_type)
47
+ attr_reader if_option: (nil | condition_type)
48
+
49
+ module PackagePrivate
50
+ def __raw_model: () -> ar_like
51
+
52
+ private
53
+ def model: () -> ar_like
54
+ end
55
+
56
+ include PackagePrivate
57
+ 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
+ end
@@ -5,6 +5,15 @@ 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
12
+ def errors: -> untyped
13
+ def is_a?: (untyped) -> bool
14
+ def ==: (untyped) -> bool
15
+ end
16
+ interface _ARLikeWithDestroy
8
17
  def save: -> bool
9
18
  def save!: -> untyped
10
19
  def destroy: -> bool
@@ -12,68 +21,58 @@ module ActiveRecordCompose
12
21
  def invalid?: -> bool
13
22
  def valid?: -> bool
14
23
  def errors: -> untyped
24
+ def is_a?: (untyped) -> bool
15
25
  def ==: (untyped) -> bool
16
26
  end
27
+ type ar_like = (_ARLike | _ARLikeWithDestroy)
17
28
 
18
- type attribute_name = (String | Symbol)
19
- type destroy_context_type = (bool | Symbol | (^() -> boolish) | (^(_ARLike) -> boolish))
20
- type condition_type = ((^() -> boolish) | (^(_ARLike) -> boolish))
21
-
22
- module DelegateAttribute
23
- extend ActiveSupport::Concern
24
-
25
- def attributes: -> Hash[String, untyped]
29
+ type condition[T] = Symbol | ^(T) [self: T] -> boolish
30
+ type callback[T] = Symbol | ^(T) [self: T] -> void
31
+ type around_callback[T] = Symbol | ^(T, Proc) [self: T] -> void
26
32
 
27
- module ClassMethods
28
- def delegate_attribute: (*untyped methods, to: untyped?, ?allow_nil: untyped?, ?private: untyped?) -> untyped
29
- end
30
- end
33
+ type attribute_name = (String | Symbol)
34
+ type destroy_context_type = ((^() -> boolish) | (^(ar_like) -> boolish))
35
+ type condition_type = ((^() -> boolish) | (^(ar_like) -> boolish))
31
36
 
32
37
  class InnerModelCollection
33
- include ::Enumerable[_ARLike]
38
+ include ::Enumerable[ar_like]
34
39
 
35
- def initialize: (Model) -> void
36
- def each: () { (_ARLike) -> void } -> InnerModelCollection | () -> Enumerator[_ARLike, self]
37
- def <<: (_ARLike) -> self
38
- def push: (_ARLike, ?destroy: destroy_context_type, ?if: (nil | Symbol | condition_type)) -> self
40
+ def each: () { (ar_like) -> void } -> InnerModelCollection | () -> Enumerator[ar_like, self]
41
+ def <<: (ar_like) -> self
42
+ def push: (ar_like, ?destroy: (bool | Symbol | destroy_context_type), ?if: (nil | Symbol | condition_type)) -> self
39
43
  def empty?: -> bool
40
44
  def clear: -> self
41
- def delete: (_ARLike | InnerModel) -> InnerModelCollection?
42
-
43
- private
44
- attr_reader owner: Model
45
- attr_reader models: Array[InnerModel]
46
- def wrap: (_ARLike | InnerModel, ?destroy: destroy_context_type, ?if: (nil | Symbol | condition_type)) -> InnerModel
47
- end
48
-
49
- class InnerModel
50
- def initialize: (_ARLike, ?destroy: destroy_context_type, ?if: (nil | condition_type)) -> void
51
- def destroy_context?: -> bool
52
- def ignore?: -> bool
53
- def save: -> bool
54
- def save!: -> untyped
55
- def invalid?: -> bool
56
- def valid?: -> bool
57
- def ==: (untyped) -> bool
58
-
59
- private
60
- attr_reader model: _ARLike
61
- attr_reader destroy_context_type: destroy_context_type
62
- attr_reader if_option: (nil | condition_type)
45
+ def delete: (ar_like) -> InnerModelCollection?
63
46
  end
64
47
 
65
48
  class Model
66
- extend ActiveModel::Callbacks
67
49
  include ActiveModel::Model
68
50
  include ActiveModel::Validations::Callbacks
69
- extend ActiveModel::Validations::ClassMethods
70
51
  include ActiveModel::Attributes
71
- include DelegateAttribute
72
- extend DelegateAttribute::ClassMethods
73
- include TransactionSupport
74
- extend TransactionSupport::ClassMethods
52
+ extend ActiveModel::Callbacks
53
+ extend ActiveModel::Validations::ClassMethods
54
+ extend ActiveModel::Validations::Callbacks::ClassMethods
55
+ extend ActiveModel::Attributes::ClassMethods
56
+
57
+ def self.before_save: (*callback[instance], ?if: condition[instance], ?unless: condition[instance], **untyped) ?{ () [self: instance] -> void } -> void
58
+ def self.around_save: (*around_callback[instance], ?if: condition[instance], ?unless: condition[instance], **untyped) ?{ () [self: instance] -> void } -> void
59
+ def self.after_save: (*callback[instance], ?if: condition[instance], ?unless: condition[instance], **untyped) ?{ () [self: instance] -> void } -> void
60
+
61
+ def self.before_create: (*callback[instance], ?if: condition[instance], ?unless: condition[instance], **untyped) ?{ () [self: instance] -> void } -> void
62
+ def self.around_create: (*around_callback[instance], ?if: condition[instance], ?unless: condition[instance], **untyped) ?{ () [self: instance] -> void } -> void
63
+ def self.after_create: (*callback[instance], ?if: condition[instance], ?unless: condition[instance], **untyped) ?{ () [self: instance] -> void } -> void
75
64
 
76
- @__models: InnerModelCollection
65
+ def self.before_update: (*callback[instance], ?if: condition[instance], ?unless: condition[instance], **untyped) ?{ () [self: instance] -> void } -> void
66
+ def self.around_update: (*around_callback[instance], ?if: condition[instance], ?unless: condition[instance], **untyped) ?{ () [self: instance] -> void } -> void
67
+ def self.after_update: (*callback[instance], ?if: condition[instance], ?unless: condition[instance], **untyped) ?{ () [self: instance] -> void } -> void
68
+
69
+ def self.after_commit: (*callback[instance], ?if: condition[instance], ?unless: condition[instance], **untyped) ?{ () [self: instance] -> void } -> void
70
+ def self.after_rollback: (*callback[instance], ?if: condition[instance], ?unless: condition[instance], **untyped) ?{ () [self: instance] -> void } -> void
71
+
72
+ def self.delegate_attribute: (*untyped methods, to: untyped, ?allow_nil: untyped?, ?private: untyped?) -> untyped
73
+ def self.connection: -> ActiveRecord::ConnectionAdapters::AbstractAdapter
74
+ def self.lease_connection: -> ActiveRecord::ConnectionAdapters::AbstractAdapter
75
+ def self.with_connection: [T] () { () -> T } -> T
77
76
 
78
77
  def initialize: (?Hash[attribute_name, untyped]) -> void
79
78
  def save: -> bool
@@ -82,6 +81,7 @@ module ActiveRecordCompose
82
81
  def create!: (?Hash[attribute_name, untyped]) -> untyped
83
82
  def update: (?Hash[attribute_name, untyped]) -> bool
84
83
  def update!: (?Hash[attribute_name, untyped]) -> untyped
84
+ def id: -> untyped
85
85
 
86
86
  private
87
87
  def models: -> InnerModelCollection
@@ -91,19 +91,4 @@ module ActiveRecordCompose
91
91
  def raise_on_save_error: -> bot
92
92
  def raise_on_save_error_message: -> String
93
93
  end
94
-
95
- module TransactionSupport
96
- include ActiveRecord::Transactions
97
-
98
- def id: -> untyped
99
-
100
- module ClassMethods
101
- def connection: -> ActiveRecord::ConnectionAdapters::AbstractAdapter
102
- def lease_connection: -> ActiveRecord::ConnectionAdapters::AbstractAdapter
103
- def with_connection: [T] () { () -> T } -> T
104
-
105
- private
106
- def ar_class: -> singleton(ActiveRecord::Base)
107
- end
108
- end
109
94
  end
metadata CHANGED
@@ -1,14 +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.1
4
+ version: 0.6.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - hamajyotan
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-12-22 00:00:00.000000000 Z
10
+ date: 2025-01-31 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activerecord
@@ -24,14 +23,14 @@ dependencies:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
25
  version: '6.1'
27
- description: activemodel form object pattern
26
+ description: activemodel form object pattern. it embraces multiple AR models and provides
27
+ a transparent interface as if they were a single model.
28
28
  email:
29
29
  - hamajyotan@gmail.com
30
30
  executables: []
31
31
  extensions: []
32
32
  extra_rdoc_files: []
33
33
  files:
34
- - ".rspec"
35
34
  - ".rubocop.yml"
36
35
  - CHANGELOG.md
37
36
  - CODE_OF_CONDUCT.md
@@ -44,6 +43,7 @@ files:
44
43
  - lib/active_record_compose/model.rb
45
44
  - lib/active_record_compose/transaction_support.rb
46
45
  - lib/active_record_compose/version.rb
46
+ - sig/_internal/package_private.rbs
47
47
  - sig/active_record_compose.rbs
48
48
  homepage: https://github.com/hamajyotan/active_record_compose
49
49
  licenses:
@@ -52,9 +52,8 @@ 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.1
55
+ documentation_uri: https://www.rubydoc.info/gems/active_record_compose/0.6.3
56
56
  rubygems_mfa_required: 'true'
57
- post_install_message:
58
57
  rdoc_options: []
59
58
  require_paths:
60
59
  - lib
@@ -69,8 +68,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
69
68
  - !ruby/object:Gem::Version
70
69
  version: '0'
71
70
  requirements: []
72
- rubygems_version: 3.5.21
73
- signing_key:
71
+ rubygems_version: 3.6.2
74
72
  specification_version: 4
75
73
  summary: activemodel form object pattern
76
74
  test_files: []
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper