active_record-associated_object 0.8.3 → 0.9.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/Gemfile.lock +1 -1
- data/README.md +93 -2
- data/lib/active_record/associated_object/version.rb +1 -1
- data/lib/active_record/associated_object.rb +42 -17
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1876318d0052119a7e4080257d42bff11fee22385ac25a5d78023ce958d46fb9
|
4
|
+
data.tar.gz: d1089ada8474d59399331a71ec0815afd5a4a792191530fd273d25f0f328a916
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f8200ebd8b229a4077af91fe9ab7b3af686de67c844a208cf843b8d828cb1d5a19fd65560bf4d911a02a67525f08541e31ad21d701fdb452f7a0113a01792cd3
|
7
|
+
data.tar.gz: d88d3a02dc2e9fbecef0a1060d1bdb35798ca8280ff860800e1c89219247b815e3ff16a7d7c5b0c193fa1474900b18aa96b23ce6ffe89c1c1adb68d135095f2d
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -151,7 +151,17 @@ end
|
|
151
151
|
### Extending the Active Record from within the Associated Object
|
152
152
|
|
153
153
|
Since `has_object` eager-loads the Associated Object class, you can also move
|
154
|
-
any integrating code into
|
154
|
+
any integrating code into the Associated Object.
|
155
|
+
|
156
|
+
If you've got a few extensions, you can use `record` to access the Active Record class:
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
class Post::Publisher < ActiveRecord::AssociatedObject
|
160
|
+
record.has_many :contracts, dependent: :destroy # `record` returns `Post` here.
|
161
|
+
end
|
162
|
+
```
|
163
|
+
|
164
|
+
Alternatively, if you have many extensions, use the `extension` block:
|
155
165
|
|
156
166
|
> [!NOTE]
|
157
167
|
> Technically, `extension` is just `Post.class_eval` but with syntactic sugar.
|
@@ -231,7 +241,7 @@ Here's what [@nshki](https://github.com/nshki) found when they tried it:
|
|
231
241
|
|
232
242
|
Let's look at testing, then we'll get to passing these POROs to jobs like the quotes mentioned!
|
233
243
|
|
234
|
-
###
|
244
|
+
### Testing Associated Objects
|
235
245
|
|
236
246
|
Follow the `app/models/post.rb` and `app/models/post/publisher.rb` naming structure in your tests and add `test/models/post/publisher_test.rb`.
|
237
247
|
|
@@ -251,6 +261,87 @@ class Post::PublisherTest < ActiveSupport::TestCase
|
|
251
261
|
end
|
252
262
|
```
|
253
263
|
|
264
|
+
### Active Model integration
|
265
|
+
|
266
|
+
Associated Objects quack like `ActiveModel`s because we:
|
267
|
+
|
268
|
+
- [`extend ActiveModel::Naming`](https://api.rubyonrails.org/classes/ActiveModel/Naming.html)
|
269
|
+
- [`include ActiveModel::Conversion`](https://api.rubyonrails.org/classes/ActiveModel/Conversion.html)
|
270
|
+
|
271
|
+
This means you can pass them to helpers like `form_with` and route helpers like `url_for` too.
|
272
|
+
|
273
|
+
> [!NOTE]
|
274
|
+
> We don't `include ActiveModel::Model` since we don't need `assign_attributes` and validations really.
|
275
|
+
|
276
|
+
```ruby
|
277
|
+
# app/controllers/post/publishers_controller.rb
|
278
|
+
class Post::PublishersController < ApplicationController
|
279
|
+
before_action :set_publisher
|
280
|
+
|
281
|
+
def new
|
282
|
+
end
|
283
|
+
|
284
|
+
def create
|
285
|
+
@publisher.publish params.expect(publisher: :toast)
|
286
|
+
redirect_back_or_to root_url, notice: "Out it goes!"
|
287
|
+
end
|
288
|
+
|
289
|
+
private
|
290
|
+
def set_publisher
|
291
|
+
# Associated Objects are POROs, so behind the scenes we're really doing `Post.find(…).publisher`.
|
292
|
+
@publisher = Post::Publisher.find(params[:id])
|
293
|
+
end
|
294
|
+
end
|
295
|
+
```
|
296
|
+
|
297
|
+
And then on the view side, you can pass it into `form_with`:
|
298
|
+
|
299
|
+
```erb
|
300
|
+
<%# app/views/post/publishers/new.html.erb %>
|
301
|
+
<%# Here `form_with` calls `url_for(@publisher)` which calls `post_publisher_path(@publisher)`. %>
|
302
|
+
<%= form_with model: @publisher do |form| %>
|
303
|
+
<%= form.text_field :toast %>
|
304
|
+
<%= form.submit "Publish with toast" %>
|
305
|
+
<% end %>
|
306
|
+
```
|
307
|
+
|
308
|
+
Finally, the routing is pretty standard fare:
|
309
|
+
|
310
|
+
```ruby
|
311
|
+
namespace :post do
|
312
|
+
resources :publishers
|
313
|
+
end
|
314
|
+
```
|
315
|
+
|
316
|
+
#### Rendering Associated Objects
|
317
|
+
|
318
|
+
Associated Objects respond to `to_partial_path`, so you can pass them directly to `render`.
|
319
|
+
|
320
|
+
We're using Rails' conventions here, so view paths look like this:
|
321
|
+
|
322
|
+
```erb
|
323
|
+
<%# With a Post::Publisher, this renders app/views/post/publishers/_publisher.html.erb %>
|
324
|
+
<%= render publisher %>
|
325
|
+
|
326
|
+
<%# With a Post::Comment::Rating, this renders app/views/post/comment/ratings/_rating.html.erb %>
|
327
|
+
<%= render rating %>
|
328
|
+
```
|
329
|
+
|
330
|
+
We've also got full support for fragment caching, so this is possible:
|
331
|
+
|
332
|
+
```erb
|
333
|
+
<%# app/views/post/publishers/_publisher.html.erb %>
|
334
|
+
<%= cache publisher do %>
|
335
|
+
<%# More publishing specific view logic. %>
|
336
|
+
<% end %>
|
337
|
+
```
|
338
|
+
|
339
|
+
> [!NOTE]
|
340
|
+
> We only support recyclable cache keys which has been the default since Rails 5.2.
|
341
|
+
> This means the Active Record you associate with must have `SomeModel.cache_versioning = true` enabled.
|
342
|
+
>
|
343
|
+
> Associated Objects respond to `cache_key`, `cache_version` and `cache_key_with_version` like Active Records.
|
344
|
+
|
254
345
|
### Polymorphic Associated Objects
|
255
346
|
|
256
347
|
If you want to share logic between associated objects, you can do so via standard Ruby modules:
|
@@ -1,38 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class ActiveRecord::AssociatedObject
|
4
|
+
extend ActiveModel::Naming
|
5
|
+
include ActiveModel::Conversion
|
6
|
+
|
2
7
|
class << self
|
3
|
-
def inherited(
|
4
|
-
|
5
|
-
|
6
|
-
attribute_name = klass.to_s.demodulize.underscore.to_sym
|
8
|
+
def inherited(new_object)
|
9
|
+
new_object.associated_via(new_object.module_parent)
|
10
|
+
end
|
7
11
|
|
8
|
-
|
9
|
-
|
12
|
+
def associated_via(record)
|
13
|
+
unless record.respond_to?(:descends_from_active_record?) && record.descends_from_active_record?
|
14
|
+
raise ArgumentError, "#{record} isn't a valid namespace; can only associate with ActiveRecord::Base subclasses"
|
10
15
|
end
|
11
16
|
|
12
|
-
|
13
|
-
|
14
|
-
klass.define_singleton_method(:attribute_name) { attribute_name }
|
15
|
-
klass.delegate :record_klass, :attribute_name, to: :class
|
17
|
+
@record, @attribute_name = record, model_name.element.to_sym
|
18
|
+
alias_method record.model_name.element, :record
|
16
19
|
end
|
17
20
|
|
21
|
+
attr_reader :record, :attribute_name
|
22
|
+
delegate :primary_key, :unscoped, :transaction, to: :record
|
23
|
+
|
18
24
|
def extension(&block)
|
19
|
-
|
25
|
+
record.class_eval(&block)
|
20
26
|
end
|
21
27
|
|
22
|
-
def respond_to_missing?(...) = record_klass.respond_to?(...) || super
|
23
|
-
delegate :unscoped, :transaction, :primary_key, to: :record_klass
|
24
|
-
|
25
28
|
def method_missing(method, ...)
|
26
|
-
if !
|
27
|
-
|
29
|
+
if !record.respond_to?(method) then super else
|
30
|
+
record.public_send(method, ...).then do |value|
|
28
31
|
value.respond_to?(:each) ? value.map(&attribute_name) : value&.public_send(attribute_name)
|
29
32
|
end
|
30
33
|
end
|
31
34
|
end
|
35
|
+
def respond_to_missing?(...) = record.respond_to?(...) || super
|
36
|
+
end
|
37
|
+
|
38
|
+
module Caching
|
39
|
+
def cache_key_with_version
|
40
|
+
"#{cache_key}-#{cache_version}".tap { _1.delete_suffix!("-") }
|
41
|
+
end
|
42
|
+
delegate :cache_version, to: :record
|
43
|
+
|
44
|
+
def cache_key
|
45
|
+
case
|
46
|
+
when !record.cache_versioning?
|
47
|
+
raise "ActiveRecord::AssociatedObject#cache_key only supports #{record_klass}.cache_versioning = true"
|
48
|
+
when new_record?
|
49
|
+
"#{model_name.cache_key}/new"
|
50
|
+
else
|
51
|
+
"#{model_name.cache_key}/#{id}"
|
52
|
+
end
|
53
|
+
end
|
32
54
|
end
|
55
|
+
include Caching
|
33
56
|
|
34
57
|
attr_reader :record
|
35
|
-
delegate :id, :
|
58
|
+
delegate :id, :new_record?, :persisted?, to: :record
|
59
|
+
delegate :updated_at, :updated_on, to: :record # Helpful when passing to `fresh_when`/`stale?`
|
60
|
+
delegate :transaction, to: :record
|
36
61
|
|
37
62
|
def initialize(record)
|
38
63
|
@record = record
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_record-associated_object
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kasper Timm Hansen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-02-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|