active_record-associated_object 0.6.0 → 0.7.0
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 +87 -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: d24bf37946a6b4e5ddcb1626e90eb91e27932ae8d9f8f29c10c095ce3c53897c
|
4
|
+
data.tar.gz: 7f7d23788109d020fb1b010e8e6f3956880dfeb012c517dfc43e1f5c7862fd6c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 72675998afa72ea4117fd3601e9df6c9dbd2f62ae1541e1b6677903dd5c32109bd06c281e9be98d7742b92dcf6e48f2792764a5c661729ef2df95d6ce54cd2c4
|
7
|
+
data.tar.gz: 3d4244067099adec3ac04a185019bd99aba3078578ede15315893eb6c8f6b9a98cf8cd6356e74e37a395f39ec20013e83529ab6f9766dd014f154d3d03591718
|
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.0)
|
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.
|
@@ -231,7 +300,20 @@ performs :publish, queue_as: :important, discard_on: SomeError do
|
|
231
300
|
end
|
232
301
|
```
|
233
302
|
|
234
|
-
See the `ActiveJob::Performs` README for more details.
|
303
|
+
See [the `ActiveJob::Performs` README](https://github.com/kaspth/active_job-performs) for more details.
|
304
|
+
|
305
|
+
### Automatic Kredis integration
|
306
|
+
|
307
|
+
We've got automatic Kredis integration for Associated Objects, so you can use any `kredis_*` type just like in Active Record classes:
|
308
|
+
|
309
|
+
```ruby
|
310
|
+
class Post::Publisher < ActiveRecord::AssociatedObject
|
311
|
+
kredis_datetime :publish_at # Uses a namespaced "post:publishers:<post_id>:publish_at" key.
|
312
|
+
end
|
313
|
+
```
|
314
|
+
|
315
|
+
> [!NOTE]
|
316
|
+
> 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
317
|
|
236
318
|
### Namespaced models
|
237
319
|
|
@@ -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}] ||= #{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.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: 2024-01-07 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.
|
68
|
+
rubygems_version: 3.5.3
|
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
|