active_record_compose 0.6.1 → 0.6.3

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: 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