active_record_compose 0.11.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +123 -230
- data/lib/active_record_compose/attributes/attribute_predicate.rb +29 -0
- data/lib/active_record_compose/attributes/delegation.rb +7 -1
- data/lib/active_record_compose/attributes/querying.rb +5 -12
- data/lib/active_record_compose/attributes.rb +3 -29
- data/lib/active_record_compose/callbacks.rb +0 -4
- data/lib/active_record_compose/model.rb +4 -2
- data/lib/active_record_compose/persistence.rb +12 -20
- data/lib/active_record_compose/transaction_support.rb +11 -10
- data/lib/active_record_compose/validations.rb +1 -0
- data/lib/active_record_compose/version.rb +1 -1
- data/sig/_internal/package_private.rbs +14 -2
- data/sig/active_record_compose.rbs +2 -6
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9cdfb23b1af43681e6c4af3e9fed615bc3dc64873461bdee6f49c92f706f33e9
|
4
|
+
data.tar.gz: 4530c0987f038605cb6dc3da229df21e8cf16af29d6ca8cf3078b1dc933cc838
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 10c37b41ff66acd993e2f5c412ed5f48213f29344a94e0ab27d40766e11f60197808d4d1b9e97118f729b4be1d272a4bdae100c8e06489c40008ac3c92f6fe61
|
7
|
+
data.tar.gz: 67c4086fce4661e89188161fd6d23610bca3dcb9ad8a6e781162909d07fbf750add3ad804d8e58bd92bf4fc1305bcebd2452a4a9d924af70e56e6a0217ecf70f
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,21 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.0.0] - 2025-09-23
|
4
|
+
|
5
|
+
- drop support rails 7.0.x
|
6
|
+
|
7
|
+
## [0.12.0] - 2025-08-21
|
8
|
+
|
9
|
+
- Omits default arguments for `#update` and `#update!`. It's to align I/F with ActiveRecord.
|
10
|
+
(https://github.com/hamajyotan/active_record_compose/pull/25)
|
11
|
+
- `#update(attributes = {})` to `#update(attributes)`
|
12
|
+
- `#update!(attributes = {})` to `#update!(attributes)`
|
13
|
+
- Omitted Specify instance variables in the `:to` option of `delegate_attribute`.
|
14
|
+
(https://github.com/hamajyotan/active_record_compose/pull/29)
|
15
|
+
- Omitted `#destroy` and `#touch` from `ActiveRecordCompose::Model`.
|
16
|
+
These were unintentionally provided by the `ActiveRecord::Transactions` module. The but in fact did not work correctly.
|
17
|
+
(https://github.com/hamajyotan/active_record_compose/pull/27)
|
18
|
+
|
3
19
|
## [0.11.3] - 2025-07-13
|
4
20
|
|
5
21
|
- refactor: Aggregation attribute module.
|
data/README.md
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# ActiveRecordCompose
|
2
2
|
|
3
|
-
|
3
|
+
ActiveRecordCompose lets you build form objects that combine multiple ActiveRecord models into a single, unified interface.
|
4
|
+
More than just a simple form object, it is designed as a **business-oriented composed model** that encapsulates complex operations-such as user registration spanning multiple tables-making them easier to write, validate, and maintain.
|
4
5
|
|
5
6
|
[](https://badge.fury.io/rb/active_record_compose)
|
6
7
|

|
@@ -10,16 +11,16 @@ activemodel (activerecord) form object pattern. it embraces multiple AR models a
|
|
10
11
|
|
11
12
|
- [Motivation](#motivation)
|
12
13
|
- [Installation](#installation)
|
13
|
-
- [
|
14
|
-
- [Basic
|
15
|
-
|
16
|
-
|
17
|
-
- [I18n](#i18n)
|
14
|
+
- [Quick Start](#quick-start)
|
15
|
+
- [Basic Example](#basic-example)
|
16
|
+
- [Attribute Delegation](#attribute-delegation)
|
17
|
+
- [Unified Error Handling](#unified-error-handling)
|
18
|
+
- [I18n Support](#i18n-support)
|
18
19
|
- [Advanced Usage](#advanced-usage)
|
19
|
-
- [
|
20
|
-
- [Callback ordering
|
21
|
-
- [
|
22
|
-
- [Sample
|
20
|
+
- [Destroy Option](#destroy-option)
|
21
|
+
- [Callback ordering with `#persisted?`](#callback-ordering-with-persisted)
|
22
|
+
- [Notes on adding models dynamically](#notes-on-adding-models-dynamically)
|
23
|
+
- [Sample Application](#sample-application)
|
23
24
|
- [Links](#links)
|
24
25
|
- [Development](#development)
|
25
26
|
- [Contributing](#contributing)
|
@@ -28,11 +29,20 @@ activemodel (activerecord) form object pattern. it embraces multiple AR models a
|
|
28
29
|
|
29
30
|
## Motivation
|
30
31
|
|
31
|
-
`ActiveRecord::Base` is responsible for persisting data to the database
|
32
|
+
In Rails, `ActiveRecord::Base` is responsible for persisting data to the database.
|
33
|
+
By defining validations and callbacks, you can model use cases effectively.
|
32
34
|
|
33
|
-
|
35
|
+
However, when a single model must serve multiple different use cases, you often end up with conditional validations (`on: :context`) or workarounds like `save(validate: false)`.
|
36
|
+
This mixes unrelated concerns into one model, leading to unnecessary complexity.
|
34
37
|
|
35
|
-
|
38
|
+
`ActiveModel::Model` helps here — it provides the familiar API (`attribute`, `errors`, validations, callbacks) without persistence, so you can isolate logic per use case.
|
39
|
+
|
40
|
+
**ActiveRecordCompose** builds on `ActiveModel::Model` and is a powerful **business object** that acts as a first-class model within Rails.
|
41
|
+
- Transparently accesses attributes across multiple models
|
42
|
+
- Saves all associated models atomically in a transaction
|
43
|
+
- Collects and exposes error information consistently
|
44
|
+
|
45
|
+
This leads to cleaner domain models, better separation of concerns, and fewer surprises in validations and callbacks.
|
36
46
|
|
37
47
|
## Installation
|
38
48
|
|
@@ -48,15 +58,15 @@ Then bundle
|
|
48
58
|
$ bundle
|
49
59
|
```
|
50
60
|
|
51
|
-
##
|
61
|
+
## Quick Start
|
52
62
|
|
53
|
-
### Basic
|
63
|
+
### Basic Example
|
54
64
|
|
55
|
-
|
65
|
+
Suppose you have two models:
|
56
66
|
|
57
67
|
```ruby
|
58
68
|
class Account < ApplicationRecord
|
59
|
-
has_one :profile
|
69
|
+
has_one :profile
|
60
70
|
validates :name, :email, presence: true
|
61
71
|
end
|
62
72
|
|
@@ -66,40 +76,23 @@ class Profile < ApplicationRecord
|
|
66
76
|
end
|
67
77
|
```
|
68
78
|
|
69
|
-
|
79
|
+
You can compose them into one form object:
|
70
80
|
|
71
81
|
```ruby
|
72
82
|
class UserRegistration < ActiveRecordCompose::Model
|
73
|
-
def initialize
|
83
|
+
def initialize(attributes = {})
|
74
84
|
@account = Account.new
|
75
85
|
@profile = @account.build_profile
|
76
|
-
|
77
|
-
super() # Don't forget to call `super()`
|
78
|
-
# RuboCop's Lint/MissingSuper cop assists in addressing this.
|
79
|
-
|
86
|
+
super(attributes)
|
80
87
|
models << account << profile
|
81
|
-
# Alternatively, it can also be written as follows:
|
82
|
-
# models.push(account)
|
83
|
-
# models.push(profile)
|
84
88
|
end
|
85
89
|
|
86
|
-
# Attribute declarations using ActiveModel::Attributes are supported.
|
87
90
|
attribute :terms_of_service, :boolean
|
88
|
-
|
89
|
-
# You can provide validation definitions limited to UserRegistration.
|
90
|
-
# Instead of directly defining validations for Account or Profile, such
|
91
|
-
# as `on: :create` in the context, the model itself explains the context.
|
92
91
|
validates :terms_of_service, presence: true
|
93
92
|
validates :email, confirmation: true
|
94
93
|
|
95
|
-
# You can provide callback definitions limited to UserRegistration.
|
96
|
-
# For example, if this is written directly in the AR model, you need to consider
|
97
|
-
# callback control for data generation during tests and other situations.
|
98
94
|
after_commit :send_email_message
|
99
95
|
|
100
|
-
# UserRegistration behaves as if it has attributes like email, name, and age
|
101
|
-
# For example, `email` is delegated to `account.email`,
|
102
|
-
# and `email=` is delegated to `account.email=`.
|
103
96
|
delegate_attribute :name, :email, to: :account
|
104
97
|
delegate_attribute :firstname, :lastname, :age, to: :profile
|
105
98
|
|
@@ -113,12 +106,11 @@ class UserRegistration < ActiveRecordCompose::Model
|
|
113
106
|
end
|
114
107
|
```
|
115
108
|
|
116
|
-
|
109
|
+
Usage:
|
117
110
|
|
118
111
|
```ruby
|
112
|
+
# === Standalone script ===
|
119
113
|
registration = UserRegistration.new
|
120
|
-
|
121
|
-
# Atomically update Account and Profile.
|
122
114
|
registration.update!(
|
123
115
|
name: "foo",
|
124
116
|
email: "bar@example.com",
|
@@ -128,38 +120,41 @@ registration.update!(
|
|
128
120
|
email_confirmation: "bar@example.com",
|
129
121
|
terms_of_service: true,
|
130
122
|
)
|
131
|
-
```
|
132
123
|
|
133
|
-
|
124
|
+
# === Or, in a Rails controller with strong parameters ===
|
125
|
+
class UserRegistrationsController < ApplicationController
|
126
|
+
def create
|
127
|
+
@registration = UserRegistration.new(user_registration_params)
|
128
|
+
if @registration.save
|
129
|
+
redirect_to root_path, notice: "Registered!"
|
130
|
+
else
|
131
|
+
render :new
|
132
|
+
end
|
133
|
+
end
|
134
134
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
135
|
+
private
|
136
|
+
def user_registration_params
|
137
|
+
params.require(:user_registration).permit(
|
138
|
+
:name, :email, :firstname, :lastname, :age, :email_confirmation, :terms_of_service
|
139
|
+
)
|
140
|
+
end
|
141
|
+
end
|
140
142
|
```
|
141
143
|
|
142
|
-
|
144
|
+
Both `Account` and `Profile` will be updated **atomically in one transaction**.
|
145
|
+
|
146
|
+
### Attribute Delegation
|
143
147
|
|
144
|
-
|
148
|
+
`delegate_attribute` allows transparent access to attributes of inner models:
|
145
149
|
|
146
150
|
```ruby
|
147
|
-
|
148
|
-
|
149
|
-
# and `email=` is delegated to `account.email=`.
|
150
|
-
delegate_attribute :name, :email, to: :account
|
151
|
-
delegate_attribute :firstname, :lastname, :age, to: :profile
|
151
|
+
delegate_attribute :name, :email, to: :account
|
152
|
+
delegate_attribute :firstname, :lastname, :age, to: :profile
|
152
153
|
```
|
153
154
|
|
154
|
-
|
155
|
+
They are also included in `#attributes`:
|
155
156
|
|
156
157
|
```ruby
|
157
|
-
registration = UserRegistration.new
|
158
|
-
registration.name = "foo"
|
159
|
-
registration.terms_of_service = true
|
160
|
-
|
161
|
-
# Not only the email_confirmation defined with attribute,
|
162
|
-
# but also the attributes defined with delegate_attribute are included.
|
163
158
|
registration.attributes
|
164
159
|
# => {
|
165
160
|
# "terms_of_service" => true,
|
@@ -171,21 +166,21 @@ registration.attributes
|
|
171
166
|
# }
|
172
167
|
```
|
173
168
|
|
174
|
-
###
|
169
|
+
### Unified Error Handling
|
175
170
|
|
176
|
-
|
171
|
+
Validation errors from inner models are collected into the composed model:
|
177
172
|
|
178
173
|
```ruby
|
179
|
-
user_registration = UserRegistration.new
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
174
|
+
user_registration = UserRegistration.new(
|
175
|
+
email: "foo@example.com",
|
176
|
+
email_confirmation: "BAZ@example.com",
|
177
|
+
age: 18,
|
178
|
+
terms_of_service: true,
|
179
|
+
)
|
184
180
|
|
185
|
-
user_registration.save
|
186
|
-
#=> false
|
181
|
+
user_registration.save # => false
|
187
182
|
|
188
|
-
user_registration.errors.
|
183
|
+
user_registration.errors.full_messages
|
189
184
|
# => [
|
190
185
|
# "Name can't be blank",
|
191
186
|
# "Firstname can't be blank",
|
@@ -194,12 +189,10 @@ user_registration.errors.to_a
|
|
194
189
|
# ]
|
195
190
|
```
|
196
191
|
|
197
|
-
### I18n
|
192
|
+
### I18n Support
|
198
193
|
|
199
|
-
When
|
200
|
-
|
201
|
-
|
202
|
-
(Replace `en` as appropriate in the context.)
|
194
|
+
When `#save!` raises `ActiveRecord::RecordInvalid`,
|
195
|
+
make sure you have locale entries such as:
|
203
196
|
|
204
197
|
```yaml
|
205
198
|
en:
|
@@ -209,204 +202,104 @@ en:
|
|
209
202
|
record_invalid: 'Validation failed: %{errors}'
|
210
203
|
```
|
211
204
|
|
212
|
-
|
213
|
-
|
214
|
-
```yaml
|
215
|
-
en:
|
216
|
-
errors:
|
217
|
-
messages:
|
218
|
-
record_invalid: 'Validation failed: %{errors}'
|
219
|
-
```
|
205
|
+
For more complete usage patterns, see the [Sample Application](#sample-application) below.
|
220
206
|
|
221
207
|
## Advanced Usage
|
222
208
|
|
223
|
-
###
|
209
|
+
### Destroy Option
|
224
210
|
|
225
|
-
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.
|
226
|
-
|
227
|
-
```ruby
|
228
|
-
class AccountResignation < ActiveRecordCompose::Model
|
229
|
-
def initialize(account)
|
230
|
-
@account = account
|
231
|
-
@profile = account.profile || account.build_profile
|
232
|
-
super()
|
233
|
-
models.push(account)
|
234
|
-
models.push(profile, destroy: true)
|
235
|
-
end
|
236
|
-
|
237
|
-
before_save :set_resigned_at
|
238
|
-
|
239
|
-
private
|
240
|
-
|
241
|
-
attr_reader :account, :profile
|
242
|
-
|
243
|
-
def set_resigned_at
|
244
|
-
account.resigned_at = Time.zone.now
|
245
|
-
end
|
246
|
-
end
|
247
|
-
```
|
248
211
|
```ruby
|
249
|
-
|
250
|
-
|
251
|
-
account.resigned_at.present? #=> nil
|
252
|
-
account.profile.blank? #=> false
|
253
|
-
|
254
|
-
account_resignation = AccountResignation.new(account)
|
255
|
-
account_resignation.save!
|
256
|
-
|
257
|
-
account.reload
|
258
|
-
account.resigned_at.present? #=> Tue, 02 Jan 2024 22:58:01.991008870 JST +09:00
|
259
|
-
account.profile.blank? #=> true
|
212
|
+
models.push(profile, destroy: true)
|
260
213
|
```
|
261
214
|
|
262
|
-
|
215
|
+
This deletes the model on `#save` instead of persisting it.
|
216
|
+
Conditional deletion is also supported:
|
263
217
|
|
264
218
|
```ruby
|
265
|
-
|
266
|
-
|
267
|
-
@account = account
|
268
|
-
@profile = account.profile || account.build_profile
|
269
|
-
super()
|
270
|
-
models.push(account)
|
271
|
-
|
272
|
-
# destroy if all blank, otherwise save.
|
273
|
-
models.push(profile, destroy: :profile_field_is_blank?)
|
274
|
-
# Alternatively, it can also be written as follows:
|
275
|
-
# models.push(profile, destroy: -> { profile_field_is_blank? })
|
276
|
-
end
|
219
|
+
models.push(profile, destroy: -> { profile_field_is_blank? })
|
220
|
+
```
|
277
221
|
|
278
|
-
|
279
|
-
delegate_attribute :name, :age, to: :profile
|
222
|
+
### Callback ordering with `#persisted?`
|
280
223
|
|
281
|
-
|
224
|
+
The result of `#persisted?` determines **which callbacks are fired**:
|
282
225
|
|
283
|
-
|
226
|
+
- `persisted? == false` -> create callbacks (`before_create`, `after_create`, ...)
|
227
|
+
- `persisted? == true` -> update callbacks (`before_update`, `after_update`, ...)
|
284
228
|
|
285
|
-
|
286
|
-
firstname.blank? && lastname.blank? && age.blank?
|
287
|
-
end
|
288
|
-
end
|
289
|
-
```
|
290
|
-
|
291
|
-
### Callback ordering by `#persisted?`
|
292
|
-
|
293
|
-
The behavior of `(before|after|around)_create` and `(before|after|around)_update` hooks depending on the evaluation result of `#persisted?`,
|
294
|
-
either the create-related callbacks or the update-related callbacks will be triggered.
|
229
|
+
This matches the behavior of normal ActiveRecord models.
|
295
230
|
|
296
231
|
```ruby
|
297
232
|
class ComposedModel < ActiveRecordCompose::Model
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
after_save
|
304
|
-
after_create { puts 'after_create called!' }
|
305
|
-
after_update { puts 'after_update called!' }
|
233
|
+
before_save { puts "before_save" }
|
234
|
+
before_create { puts "before_create" }
|
235
|
+
before_update { puts "before_update" }
|
236
|
+
after_create { puts "after_create" }
|
237
|
+
after_update { puts "after_update" }
|
238
|
+
after_save { puts "after_save" }
|
306
239
|
|
307
240
|
def persisted?
|
308
|
-
|
309
|
-
# For example, it could be transferred to the primary model to be manipulated.
|
310
|
-
#
|
311
|
-
# # ex.)
|
312
|
-
# def persisted? = the_model.persisted?
|
313
|
-
#
|
314
|
-
true
|
241
|
+
account.persisted?
|
315
242
|
end
|
316
243
|
end
|
317
244
|
```
|
318
245
|
|
319
|
-
|
320
|
-
# when `model.persisted?` returns `true`
|
246
|
+
Example:
|
321
247
|
|
248
|
+
```ruby
|
249
|
+
# When persisted? == false
|
322
250
|
model = ComposedModel.new
|
323
251
|
|
324
|
-
model.save
|
325
|
-
|
326
|
-
#
|
327
|
-
#
|
328
|
-
#
|
329
|
-
# after_save called!
|
330
|
-
```
|
331
|
-
|
332
|
-
```ruby
|
333
|
-
# when `model.persisted?` returns `false`
|
252
|
+
model.save
|
253
|
+
# => before_save
|
254
|
+
# => before_create
|
255
|
+
# => after_create
|
256
|
+
# => after_save
|
334
257
|
|
258
|
+
# When persisted? == true
|
335
259
|
model = ComposedModel.new
|
260
|
+
def model.persisted?; true; end
|
336
261
|
|
337
|
-
model.save
|
338
|
-
|
339
|
-
#
|
340
|
-
#
|
341
|
-
#
|
342
|
-
# after_save called!
|
262
|
+
model.save
|
263
|
+
# => before_save
|
264
|
+
# => before_update
|
265
|
+
# => after_update
|
266
|
+
# => after_save
|
343
267
|
```
|
344
268
|
|
345
|
-
###
|
346
|
-
|
347
|
-
The interface remains consistent with standard ActiveModel and ActiveRecord models, so the :context option works with #save.
|
348
|
-
|
349
|
-
```ruby
|
350
|
-
composed_model.valid?(:custom_context)
|
351
|
-
|
352
|
-
composed_model.save(context: :custom_context)
|
353
|
-
```
|
269
|
+
### Notes on adding models dynamically
|
354
270
|
|
355
|
-
|
356
|
-
|
271
|
+
Avoid adding `models` to the models array **after validation has already run**
|
272
|
+
(for example, inside `after_validation` or `before_save` callbacks).
|
357
273
|
|
358
274
|
```ruby
|
359
|
-
class
|
360
|
-
|
361
|
-
validates :email, presence: true
|
362
|
-
validates :email, format: { with: /\.edu\z/ }, on: :education
|
275
|
+
class Example < ActiveRecordCompose::Model
|
276
|
+
before_save { models << AnotherModel.new }
|
363
277
|
end
|
278
|
+
```
|
364
279
|
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
super(attributes)
|
369
|
-
end
|
370
|
-
|
371
|
-
attribute :accept, :boolean
|
372
|
-
validates :accept, presence: true, on: :education
|
280
|
+
In this case, the newly added model will **not** run validations for the current save cycle.
|
281
|
+
This may look like a bug, but it is the expected behavior: validations are only applied
|
282
|
+
to models that were registered before validation started.
|
373
283
|
|
374
|
-
|
375
|
-
|
376
|
-
|
284
|
+
We intentionally do not restrict this at the framework level, since there may be valid
|
285
|
+
advanced use cases where models are manipulated dynamically.
|
286
|
+
Instead, this behavior is documented here so that developers can make an informed decision.
|
377
287
|
|
378
|
-
|
379
|
-
end
|
380
|
-
```
|
381
|
-
```ruby
|
382
|
-
r = Registration.new(name: 'foo', email: 'example@example.com', accept: false)
|
383
|
-
r.valid?
|
384
|
-
#=> true
|
385
|
-
|
386
|
-
r.valid?(:education)
|
387
|
-
#=> false
|
388
|
-
r.errors.map { [_1.attribute, _1.type] }
|
389
|
-
#=> [[:email, :invalid], [:accept, :blank]]
|
390
|
-
|
391
|
-
r.email = 'example@example.edu'
|
392
|
-
r.accept = true
|
393
|
-
|
394
|
-
r.valid?(:education)
|
395
|
-
#=> true
|
396
|
-
r.save(context: :education)
|
397
|
-
#=> true
|
398
|
-
```
|
288
|
+
## Sample Application
|
399
289
|
|
400
|
-
|
290
|
+
The sample app demonstrates a more complete usage of ActiveRecordCompose
|
291
|
+
(e.g., user registration flows involving multiple models).
|
292
|
+
It is not meant to cover every possible pattern, but can serve as a reference
|
293
|
+
for putting the library into practice.
|
401
294
|
|
402
|
-
|
295
|
+
Try it out in your browser with GitHub Codespaces (or locally):
|
403
296
|
|
404
297
|
- https://github.com/hamajyotan/active_record_compose-example
|
405
298
|
|
406
299
|
## Links
|
407
300
|
|
408
|
-
- [
|
409
|
-
- [
|
301
|
+
- [API Documentation (YARD)](https://hamajyotan.github.io/active_record_compose/)
|
302
|
+
- [Blog article introducing the concept](https://dev.to/hamajyotan/smart-way-to-update-multiple-models-simultaneously-in-rails-51b6)
|
410
303
|
|
411
304
|
## Development
|
412
305
|
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordCompose
|
4
|
+
module Attributes
|
5
|
+
# @private
|
6
|
+
class AttributePredicate
|
7
|
+
def initialize(value)
|
8
|
+
@value = value
|
9
|
+
end
|
10
|
+
|
11
|
+
def call
|
12
|
+
case value
|
13
|
+
when true then true
|
14
|
+
when false, nil then false
|
15
|
+
else
|
16
|
+
if value.respond_to?(:zero?)
|
17
|
+
!value.zero?
|
18
|
+
else
|
19
|
+
value.present?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
attr_reader :value
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "attribute_predicate"
|
4
|
+
|
3
5
|
module ActiveRecordCompose
|
4
6
|
module Attributes
|
5
7
|
# @private
|
@@ -17,7 +19,11 @@ module ActiveRecordCompose
|
|
17
19
|
|
18
20
|
def define_delegated_attribute(klass)
|
19
21
|
klass.delegate(reader, writer, to:, allow_nil:)
|
20
|
-
klass.
|
22
|
+
klass.module_eval <<~RUBY, __FILE__, __LINE__ + 1
|
23
|
+
def #{reader}?
|
24
|
+
ActiveRecordCompose::Attributes::AttributePredicate.new(#{reader}).call
|
25
|
+
end
|
26
|
+
RUBY
|
21
27
|
end
|
22
28
|
|
23
29
|
# @return [String] The attribute name as string
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "attribute_predicate"
|
4
|
+
|
3
5
|
module ActiveRecordCompose
|
4
6
|
module Attributes
|
5
7
|
# @private
|
@@ -50,19 +52,10 @@ module ActiveRecordCompose
|
|
50
52
|
|
51
53
|
private
|
52
54
|
|
53
|
-
def attribute?(attr_name)
|
54
|
-
value = public_send(attr_name)
|
55
|
+
def attribute?(attr_name) = query?(public_send(attr_name))
|
55
56
|
|
56
|
-
|
57
|
-
|
58
|
-
when false, nil then false
|
59
|
-
else
|
60
|
-
if value.respond_to?(:zero?)
|
61
|
-
!value.zero?
|
62
|
-
else
|
63
|
-
value.present?
|
64
|
-
end
|
65
|
-
end
|
57
|
+
def query?(value)
|
58
|
+
ActiveRecordCompose::Attributes::AttributePredicate.new(value).call
|
66
59
|
end
|
67
60
|
end
|
68
61
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "attributes/attribute_predicate"
|
3
4
|
require_relative "attributes/delegation"
|
4
5
|
require_relative "attributes/querying"
|
5
6
|
|
@@ -62,9 +63,6 @@ module ActiveRecordCompose
|
|
62
63
|
end
|
63
64
|
|
64
65
|
module ClassMethods
|
65
|
-
ALLOW_NIL_DEFAULT = Object.new.freeze # steep:ignore
|
66
|
-
private_constant :ALLOW_NIL_DEFAULT
|
67
|
-
|
68
66
|
# Defines the reader and writer for the specified attribute.
|
69
67
|
#
|
70
68
|
# @example
|
@@ -97,34 +95,10 @@ module ActiveRecordCompose
|
|
97
95
|
# registration.attributes
|
98
96
|
# # => { "original_attribute" => "qux", "name" => "bar" }
|
99
97
|
#
|
100
|
-
def delegate_attribute(*attributes, to:, allow_nil:
|
101
|
-
# steep:ignore:start
|
98
|
+
def delegate_attribute(*attributes, to:, allow_nil: false)
|
102
99
|
if to.start_with?("@")
|
103
|
-
|
104
|
-
suggested_method =
|
105
|
-
if to.start_with?("@@")
|
106
|
-
"def #{suggested_reader_name} = #{to}"
|
107
|
-
else
|
108
|
-
"attr_reader :#{suggested_reader_name}"
|
109
|
-
end
|
110
|
-
|
111
|
-
message = <<~MSG
|
112
|
-
Direct use of instance or class variables in `to:` will be removed in the next minor version.
|
113
|
-
Please define a reader method (private is fine) and refer to it by name instead.
|
114
|
-
|
115
|
-
For example,
|
116
|
-
delegate_attribute #{attributes.map { ":#{_1}" }.join(", ")}, to: :#{to}#{", allow_nil: #{allow_nil}" if allow_nil != ALLOW_NIL_DEFAULT}
|
117
|
-
|
118
|
-
Instead of the above, use the following
|
119
|
-
delegate_attribute #{attributes.map { ":#{_1}" }.join(", ")}, to: :#{suggested_reader_name}#{", allow_nil: #{allow_nil}" if allow_nil != ALLOW_NIL_DEFAULT}
|
120
|
-
private
|
121
|
-
#{suggested_method}
|
122
|
-
|
123
|
-
MSG
|
124
|
-
(ActiveRecord.respond_to?(:deprecator) ? ActiveRecord.deprecator : ActiveSupport::Deprecation).warn(message)
|
100
|
+
raise ArgumentError, "Instance variables cannot be specified in delegate to. (#{to})"
|
125
101
|
end
|
126
|
-
allow_nil = false if allow_nil == ALLOW_NIL_DEFAULT
|
127
|
-
# steep:ignore:end
|
128
102
|
|
129
103
|
delegations = attributes.map { Delegation.new(attribute: _1, to:, allow_nil:) }
|
130
104
|
delegations.each { _1.define_delegated_attribute(self) }
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "validations"
|
4
|
-
|
5
3
|
module ActiveRecordCompose
|
6
4
|
# @private
|
7
5
|
#
|
@@ -23,8 +21,6 @@ module ActiveRecordCompose
|
|
23
21
|
include ActiveModel::Validations::Callbacks
|
24
22
|
|
25
23
|
included do
|
26
|
-
include ActiveRecordCompose::Validations
|
27
|
-
|
28
24
|
define_model_callbacks :save
|
29
25
|
define_model_callbacks :create
|
30
26
|
define_model_callbacks :update
|
@@ -1,9 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "attributes"
|
4
|
-
require_relative "callbacks"
|
5
4
|
require_relative "composed_collection"
|
6
5
|
require_relative "persistence"
|
6
|
+
require_relative "transaction_support"
|
7
|
+
require_relative "validations"
|
7
8
|
|
8
9
|
module ActiveRecordCompose
|
9
10
|
# This is the core class of {ActiveRecordCompose}.
|
@@ -83,7 +84,8 @@ module ActiveRecordCompose
|
|
83
84
|
|
84
85
|
include ActiveRecordCompose::Attributes
|
85
86
|
include ActiveRecordCompose::Persistence
|
86
|
-
include ActiveRecordCompose::
|
87
|
+
include ActiveRecordCompose::Validations
|
88
|
+
include ActiveRecordCompose::TransactionSupport
|
87
89
|
|
88
90
|
begin
|
89
91
|
# @group Model Core
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "callbacks"
|
3
4
|
require_relative "composed_collection"
|
4
|
-
require_relative "transaction_support"
|
5
5
|
|
6
6
|
module ActiveRecordCompose
|
7
7
|
using ComposedCollection::PackagePrivate
|
@@ -9,7 +9,7 @@ module ActiveRecordCompose
|
|
9
9
|
# @private
|
10
10
|
module Persistence
|
11
11
|
extend ActiveSupport::Concern
|
12
|
-
include ActiveRecordCompose::
|
12
|
+
include ActiveRecordCompose::Callbacks
|
13
13
|
|
14
14
|
# Save the models that exist in models.
|
15
15
|
# Returns false if any of the targets fail, true if all succeed.
|
@@ -24,11 +24,9 @@ module ActiveRecordCompose
|
|
24
24
|
#
|
25
25
|
# @return [Boolean] returns true on success, false on failure.
|
26
26
|
def save(**options)
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
false
|
31
|
-
end
|
27
|
+
with_callbacks { save_models(**options, bang: false) }
|
28
|
+
rescue ActiveRecord::RecordInvalid
|
29
|
+
false
|
32
30
|
end
|
33
31
|
|
34
32
|
# Save the models that exist in models.
|
@@ -43,28 +41,22 @@ module ActiveRecordCompose
|
|
43
41
|
# If the contexts differ, we recommend separating them into different model definitions.
|
44
42
|
#
|
45
43
|
def save!(**options)
|
46
|
-
|
47
|
-
with_callbacks { save_models(**options, bang: true) }
|
48
|
-
end || raise_on_save_error
|
44
|
+
with_callbacks { save_models(**options, bang: true) } || raise_on_save_error
|
49
45
|
end
|
50
46
|
|
51
47
|
# Assign attributes and save.
|
52
48
|
#
|
53
49
|
# @return [Boolean] returns true on success, false on failure.
|
54
|
-
def update(attributes
|
55
|
-
|
56
|
-
|
57
|
-
save
|
58
|
-
end
|
50
|
+
def update(attributes)
|
51
|
+
assign_attributes(attributes)
|
52
|
+
save
|
59
53
|
end
|
60
54
|
|
61
55
|
# Behavior is same to `#update`, but raises an exception prematurely on failure.
|
62
56
|
#
|
63
|
-
def update!(attributes
|
64
|
-
|
65
|
-
|
66
|
-
save!
|
67
|
-
end
|
57
|
+
def update!(attributes)
|
58
|
+
assign_attributes(attributes)
|
59
|
+
save!
|
68
60
|
end
|
69
61
|
|
70
62
|
private
|
@@ -6,18 +6,19 @@ module ActiveRecordCompose
|
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
include ActiveRecord::Transactions
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
end
|
9
|
+
included do
|
10
|
+
# ActiveRecord::Transactions is defined so that methods such as save,
|
11
|
+
# destroy and touch are wrapped with_transaction_returning_status.
|
12
|
+
# However, ActiveRecordCompose::Model does not support destroy and touch, and
|
13
|
+
# we want to keep these operations as undefined behavior, so we remove the definition here.
|
14
|
+
undef_method :destroy, :touch
|
15
|
+
end
|
17
16
|
|
18
|
-
|
17
|
+
module ClassMethods
|
18
|
+
delegate :with_connection, :lease_connection, to: :ar_class
|
19
19
|
|
20
|
-
|
20
|
+
# In ActiveRecord, it is soft deprecated.
|
21
|
+
delegate :connection, to: :ar_class
|
21
22
|
|
22
23
|
def composite_primary_key? = false # steep:ignore
|
23
24
|
|
@@ -15,6 +15,16 @@ module ActiveRecordCompose
|
|
15
15
|
def delegated_attributes=: (Array[Delegation]) -> untyped
|
16
16
|
end
|
17
17
|
|
18
|
+
class AttributePredicate
|
19
|
+
def initialize: (untyped value) -> void
|
20
|
+
def call: -> bool
|
21
|
+
|
22
|
+
@value: untyped
|
23
|
+
|
24
|
+
private
|
25
|
+
attr_reader value: untyped
|
26
|
+
end
|
27
|
+
|
18
28
|
class Delegation
|
19
29
|
def initialize: (attribute: String, to: Symbol, ?allow_nil: bool) -> void
|
20
30
|
def attribute: () -> Symbol
|
@@ -40,6 +50,7 @@ module ActiveRecordCompose
|
|
40
50
|
|
41
51
|
private
|
42
52
|
def attribute?: (attribute_name) -> untyped
|
53
|
+
def query?: (untyped value) -> bool
|
43
54
|
end
|
44
55
|
end
|
45
56
|
|
@@ -87,6 +98,7 @@ module ActiveRecordCompose
|
|
87
98
|
end
|
88
99
|
|
89
100
|
module TransactionSupport
|
101
|
+
extend ActiveSupport::Concern
|
90
102
|
include ActiveRecord::Transactions
|
91
103
|
|
92
104
|
def id: -> untyped
|
@@ -108,8 +120,8 @@ module ActiveRecordCompose
|
|
108
120
|
|
109
121
|
def save: (**untyped options) -> bool
|
110
122
|
def save!: (**untyped options) -> untyped
|
111
|
-
def update: (
|
112
|
-
def update!: (
|
123
|
+
def update: (Hash[attribute_name, untyped]) -> bool
|
124
|
+
def update!: (Hash[attribute_name, untyped]) -> untyped
|
113
125
|
|
114
126
|
private
|
115
127
|
def models: -> ComposedCollection
|
@@ -79,14 +79,10 @@ module ActiveRecordCompose
|
|
79
79
|
def initialize: (?Hash[attribute_name, untyped]) -> void
|
80
80
|
def save: (**untyped options) -> bool
|
81
81
|
def save!: (**untyped options) -> untyped
|
82
|
-
def update: (
|
83
|
-
def update!: (
|
84
|
-
def id: -> untyped
|
82
|
+
def update: (Hash[attribute_name, untyped]) -> bool
|
83
|
+
def update!: (Hash[attribute_name, untyped]) -> untyped
|
85
84
|
|
86
85
|
private
|
87
86
|
def models: -> ComposedCollection
|
88
|
-
def save_models: (bang: bool, **untyped options) -> bool
|
89
|
-
def raise_on_save_error: -> bot
|
90
|
-
def raise_on_save_error_message: -> String
|
91
87
|
end
|
92
88
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_record_compose
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- hamajyotan
|
@@ -15,7 +15,7 @@ dependencies:
|
|
15
15
|
requirements:
|
16
16
|
- - ">="
|
17
17
|
- !ruby/object:Gem::Version
|
18
|
-
version: '7.
|
18
|
+
version: '7.1'
|
19
19
|
- - "<"
|
20
20
|
- !ruby/object:Gem::Version
|
21
21
|
version: '8.1'
|
@@ -25,7 +25,7 @@ dependencies:
|
|
25
25
|
requirements:
|
26
26
|
- - ">="
|
27
27
|
- !ruby/object:Gem::Version
|
28
|
-
version: '7.
|
28
|
+
version: '7.1'
|
29
29
|
- - "<"
|
30
30
|
- !ruby/object:Gem::Version
|
31
31
|
version: '8.1'
|
@@ -45,6 +45,7 @@ files:
|
|
45
45
|
- README.md
|
46
46
|
- lib/active_record_compose.rb
|
47
47
|
- lib/active_record_compose/attributes.rb
|
48
|
+
- lib/active_record_compose/attributes/attribute_predicate.rb
|
48
49
|
- lib/active_record_compose/attributes/delegation.rb
|
49
50
|
- lib/active_record_compose/attributes/querying.rb
|
50
51
|
- lib/active_record_compose/callbacks.rb
|
@@ -80,7 +81,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
80
81
|
- !ruby/object:Gem::Version
|
81
82
|
version: '0'
|
82
83
|
requirements: []
|
83
|
-
rubygems_version: 3.
|
84
|
+
rubygems_version: 3.7.2
|
84
85
|
specification_version: 4
|
85
86
|
summary: activemodel form object pattern
|
86
87
|
test_files: []
|