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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4212b6fcecceb21ae133688c2a3332d29b8c0f5760f5a22a8ba3450037a95b20
4
- data.tar.gz: 4b96fde3db82d00a3c94153cb9b212f1091bd0040d7d9771d300f2a0a0147d6f
3
+ metadata.gz: 1876318d0052119a7e4080257d42bff11fee22385ac25a5d78023ce958d46fb9
4
+ data.tar.gz: d1089ada8474d59399331a71ec0815afd5a4a792191530fd273d25f0f328a916
5
5
  SHA512:
6
- metadata.gz: 262b75fdf718925e7772b7c1dd4e49ab6b73f6706be97081433712a4f3efef524b1677194ff5376415fe32e770bfa080afd5b347c2ccf78786517dfe5a21a9fb
7
- data.tar.gz: 47c40f2a351cd49fe9bc5e0488d37ce79bcb3d30332b3aabbaff681ef7ac7f937d81d01c60d6b7f8393d3fc28f7fa92fb9b4d5eb99ae607d95d0389deea12764
6
+ metadata.gz: f8200ebd8b229a4077af91fe9ab7b3af686de67c844a208cf843b8d828cb1d5a19fd65560bf4d911a02a67525f08541e31ad21d701fdb452f7a0113a01792cd3
7
+ data.tar.gz: d88d3a02dc2e9fbecef0a1060d1bdb35798ca8280ff860800e1c89219247b815e3ff16a7d7c5b0c193fa1474900b18aa96b23ce6ffe89c1c1adb68d135095f2d
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- active_record-associated_object (0.8.3)
4
+ active_record-associated_object (0.9.0)
5
5
  activerecord (>= 6.1)
6
6
 
7
7
  GEM
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 a provided `extension` block:
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
- ### A Quick Aside: Testing Associated Objects
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:
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRecord
4
4
  class AssociatedObject
5
- VERSION = "0.8.3"
5
+ VERSION = "0.9.0"
6
6
  end
7
7
  end
@@ -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(klass)
4
- record_klass = klass.module_parent
5
- record_name = klass.module_parent_name.demodulize.underscore
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
- unless record_klass.respond_to?(:descends_from_active_record?) && record_klass.descends_from_active_record?
9
- raise ArgumentError, "#{record_klass} isn't valid; can only associate with ActiveRecord::Base subclasses"
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
- klass.alias_method record_name, :record
13
- klass.define_singleton_method(:record_klass) { record_klass }
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
- record_klass.class_eval(&block)
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 !record_klass.respond_to?(method) then super else
27
- record_klass.public_send(method, ...).then do |value|
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, :transaction, to: :record
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.8.3
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: 2024-12-13 00:00:00.000000000 Z
11
+ date: 2025-02-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord