active_record_compose 0.12.0 → 1.0.1
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 +12 -0
- data/README.md +130 -231
- data/lib/active_record_compose/model.rb +23 -0
- data/lib/active_record_compose/transaction_support.rb +0 -4
- data/lib/active_record_compose/version.rb +1 -1
- data/sig/_internal/package_private.rbs +0 -2
- data/sig/active_record_compose.rbs +2 -0
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a8fa5fa4117b180d20233c6dc06ca8797c2c9931cca2708e5a01bd2b560c42d2
|
|
4
|
+
data.tar.gz: cdb73d45e04a3195f9834390f9e9b6e8242e4a9b325a6ec684d3c7c2f97076ef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2317d9e39b054c728b0847c6cde3f556edfcfa57de9b1c6023f10cf264f5c498bcf654e252d7a29c0a071d349ade5f00943e512938a427b6d253bd43592ebace
|
|
7
|
+
data.tar.gz: 71ad4690a7af527a1d323e3ccc84b25c0b7f875fcc3655883d0b8c2e35fe165b3b1f17a6201152e6d0281b21716976c77eec26db6c5712c0630d2741e6b72886
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.0.1] - 2025-10-17
|
|
4
|
+
|
|
5
|
+
* Removed the private interface `composite_primary_key?`
|
|
6
|
+
This was previously an internal ActiveRecord dependency, but was not exposed in the release version.
|
|
7
|
+
(https://github.com/hamajyotan/active_record_compose/pull/39)
|
|
8
|
+
* Relaxed ActiveRecord dependency upper bound to < 8.2
|
|
9
|
+
(https://github.com/hamajyotan/active_record_compose/pull/42)
|
|
10
|
+
|
|
11
|
+
## [1.0.0] - 2025-09-23
|
|
12
|
+
|
|
13
|
+
- drop support rails 7.0.x
|
|
14
|
+
|
|
3
15
|
## [0.12.0] - 2025-08-21
|
|
4
16
|
|
|
5
17
|
- Omits default arguments for `#update` and `#update!`. It's to align I/F with ActiveRecord.
|
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,47 @@ registration.update!(
|
|
|
128
120
|
email_confirmation: "bar@example.com",
|
|
129
121
|
terms_of_service: true,
|
|
130
122
|
)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
123
|
+
# `#update!` SQL log
|
|
124
|
+
# BEGIN immediate TRANSACTION
|
|
125
|
+
# INSERT INTO "accounts" ("created_at", "email", "name", "updated_at") VALUES (...
|
|
126
|
+
# INSERT INTO "profiles" ("account_id", "age", "created_at", "firstname", "lastname", ...
|
|
127
|
+
# COMMIT TRANSACTION
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# === Or, in a Rails controller with strong parameters ===
|
|
131
|
+
class UserRegistrationsController < ApplicationController
|
|
132
|
+
def create
|
|
133
|
+
@registration = UserRegistration.new(user_registration_params)
|
|
134
|
+
if @registration.save
|
|
135
|
+
redirect_to root_path, notice: "Registered!"
|
|
136
|
+
else
|
|
137
|
+
render :new
|
|
138
|
+
end
|
|
139
|
+
end
|
|
134
140
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
141
|
+
private
|
|
142
|
+
def user_registration_params
|
|
143
|
+
params.require(:user_registration).permit(
|
|
144
|
+
:name, :email, :firstname, :lastname, :age, :email_confirmation, :terms_of_service
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
140
148
|
```
|
|
141
149
|
|
|
142
|
-
|
|
150
|
+
Both `Account` and `Profile` will be updated **atomically in one transaction**.
|
|
143
151
|
|
|
144
|
-
|
|
152
|
+
### Attribute Delegation
|
|
153
|
+
|
|
154
|
+
`delegate_attribute` allows transparent access to attributes of inner models:
|
|
145
155
|
|
|
146
156
|
```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
|
|
157
|
+
delegate_attribute :name, :email, to: :account
|
|
158
|
+
delegate_attribute :firstname, :lastname, :age, to: :profile
|
|
152
159
|
```
|
|
153
160
|
|
|
154
|
-
|
|
161
|
+
They are also included in `#attributes`:
|
|
155
162
|
|
|
156
163
|
```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
164
|
registration.attributes
|
|
164
165
|
# => {
|
|
165
166
|
# "terms_of_service" => true,
|
|
@@ -171,21 +172,21 @@ registration.attributes
|
|
|
171
172
|
# }
|
|
172
173
|
```
|
|
173
174
|
|
|
174
|
-
###
|
|
175
|
+
### Unified Error Handling
|
|
175
176
|
|
|
176
|
-
|
|
177
|
+
Validation errors from inner models are collected into the composed model:
|
|
177
178
|
|
|
178
179
|
```ruby
|
|
179
|
-
user_registration = UserRegistration.new
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
180
|
+
user_registration = UserRegistration.new(
|
|
181
|
+
email: "foo@example.com",
|
|
182
|
+
email_confirmation: "BAZ@example.com",
|
|
183
|
+
age: 18,
|
|
184
|
+
terms_of_service: true,
|
|
185
|
+
)
|
|
184
186
|
|
|
185
|
-
user_registration.save
|
|
186
|
-
#=> false
|
|
187
|
+
user_registration.save # => false
|
|
187
188
|
|
|
188
|
-
user_registration.errors.
|
|
189
|
+
user_registration.errors.full_messages
|
|
189
190
|
# => [
|
|
190
191
|
# "Name can't be blank",
|
|
191
192
|
# "Firstname can't be blank",
|
|
@@ -194,12 +195,10 @@ user_registration.errors.to_a
|
|
|
194
195
|
# ]
|
|
195
196
|
```
|
|
196
197
|
|
|
197
|
-
### I18n
|
|
198
|
+
### I18n Support
|
|
198
199
|
|
|
199
|
-
When
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
(Replace `en` as appropriate in the context.)
|
|
200
|
+
When `#save!` raises `ActiveRecord::RecordInvalid`,
|
|
201
|
+
make sure you have locale entries such as:
|
|
203
202
|
|
|
204
203
|
```yaml
|
|
205
204
|
en:
|
|
@@ -209,204 +208,104 @@ en:
|
|
|
209
208
|
record_invalid: 'Validation failed: %{errors}'
|
|
210
209
|
```
|
|
211
210
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
```yaml
|
|
215
|
-
en:
|
|
216
|
-
errors:
|
|
217
|
-
messages:
|
|
218
|
-
record_invalid: 'Validation failed: %{errors}'
|
|
219
|
-
```
|
|
211
|
+
For more complete usage patterns, see the [Sample Application](#sample-application) below.
|
|
220
212
|
|
|
221
213
|
## Advanced Usage
|
|
222
214
|
|
|
223
|
-
###
|
|
224
|
-
|
|
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.
|
|
215
|
+
### Destroy Option
|
|
226
216
|
|
|
227
217
|
```ruby
|
|
228
|
-
|
|
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
|
|
218
|
+
models.push(profile, destroy: true)
|
|
247
219
|
```
|
|
248
|
-
```ruby
|
|
249
|
-
account = Account.last
|
|
250
220
|
|
|
251
|
-
|
|
252
|
-
|
|
221
|
+
This deletes the model on `#save` instead of persisting it.
|
|
222
|
+
Conditional deletion is also supported:
|
|
253
223
|
|
|
254
|
-
|
|
255
|
-
|
|
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
|
|
224
|
+
```ruby
|
|
225
|
+
models.push(profile, destroy: -> { profile_field_is_blank? })
|
|
260
226
|
```
|
|
261
227
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
```ruby
|
|
265
|
-
class AccountRegistration < ActiveRecordCompose::Model
|
|
266
|
-
def initialize(account)
|
|
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
|
|
228
|
+
### Callback ordering with `#persisted?`
|
|
277
229
|
|
|
278
|
-
|
|
279
|
-
delegate_attribute :name, :age, to: :profile
|
|
230
|
+
The result of `#persisted?` determines **which callbacks are fired**:
|
|
280
231
|
|
|
281
|
-
|
|
232
|
+
- `persisted? == false` -> create callbacks (`before_create`, `after_create`, ...)
|
|
233
|
+
- `persisted? == true` -> update callbacks (`before_update`, `after_update`, ...)
|
|
282
234
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
def profile_field_is_blank?
|
|
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.
|
|
235
|
+
This matches the behavior of normal ActiveRecord models.
|
|
295
236
|
|
|
296
237
|
```ruby
|
|
297
238
|
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!' }
|
|
239
|
+
before_save { puts "before_save" }
|
|
240
|
+
before_create { puts "before_create" }
|
|
241
|
+
before_update { puts "before_update" }
|
|
242
|
+
after_create { puts "after_create" }
|
|
243
|
+
after_update { puts "after_update" }
|
|
244
|
+
after_save { puts "after_save" }
|
|
306
245
|
|
|
307
246
|
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
|
|
247
|
+
account.persisted?
|
|
315
248
|
end
|
|
316
249
|
end
|
|
317
250
|
```
|
|
318
251
|
|
|
319
|
-
|
|
320
|
-
# when `model.persisted?` returns `true`
|
|
252
|
+
Example:
|
|
321
253
|
|
|
254
|
+
```ruby
|
|
255
|
+
# When persisted? == false
|
|
322
256
|
model = ComposedModel.new
|
|
323
257
|
|
|
324
|
-
model.save
|
|
325
|
-
|
|
326
|
-
#
|
|
327
|
-
#
|
|
328
|
-
#
|
|
329
|
-
# after_save called!
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
```ruby
|
|
333
|
-
# when `model.persisted?` returns `false`
|
|
258
|
+
model.save
|
|
259
|
+
# => before_save
|
|
260
|
+
# => before_create
|
|
261
|
+
# => after_create
|
|
262
|
+
# => after_save
|
|
334
263
|
|
|
264
|
+
# When persisted? == true
|
|
335
265
|
model = ComposedModel.new
|
|
266
|
+
def model.persisted?; true; end
|
|
336
267
|
|
|
337
|
-
model.save
|
|
338
|
-
|
|
339
|
-
#
|
|
340
|
-
#
|
|
341
|
-
#
|
|
342
|
-
# after_save called!
|
|
268
|
+
model.save
|
|
269
|
+
# => before_save
|
|
270
|
+
# => before_update
|
|
271
|
+
# => after_update
|
|
272
|
+
# => after_save
|
|
343
273
|
```
|
|
344
274
|
|
|
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
|
-
```
|
|
275
|
+
### Notes on adding models dynamically
|
|
354
276
|
|
|
355
|
-
|
|
356
|
-
|
|
277
|
+
Avoid adding `models` to the models array **after validation has already run**
|
|
278
|
+
(for example, inside `after_validation` or `before_save` callbacks).
|
|
357
279
|
|
|
358
280
|
```ruby
|
|
359
|
-
class
|
|
360
|
-
|
|
361
|
-
validates :email, presence: true
|
|
362
|
-
validates :email, format: { with: /\.edu\z/ }, on: :education
|
|
281
|
+
class Example < ActiveRecordCompose::Model
|
|
282
|
+
before_save { models << AnotherModel.new }
|
|
363
283
|
end
|
|
284
|
+
```
|
|
364
285
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
super(attributes)
|
|
369
|
-
end
|
|
370
|
-
|
|
371
|
-
attribute :accept, :boolean
|
|
372
|
-
validates :accept, presence: true, on: :education
|
|
286
|
+
In this case, the newly added model will **not** run validations for the current save cycle.
|
|
287
|
+
This may look like a bug, but it is the expected behavior: validations are only applied
|
|
288
|
+
to models that were registered before validation started.
|
|
373
289
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
290
|
+
We intentionally do not restrict this at the framework level, since there may be valid
|
|
291
|
+
advanced use cases where models are manipulated dynamically.
|
|
292
|
+
Instead, this behavior is documented here so that developers can make an informed decision.
|
|
377
293
|
|
|
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
|
-
```
|
|
294
|
+
## Sample Application
|
|
399
295
|
|
|
400
|
-
|
|
296
|
+
The sample app demonstrates a more complete usage of ActiveRecordCompose
|
|
297
|
+
(e.g., user registration flows involving multiple models).
|
|
298
|
+
It is not meant to cover every possible pattern, but can serve as a reference
|
|
299
|
+
for putting the library into practice.
|
|
401
300
|
|
|
402
|
-
|
|
301
|
+
Try it out in your browser with GitHub Codespaces (or locally):
|
|
403
302
|
|
|
404
303
|
- https://github.com/hamajyotan/active_record_compose-example
|
|
405
304
|
|
|
406
305
|
## Links
|
|
407
306
|
|
|
408
|
-
- [
|
|
409
|
-
- [
|
|
307
|
+
- [API Documentation (YARD)](https://hamajyotan.github.io/active_record_compose/)
|
|
308
|
+
- [Blog article introducing the concept](https://dev.to/hamajyotan/smart-way-to-update-multiple-models-simultaneously-in-rails-51b6)
|
|
410
309
|
|
|
411
310
|
## Development
|
|
412
311
|
|
|
@@ -359,6 +359,29 @@ module ActiveRecordCompose
|
|
|
359
359
|
super
|
|
360
360
|
end
|
|
361
361
|
|
|
362
|
+
# Returns the ID value. This value is used when passing it to the `:model` option of `form_with`, etc.
|
|
363
|
+
# Normally it returns nil, but it can be overridden to delegate to the containing model.
|
|
364
|
+
#
|
|
365
|
+
# @example Redefine the id method by delegating to the containing model
|
|
366
|
+
# class Foo < ActiveRecordCompose::Model
|
|
367
|
+
# def initialize(primary_model)
|
|
368
|
+
# @primary_model = primary_model
|
|
369
|
+
# # ...
|
|
370
|
+
# end
|
|
371
|
+
#
|
|
372
|
+
# def id
|
|
373
|
+
# primary_model.id
|
|
374
|
+
# end
|
|
375
|
+
#
|
|
376
|
+
# private
|
|
377
|
+
#
|
|
378
|
+
# attr_reader :primary_model
|
|
379
|
+
# end
|
|
380
|
+
#
|
|
381
|
+
# @return [Object] ID value
|
|
382
|
+
#
|
|
383
|
+
def id = nil
|
|
384
|
+
|
|
362
385
|
private
|
|
363
386
|
|
|
364
387
|
# Returns a collection of model elements to encapsulate.
|
|
@@ -20,15 +20,11 @@ module ActiveRecordCompose
|
|
|
20
20
|
# In ActiveRecord, it is soft deprecated.
|
|
21
21
|
delegate :connection, to: :ar_class
|
|
22
22
|
|
|
23
|
-
def composite_primary_key? = false # steep:ignore
|
|
24
|
-
|
|
25
23
|
private
|
|
26
24
|
|
|
27
25
|
def ar_class = ActiveRecord::Base
|
|
28
26
|
end
|
|
29
27
|
|
|
30
|
-
def id = nil
|
|
31
|
-
|
|
32
28
|
def trigger_transactional_callbacks? = true
|
|
33
29
|
def restore_transaction_record_state(_force_restore_state = false) = nil
|
|
34
30
|
end
|
|
@@ -101,8 +101,6 @@ module ActiveRecordCompose
|
|
|
101
101
|
extend ActiveSupport::Concern
|
|
102
102
|
include ActiveRecord::Transactions
|
|
103
103
|
|
|
104
|
-
def id: -> untyped
|
|
105
|
-
|
|
106
104
|
module ClassMethods
|
|
107
105
|
def connection: -> ActiveRecord::ConnectionAdapters::AbstractAdapter
|
|
108
106
|
def lease_connection: -> ActiveRecord::ConnectionAdapters::AbstractAdapter
|
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.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- hamajyotan
|
|
@@ -15,20 +15,20 @@ 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
|
-
version: '8.
|
|
21
|
+
version: '8.2'
|
|
22
22
|
type: :runtime
|
|
23
23
|
prerelease: false
|
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
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
|
-
version: '8.
|
|
31
|
+
version: '8.2'
|
|
32
32
|
description: activemodel form object pattern. it embraces multiple AR models and provides
|
|
33
33
|
a transparent interface as if they were a single model.
|
|
34
34
|
email:
|
|
@@ -81,7 +81,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
82
|
version: '0'
|
|
83
83
|
requirements: []
|
|
84
|
-
rubygems_version: 3.
|
|
84
|
+
rubygems_version: 3.7.2
|
|
85
85
|
specification_version: 4
|
|
86
86
|
summary: activemodel form object pattern
|
|
87
87
|
test_files: []
|