active_record-associated_object 0.6.0 → 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +43 -41
- data/README.md +91 -5
- data/lib/active_record/associated_object/object_association.rb +16 -7
- data/lib/active_record/associated_object/railtie.rb +1 -1
- data/lib/active_record/associated_object/version.rb +1 -1
- data/lib/active_record/associated_object.rb +4 -0
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4b86335fcc0e7c367775d2f21fef7537a39c847022ef83feebc1bd68ed21baf1
|
4
|
+
data.tar.gz: b2031efb700cccd28eb7b4414ee4bb8a4e45c4bd0912658f4d54301188987cec
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 664b086f82fcaa6c7029d7e3b44cc4fb8cbeaa581761927054cf80bfddad26cd63b12504a380f00109926e23310ef7bcef4970f99acb5c6b7397080d0410b64d
|
7
|
+
data.tar.gz: 0b3053a708426b83fcbe233394aed806347e8dc74d6305b2a19221de21b30f25e70a8c0e9918b62255639d6d6ac4e3f77a01a514efb98522bd1db78a5a217216
|
data/Gemfile.lock
CHANGED
@@ -1,39 +1,40 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
active_record-associated_object (0.
|
4
|
+
active_record-associated_object (0.7.1)
|
5
5
|
activerecord (>= 6.1)
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: https://rubygems.org/
|
9
9
|
specs:
|
10
|
-
actionpack (7.1.
|
11
|
-
actionview (= 7.1.
|
12
|
-
activesupport (= 7.1.
|
10
|
+
actionpack (7.1.2)
|
11
|
+
actionview (= 7.1.2)
|
12
|
+
activesupport (= 7.1.2)
|
13
13
|
nokogiri (>= 1.8.5)
|
14
|
+
racc
|
14
15
|
rack (>= 2.2.4)
|
15
16
|
rack-session (>= 1.0.1)
|
16
17
|
rack-test (>= 0.6.3)
|
17
18
|
rails-dom-testing (~> 2.2)
|
18
19
|
rails-html-sanitizer (~> 1.6)
|
19
|
-
actionview (7.1.
|
20
|
-
activesupport (= 7.1.
|
20
|
+
actionview (7.1.2)
|
21
|
+
activesupport (= 7.1.2)
|
21
22
|
builder (~> 3.1)
|
22
23
|
erubi (~> 1.11)
|
23
24
|
rails-dom-testing (~> 2.2)
|
24
25
|
rails-html-sanitizer (~> 1.6)
|
25
|
-
active_job-performs (0.
|
26
|
+
active_job-performs (0.3.0)
|
26
27
|
activejob (>= 6.1)
|
27
|
-
activejob (7.1.
|
28
|
-
activesupport (= 7.1.
|
28
|
+
activejob (7.1.2)
|
29
|
+
activesupport (= 7.1.2)
|
29
30
|
globalid (>= 0.3.6)
|
30
|
-
activemodel (7.1.
|
31
|
-
activesupport (= 7.1.
|
32
|
-
activerecord (7.1.
|
33
|
-
activemodel (= 7.1.
|
34
|
-
activesupport (= 7.1.
|
31
|
+
activemodel (7.1.2)
|
32
|
+
activesupport (= 7.1.2)
|
33
|
+
activerecord (7.1.2)
|
34
|
+
activemodel (= 7.1.2)
|
35
|
+
activesupport (= 7.1.2)
|
35
36
|
timeout (>= 0.4.0)
|
36
|
-
activesupport (7.1.
|
37
|
+
activesupport (7.1.2)
|
37
38
|
base64
|
38
39
|
bigdecimal
|
39
40
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
@@ -43,45 +44,45 @@ GEM
|
|
43
44
|
minitest (>= 5.1)
|
44
45
|
mutex_m
|
45
46
|
tzinfo (~> 2.0)
|
46
|
-
base64 (0.
|
47
|
-
bigdecimal (3.1.
|
47
|
+
base64 (0.2.0)
|
48
|
+
bigdecimal (3.1.5)
|
48
49
|
builder (3.2.4)
|
49
50
|
concurrent-ruby (1.2.2)
|
50
51
|
connection_pool (2.4.1)
|
51
52
|
crass (1.0.6)
|
52
|
-
debug (1.
|
53
|
-
irb (
|
54
|
-
reline (>= 0.3.
|
55
|
-
drb (2.
|
53
|
+
debug (1.9.1)
|
54
|
+
irb (~> 1.10)
|
55
|
+
reline (>= 0.3.8)
|
56
|
+
drb (2.2.0)
|
56
57
|
ruby2_keywords
|
57
58
|
erubi (1.12.0)
|
58
59
|
globalid (1.2.1)
|
59
60
|
activesupport (>= 6.1)
|
60
61
|
i18n (1.14.1)
|
61
62
|
concurrent-ruby (~> 1.0)
|
62
|
-
io-console (0.
|
63
|
-
irb (1.
|
63
|
+
io-console (0.7.1)
|
64
|
+
irb (1.11.0)
|
64
65
|
rdoc
|
65
66
|
reline (>= 0.3.8)
|
66
|
-
kredis (1.
|
67
|
+
kredis (1.7.0)
|
67
68
|
activemodel (>= 6.0.0)
|
68
69
|
activesupport (>= 6.0.0)
|
69
70
|
redis (>= 4.2, < 6)
|
70
|
-
loofah (2.
|
71
|
+
loofah (2.22.0)
|
71
72
|
crass (~> 1.0.2)
|
72
73
|
nokogiri (>= 1.12.0)
|
73
74
|
minitest (5.20.0)
|
74
75
|
minitest-sprint (1.2.2)
|
75
76
|
path_expander (~> 1.1)
|
76
|
-
mutex_m (0.
|
77
|
-
nokogiri (1.
|
77
|
+
mutex_m (0.2.0)
|
78
|
+
nokogiri (1.16.0-arm64-darwin)
|
78
79
|
racc (~> 1.4)
|
79
|
-
nokogiri (1.
|
80
|
+
nokogiri (1.16.0-x86_64-linux)
|
80
81
|
racc (~> 1.4)
|
81
82
|
path_expander (1.1.1)
|
82
|
-
psych (5.1.
|
83
|
+
psych (5.1.2)
|
83
84
|
stringio
|
84
|
-
racc (1.7.
|
85
|
+
racc (1.7.3)
|
85
86
|
rack (3.0.8)
|
86
87
|
rack-session (2.0.0)
|
87
88
|
rack (>= 3.0.0)
|
@@ -97,29 +98,29 @@ GEM
|
|
97
98
|
rails-html-sanitizer (1.6.0)
|
98
99
|
loofah (~> 2.21)
|
99
100
|
nokogiri (~> 1.14)
|
100
|
-
railties (7.1.
|
101
|
-
actionpack (= 7.1.
|
102
|
-
activesupport (= 7.1.
|
101
|
+
railties (7.1.2)
|
102
|
+
actionpack (= 7.1.2)
|
103
|
+
activesupport (= 7.1.2)
|
103
104
|
irb
|
104
105
|
rackup (>= 1.0.0)
|
105
106
|
rake (>= 12.2)
|
106
107
|
thor (~> 1.0, >= 1.2.2)
|
107
108
|
zeitwerk (~> 2.6)
|
108
109
|
rake (13.1.0)
|
109
|
-
rdoc (6.
|
110
|
+
rdoc (6.6.2)
|
110
111
|
psych (>= 4.0.0)
|
111
112
|
redis (5.0.8)
|
112
113
|
redis-client (>= 0.17.0)
|
113
|
-
redis-client (0.
|
114
|
+
redis-client (0.19.1)
|
114
115
|
connection_pool
|
115
|
-
reline (0.
|
116
|
+
reline (0.4.2)
|
116
117
|
io-console (~> 0.5)
|
117
118
|
ruby2_keywords (0.0.5)
|
118
|
-
sqlite3 (1.
|
119
|
-
sqlite3 (1.
|
120
|
-
stringio (3.0
|
119
|
+
sqlite3 (1.7.0-arm64-darwin)
|
120
|
+
sqlite3 (1.7.0-x86_64-linux)
|
121
|
+
stringio (3.1.0)
|
121
122
|
thor (1.3.0)
|
122
|
-
timeout (0.4.
|
123
|
+
timeout (0.4.1)
|
123
124
|
tzinfo (2.0.6)
|
124
125
|
concurrent-ruby (~> 1.0)
|
125
126
|
webrick (1.8.1)
|
@@ -129,6 +130,7 @@ PLATFORMS
|
|
129
130
|
arm64-darwin-20
|
130
131
|
arm64-darwin-21
|
131
132
|
arm64-darwin-22
|
133
|
+
arm64-darwin-23
|
132
134
|
x86_64-linux
|
133
135
|
|
134
136
|
DEPENDENCIES
|
@@ -144,4 +146,4 @@ DEPENDENCIES
|
|
144
146
|
sqlite3
|
145
147
|
|
146
148
|
BUNDLED WITH
|
147
|
-
2.4
|
149
|
+
2.5.4
|
data/README.md
CHANGED
@@ -49,13 +49,19 @@ class Post::Publisher
|
|
49
49
|
end
|
50
50
|
|
51
51
|
class Post < ApplicationRecord
|
52
|
-
def publisher = @publisher ||= Post::Publisher.new(self)
|
52
|
+
def publisher = (@associated_objects ||= {})[:publisher] ||= Post::Publisher.new(self)
|
53
53
|
end
|
54
54
|
```
|
55
55
|
|
56
|
+
Note: due to Ruby's Object Shapes, we use a single `@associated_objects` instance variable that's assigned to `nil` on `Post.new`. This prevents Active Record's from ballooning into many different shapes in Ruby's internals.
|
57
|
+
We've fixed this so you don't need to care, but this is what's happening.
|
58
|
+
|
56
59
|
> [!TIP]
|
57
60
|
> `has_object` only requires a namespace and an initializer that takes a single argument. The above `Post::Publisher` is perfectly valid as an Associated Object — same goes for `class Post::Publisher < Data.define(:post); end`.
|
58
61
|
|
62
|
+
> [!TIP]
|
63
|
+
> You can pass multiple names too: `has_object :publisher, :classified, :fortification`. I recommend `-[i]er`, `-[i]ed` and `-ion` as the general naming conventions for your Associated Objects.
|
64
|
+
|
59
65
|
See how we're always expecting a link to the model, here `post`?
|
60
66
|
|
61
67
|
Because of that, you can rely on `post` from the associated object:
|
@@ -86,9 +92,9 @@ So `has_object` can state this and forward those callbacks onto the Associated O
|
|
86
92
|
|
87
93
|
```ruby
|
88
94
|
class Post < ActiveRecord::Base
|
89
|
-
# Passing `true`
|
90
|
-
has_object :publisher, after_create_commit: :publish,
|
91
|
-
|
95
|
+
# Passing `true` forwards the same name, e.g. `after_touch`.
|
96
|
+
has_object :publisher, after_touch: true, after_create_commit: :publish,
|
97
|
+
before_destroy: :prevent_errant_post_destroy
|
92
98
|
|
93
99
|
# The above is the same as writing:
|
94
100
|
after_create_commit { publisher.publish }
|
@@ -111,6 +117,69 @@ class Post::Publisher < ActiveRecord::AssociatedObject
|
|
111
117
|
end
|
112
118
|
```
|
113
119
|
|
120
|
+
### Extending the Active Record from within the Associated Object
|
121
|
+
|
122
|
+
Since `has_object` eager-loads the Associated Object class, you can also move
|
123
|
+
any integrating code into a provided `extension` block:
|
124
|
+
|
125
|
+
> [!NOTE]
|
126
|
+
> Technically, `extension` is just `Post.class_eval` but with syntactic sugar.
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
class Post::Publisher < ActiveRecord::AssociatedObject
|
130
|
+
extension do
|
131
|
+
# Here we're within Post and can extend it:
|
132
|
+
has_many :contracts, dependent: :destroy do
|
133
|
+
def signed? = all?(&:signed?)
|
134
|
+
end
|
135
|
+
|
136
|
+
def self.with_contracts = includes(:contracts)
|
137
|
+
|
138
|
+
after_create_commit :publish_later, if: -> { contracts.signed? }
|
139
|
+
|
140
|
+
# An integrating method that operates on `publisher`.
|
141
|
+
private def publish_later = publisher.publish_later
|
142
|
+
end
|
143
|
+
end
|
144
|
+
```
|
145
|
+
|
146
|
+
This is meant as an alternative to having a wrapping `ActiveSupport::Concern` in yet-another file like this:
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
class Post < ApplicationRecord
|
150
|
+
include Published
|
151
|
+
end
|
152
|
+
|
153
|
+
# app/models/post/published.rb
|
154
|
+
module Post::Published
|
155
|
+
extend ActiveSupport::Concern
|
156
|
+
|
157
|
+
included do
|
158
|
+
has_many :contracts, dependent: :destroy do
|
159
|
+
def signed? = all?(&:signed?)
|
160
|
+
end
|
161
|
+
|
162
|
+
has_object :publisher
|
163
|
+
after_create_commit :publish_later, if: -> { contracts.signed? }
|
164
|
+
end
|
165
|
+
|
166
|
+
class_methods do
|
167
|
+
def with_contracts = includes(:contracts)
|
168
|
+
end
|
169
|
+
|
170
|
+
# An integrating method that operates on `publisher`.
|
171
|
+
private def publish_later = publisher.publish_later
|
172
|
+
end
|
173
|
+
```
|
174
|
+
|
175
|
+
> [!NOTE]
|
176
|
+
> Notice how in the `extension` version you don't need to:
|
177
|
+
>
|
178
|
+
> - have a naming convention for Concerns and where to place them.
|
179
|
+
> - look up two files to read the feature (the concern and the associated object).
|
180
|
+
> - wrap integrating code in an `included` block.
|
181
|
+
> - wrap class methods in a `class_methods` block.
|
182
|
+
|
114
183
|
### Primary Benefit: Organization through Convention
|
115
184
|
|
116
185
|
The primary benefit for right now is that by focusing the concept of namespaced Collaborator Objects through Associated Objects, you will start seeing them when you're modelling new features and it'll change how you structure and write your apps.
|
@@ -125,6 +194,10 @@ And about a month later it was still holding up:
|
|
125
194
|
|
126
195
|
> Just checking in to say we've added like another 4 associated objects to production since my last message. `ActiveRecord::AssociatedObject` + `ActiveJob::Performs` is like a 1-2 punch super power. I'm a bit surprised that this isn't Rails core to be honest. I want to migrate so much of our code over to this. It feels much more organized and sane. Then my app/jobs folder won't have much in it because most jobs will actually be via some associated object's _later method. app/jobs will then basically be cron-type things (deactivate any expired subscriptions).
|
127
196
|
|
197
|
+
Here's what [@nshki](https://github.com/nshki) found when they tried it:
|
198
|
+
|
199
|
+
> Spent some time playing with [@kaspth](https://github.com/kaspth)'s `ActiveRecord::AssociatedObject` and `ActiveJob::Performs` and wow! The conventions these gems put in place help simplify a codebase drastically. I particularly love `ActiveJob::Performs`—it helped me refactor out all `ApplicationJob` classes I had and keep important context in the right domain model.
|
200
|
+
|
128
201
|
Let's look at testing, then we'll get to passing these POROs to jobs like the quotes mentioned!
|
129
202
|
|
130
203
|
### A Quick Aside: Testing Associated Objects
|
@@ -231,7 +304,20 @@ performs :publish, queue_as: :important, discard_on: SomeError do
|
|
231
304
|
end
|
232
305
|
```
|
233
306
|
|
234
|
-
See the `ActiveJob::Performs` README for more details.
|
307
|
+
See [the `ActiveJob::Performs` README](https://github.com/kaspth/active_job-performs) for more details.
|
308
|
+
|
309
|
+
### Automatic Kredis integration
|
310
|
+
|
311
|
+
We've got automatic Kredis integration for Associated Objects, so you can use any `kredis_*` type just like in Active Record classes:
|
312
|
+
|
313
|
+
```ruby
|
314
|
+
class Post::Publisher < ActiveRecord::AssociatedObject
|
315
|
+
kredis_datetime :publish_at # Uses a namespaced "post:publishers:<post_id>:publish_at" key.
|
316
|
+
end
|
317
|
+
```
|
318
|
+
|
319
|
+
> [!NOTE]
|
320
|
+
> Under the hood, this reuses the same info we needed for automatic Active Job support. Namely, the Active Record class, here `Post`, and its `id`.
|
235
321
|
|
236
322
|
### Namespaced models
|
237
323
|
|
@@ -1,4 +1,6 @@
|
|
1
1
|
module ActiveRecord::AssociatedObject::ObjectAssociation
|
2
|
+
def self.included(klass) = klass.extend(ClassMethods)
|
3
|
+
|
2
4
|
using Module.new {
|
3
5
|
refine Module do
|
4
6
|
def extend_source_from(chunks, &block)
|
@@ -9,15 +11,22 @@ module ActiveRecord::AssociatedObject::ObjectAssociation
|
|
9
11
|
end
|
10
12
|
}
|
11
13
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
14
|
+
module ClassMethods
|
15
|
+
def has_object(*names, **callbacks)
|
16
|
+
extend_source_from(names) do |name|
|
17
|
+
"def #{name}; (@associated_objects ||= {})[:#{name}] ||= #{const_get(name.to_s.classify)}.new(self); end"
|
18
|
+
end
|
16
19
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
+
extend_source_from(names) do |name|
|
21
|
+
callbacks.map do |callback, method|
|
22
|
+
"#{callback} { #{name}.#{method == true ? callback : method} }"
|
23
|
+
end
|
20
24
|
end
|
21
25
|
end
|
22
26
|
end
|
27
|
+
|
28
|
+
def init_internals
|
29
|
+
@associated_objects = nil
|
30
|
+
super
|
31
|
+
end
|
23
32
|
end
|
@@ -16,7 +16,7 @@ class ActiveRecord::AssociatedObject::Railtie < Rails::Railtie
|
|
16
16
|
|
17
17
|
ActiveSupport.on_load :active_record do
|
18
18
|
require "active_record/associated_object/object_association"
|
19
|
-
|
19
|
+
include ActiveRecord::AssociatedObject::ObjectAssociation
|
20
20
|
end
|
21
21
|
end
|
22
22
|
end
|
@@ -15,6 +15,10 @@ class ActiveRecord::AssociatedObject
|
|
15
15
|
klass.delegate :record_klass, :attribute_name, to: :class
|
16
16
|
end
|
17
17
|
|
18
|
+
def extension(&block)
|
19
|
+
record_klass.class_eval(&block)
|
20
|
+
end
|
21
|
+
|
18
22
|
def respond_to_missing?(...) = record_klass.respond_to?(...) || super
|
19
23
|
delegate :unscoped, :transaction, :primary_key, to: :record_klass
|
20
24
|
|
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.7.1
|
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: 2024-01-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -65,7 +65,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
65
65
|
- !ruby/object:Gem::Version
|
66
66
|
version: '0'
|
67
67
|
requirements: []
|
68
|
-
rubygems_version: 3.4
|
68
|
+
rubygems_version: 3.5.4
|
69
69
|
signing_key:
|
70
70
|
specification_version: 4
|
71
71
|
summary: Associate a Ruby PORO with an Active Record class and have it quack like
|