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 +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +176 -165
- data/lib/active_record_compose/delegate_attribute.rb +5 -10
- data/lib/active_record_compose/inner_model.rb +26 -4
- data/lib/active_record_compose/inner_model_collection.rb +3 -5
- data/lib/active_record_compose/model.rb +3 -3
- data/lib/active_record_compose/version.rb +1 -1
- data/sig/_internal/package_private.rbs +82 -0
- data/sig/active_record_compose.rbs +46 -61
- metadata +7 -9
- data/.rspec +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6a3d2a1c6e4aac0bc387a6e0d9ba2917dafeacec4d46b2ad617d54e45ff9786f
|
4
|
+
data.tar.gz: d71b88c9b069eca62e49d2e5ff25c447a5e70e741d29b76f572c5a21b4994a4b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
###
|
49
|
+
### Basic usage
|
24
50
|
|
25
|
-
|
26
|
-
|
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
|
-
|
59
|
+
class Profile < ApplicationRecord
|
60
|
+
belongs_to :account
|
61
|
+
validates :firstname, :lastname, :age, presence: true
|
62
|
+
end
|
63
|
+
```
|
29
64
|
|
30
|
-
|
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
|
38
|
-
def initialize
|
39
|
-
@account =
|
40
|
-
|
68
|
+
class UserRegistration < ActiveRecordCompose::Model
|
69
|
+
def initialize
|
70
|
+
@account = Account.new
|
71
|
+
@profile = @account.build_profile
|
41
72
|
|
42
|
-
#
|
43
|
-
|
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
|
-
#
|
47
|
-
|
48
|
-
delegate :id, :persisted?, to: :account
|
82
|
+
# Attribute declarations using ActiveModel::Attributes are supported.
|
83
|
+
attribute :terms_of_service, :boolean
|
49
84
|
|
50
|
-
#
|
51
|
-
|
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
|
-
#
|
54
|
-
#
|
55
|
-
|
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
|
106
|
+
def send_email_message
|
62
107
|
SendEmailConfirmationJob.perform_later(account)
|
63
108
|
end
|
64
109
|
end
|
65
110
|
```
|
66
111
|
|
67
|
-
|
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
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
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
|
-
|
87
|
-
|
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
|
-
|
138
|
+
### `delegate_attribute`
|
90
139
|
|
91
|
-
|
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
|
-
|
109
|
-
account.
|
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
|
-
|
112
|
-
|
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
|
-
|
170
|
+
### Promotion to model from AR-model errors
|
116
171
|
|
117
|
-
|
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
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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
|
-
|
132
|
-
delegate_attribute :name, :email, to: :account
|
133
|
-
delegate_attribute :firstname, :lastname, :age, to: :profile
|
193
|
+
### I18n
|
134
194
|
|
135
|
-
|
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
|
-
|
138
|
-
|
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
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
-
|
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
|
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
|
262
|
+
def initialize(account)
|
201
263
|
@account = account
|
202
264
|
@profile = account.profile || account.build_profile
|
203
|
-
super(
|
265
|
+
super()
|
204
266
|
models.push(account)
|
205
|
-
|
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 :
|
209
|
-
delegate_attribute :
|
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
|
216
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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:)
|
56
|
-
delegated_attributes =
|
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
|
-
|
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
|
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!
|
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
|
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 }
|
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)
|
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
|
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
|
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
|
161
|
+
wms = models.__wrapped_models
|
162
162
|
wms.all? { bang ? _1.save! : _1.save }
|
163
163
|
end
|
164
164
|
|
@@ -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
|
19
|
-
type
|
20
|
-
type
|
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
|
-
|
28
|
-
|
29
|
-
|
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[
|
38
|
+
include ::Enumerable[ar_like]
|
34
39
|
|
35
|
-
def
|
36
|
-
def
|
37
|
-
def
|
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: (
|
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
|
-
|
72
|
-
extend
|
73
|
-
|
74
|
-
extend
|
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
|
-
|
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.
|
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:
|
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.
|
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.
|
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