active_record-associated_object 0.8.3 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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