rails-hyperdrive-layered-rails 0.1.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 +7 -0
- data/LICENSE +30 -0
- data/README.md +89 -0
- data/UPSTREAM +4 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/SKILL.md.erb +315 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/authorization-to-policy.md +79 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/callbacks-to-service.md +72 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/complex-input-to-form-object.md +119 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/current-from-model.md +63 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/god-object-decomposition.md +81 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/implicit-to-explicit-state-machine.md +77 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/query-to-query-object.md +63 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/view-logic-to-presenter.md +60 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/callbacks.md +114 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/concerns.md +92 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/helpers.md +66 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/jobs.md +71 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/layer-violations.md +176 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/service-objects.md +131 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/testing.md +37 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/core/architecture-layers.md +204 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/core/extraction-signals.md +314 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/core/specification-test.md +229 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/action-policy.md +437 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/active-agent.md +275 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/active-delivery.md +221 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/active-job-performs.md +171 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/alba.md +257 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/anyway-config.md +212 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/rubanok.md +246 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/view-component.md +223 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/workflow.md +305 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/collaborator-objects.md +209 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/concerns.md +381 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/filter-objects.md +222 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/form-objects.md +267 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/policy-objects.md +298 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/presenters.md +257 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/query-objects.md +208 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/repositories.md +382 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/serializers.md +256 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/service-objects.md +170 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/state-machines.md +368 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/value-objects.md +267 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/ai-integration.md +372 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/authorization.md +398 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/callbacks.md +336 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/configuration.md +383 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/current-attributes.md +307 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/instrumentation.md +386 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/notifications.md +373 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/view-components.md +454 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/analyze-callbacks.md +255 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/analyze-gods.md +279 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/analyze-services.md +1444 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/analyze.md +376 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/plan.md +249 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/review.md +473 -0
- data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/spec-test.md +282 -0
- data/lib/rails-hyperdrive-layered-rails/version.rb +3 -0
- data/lib/rails-hyperdrive-layered-rails.rb +7 -0
- metadata +106 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Active Delivery
|
|
2
|
+
|
|
3
|
+
Abstraction layer over delivery mechanisms (email, push, SMS, etc.).
|
|
4
|
+
|
|
5
|
+
**GitHub**: https://github.com/palkan/active_delivery
|
|
6
|
+
**Layer**: Application (delivery classes) / Infrastructure (notifiers)
|
|
7
|
+
|
|
8
|
+
## Contents
|
|
9
|
+
|
|
10
|
+
- Installation
|
|
11
|
+
- Basic Usage
|
|
12
|
+
- Multiple Channels
|
|
13
|
+
- Conditional Delivery
|
|
14
|
+
- User Preferences
|
|
15
|
+
- Custom Delivery Lines
|
|
16
|
+
- Testing
|
|
17
|
+
- Inline Mode (Development)
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
# Gemfile
|
|
23
|
+
gem "active_delivery"
|
|
24
|
+
|
|
25
|
+
# Generate delivery
|
|
26
|
+
rails generate delivery Posts
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Basic Usage
|
|
30
|
+
|
|
31
|
+
### Define Delivery
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
# app/deliveries/posts_delivery.rb
|
|
35
|
+
class PostsDelivery < ApplicationDelivery
|
|
36
|
+
deliver_by :mailer
|
|
37
|
+
|
|
38
|
+
def published(post)
|
|
39
|
+
params(post: post, author: post.author)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def commented(post, comment)
|
|
43
|
+
params(post: post, comment: comment, author: post.author)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Define Mailer
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
# app/mailers/posts_mailer.rb
|
|
52
|
+
class PostsMailer < ApplicationMailer
|
|
53
|
+
def published
|
|
54
|
+
@post = params[:post]
|
|
55
|
+
@author = params[:author]
|
|
56
|
+
|
|
57
|
+
mail(to: @author.email, subject: "Your post is published!")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def commented
|
|
61
|
+
@post = params[:post]
|
|
62
|
+
@comment = params[:comment]
|
|
63
|
+
|
|
64
|
+
mail(to: params[:author].email, subject: "New comment on your post")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Trigger Delivery
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
# Deliver later (async)
|
|
73
|
+
PostsDelivery.with(post: post).published.deliver_later
|
|
74
|
+
|
|
75
|
+
# Deliver now (sync)
|
|
76
|
+
PostsDelivery.with(post: post).published.deliver_now
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Multiple Channels
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
class PostsDelivery < ApplicationDelivery
|
|
83
|
+
deliver_by :mailer
|
|
84
|
+
deliver_by :push, to: PushNotifier
|
|
85
|
+
deliver_by :slack, to: SlackNotifier
|
|
86
|
+
|
|
87
|
+
def published(post)
|
|
88
|
+
params(post: post, author: post.author)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Push notifier
|
|
93
|
+
class PushNotifier < AbstractNotifier::Base
|
|
94
|
+
self.driver = PushDriver.new
|
|
95
|
+
|
|
96
|
+
def published
|
|
97
|
+
notification(
|
|
98
|
+
user: params[:author],
|
|
99
|
+
title: "Post Published",
|
|
100
|
+
body: "Your post '#{params[:post].title}' is now live!"
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Slack notifier
|
|
106
|
+
class SlackNotifier < AbstractNotifier::Base
|
|
107
|
+
self.driver = SlackDriver.new
|
|
108
|
+
|
|
109
|
+
def published
|
|
110
|
+
notification(
|
|
111
|
+
channel: "#content",
|
|
112
|
+
text: "New post: #{params[:post].title}"
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Conditional Delivery
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
class PostsDelivery < ApplicationDelivery
|
|
122
|
+
deliver_by :mailer
|
|
123
|
+
deliver_by :push, if: :push_enabled?
|
|
124
|
+
deliver_by :slack, if: -> { params[:post].featured? }
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def push_enabled?
|
|
129
|
+
params[:author]&.push_notifications_enabled?
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## User Preferences
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
class ApplicationDelivery < ActiveDelivery::Base
|
|
138
|
+
deliver_by :mailer, if: :email_enabled?
|
|
139
|
+
deliver_by :push, if: :push_enabled?
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
def recipient
|
|
144
|
+
params[:user] || params[:recipient] || params[:author]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def email_enabled?
|
|
148
|
+
recipient&.email_notifications?
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def push_enabled?
|
|
152
|
+
recipient&.push_notifications?
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Custom Delivery Lines
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
# Webhook delivery
|
|
161
|
+
class WebhookLine < ActiveDelivery::Lines::Base
|
|
162
|
+
def resolve_class(name)
|
|
163
|
+
"#{name}Webhook".safe_constantize
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def notify(handler, mid, **options)
|
|
167
|
+
handler.public_send(mid)&.deliver
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
ActiveDelivery::Base.register_line :webhook, WebhookLine
|
|
172
|
+
|
|
173
|
+
class PostsDelivery < ApplicationDelivery
|
|
174
|
+
deliver_by :webhook
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
class PostsWebhook
|
|
178
|
+
def published
|
|
179
|
+
# Send webhook
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Testing
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
RSpec.describe PostsDelivery do
|
|
188
|
+
let(:post) { create(:post) }
|
|
189
|
+
|
|
190
|
+
describe "#published" do
|
|
191
|
+
it "delivers email" do
|
|
192
|
+
expect {
|
|
193
|
+
described_class.with(post: post).published.deliver_now
|
|
194
|
+
}.to have_delivered_to(PostsMailer, :published)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it "delivers push notification when enabled" do
|
|
198
|
+
post.author.update!(push_notifications: true)
|
|
199
|
+
|
|
200
|
+
expect {
|
|
201
|
+
described_class.with(post: post).published.deliver_now
|
|
202
|
+
}.to have_delivered_to(PushNotifier, :published)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
it "skips push when disabled" do
|
|
206
|
+
post.author.update!(push_notifications: false)
|
|
207
|
+
|
|
208
|
+
expect {
|
|
209
|
+
described_class.with(post: post).published.deliver_now
|
|
210
|
+
}.not_to have_delivered_to(PushNotifier, :published)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Inline Mode (Development)
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
# config/environments/development.rb
|
|
220
|
+
config.active_delivery.deliver_later_queue_adapter = :inline
|
|
221
|
+
```
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# active_job-performs
|
|
2
|
+
|
|
3
|
+
Eliminate boilerplate job classes by declaring background methods directly on models.
|
|
4
|
+
|
|
5
|
+
**Repository:** [kaspth/active_job-performs](https://github.com/kaspth/active_job-performs)
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
- When to Use
|
|
10
|
+
- The `performs` Pattern
|
|
11
|
+
- Detection Signals
|
|
12
|
+
- Configuration
|
|
13
|
+
- Application-Wide `performs` Patterns
|
|
14
|
+
- When NOT to Use
|
|
15
|
+
- Migration Path
|
|
16
|
+
- Layer Placement
|
|
17
|
+
|
|
18
|
+
## When to Use
|
|
19
|
+
|
|
20
|
+
Use when you have **anemic jobs** - job classes that just call a single method on a model:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
# BAD: Anemic job
|
|
24
|
+
class NotifyRecipientsJob < ApplicationJob
|
|
25
|
+
def perform(notifiable)
|
|
26
|
+
notifiable.notify_recipients
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# BAD: Model method just to enqueue job
|
|
31
|
+
class Post < ApplicationRecord
|
|
32
|
+
def notify_recipients_later
|
|
33
|
+
NotifyRecipientsJob.perform_later(self)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## The `performs` Pattern
|
|
39
|
+
|
|
40
|
+
Replace anemic jobs with `performs`:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
# GOOD: Short form (no job options needed)
|
|
44
|
+
class Post < ApplicationRecord
|
|
45
|
+
performs def notify_recipients
|
|
46
|
+
# Notification logic
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Usage
|
|
51
|
+
post.notify_recipients_later
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This generates:
|
|
55
|
+
- `Post::NotifyRecipientsJob` (automatically)
|
|
56
|
+
- `notify_recipients_later` instance method
|
|
57
|
+
- `notify_recipients_later_bulk` class method (Rails 7.1+)
|
|
58
|
+
|
|
59
|
+
### When to Use Short vs Long Form
|
|
60
|
+
|
|
61
|
+
**Short form** - when no job configuration needed:
|
|
62
|
+
```ruby
|
|
63
|
+
performs def sync_with_provider
|
|
64
|
+
# Simple async execution
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Long form** - when job options are required:
|
|
69
|
+
```ruby
|
|
70
|
+
performs :sync_with_provider, queue_as: :critical, discard_on: ApiError
|
|
71
|
+
|
|
72
|
+
def sync_with_provider
|
|
73
|
+
# ...
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Detection Signals
|
|
78
|
+
|
|
79
|
+
**Anemic job indicators:**
|
|
80
|
+
- Job's `perform` method is a single line calling a method on the argument
|
|
81
|
+
- Model has `*_later` method that just calls `SomeJob.perform_later(self)`
|
|
82
|
+
- Job class has no logic beyond delegation
|
|
83
|
+
- Job folder has many similar thin wrapper jobs
|
|
84
|
+
|
|
85
|
+
## Configuration
|
|
86
|
+
|
|
87
|
+
### Job Options
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
class Post < ApplicationRecord
|
|
91
|
+
performs :publish,
|
|
92
|
+
queue_as: :critical,
|
|
93
|
+
discard_on: ActiveRecord::RecordNotFound do
|
|
94
|
+
retry_on TimeoutError, wait: :polynomially_longer
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def publish
|
|
98
|
+
# ...
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Delayed Execution
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
class Post < ApplicationRecord
|
|
107
|
+
performs :social_boost, wait: 5.minutes
|
|
108
|
+
performs :publish, wait_until: -> post { Date.tomorrow.noon }
|
|
109
|
+
|
|
110
|
+
def social_boost; end
|
|
111
|
+
def publish; end
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Private Methods
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
class Post < ApplicationRecord
|
|
119
|
+
performs :sync_to_warehouse
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def sync_to_warehouse
|
|
124
|
+
# Only accessible via sync_to_warehouse_later
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Method Suffixes
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
class Post < ApplicationRecord
|
|
133
|
+
performs :publish! # Creates publish_later!
|
|
134
|
+
performs :dangerous? # Creates dangerous_later?
|
|
135
|
+
end
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Application-Wide `performs` Patterns
|
|
139
|
+
|
|
140
|
+
Define common async operations in `ApplicationRecord`:
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
class ApplicationRecord < ActiveRecord::Base
|
|
144
|
+
self.abstract_class = true
|
|
145
|
+
|
|
146
|
+
performs :destroy, queue_as: "active_record.destroy"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Any model can now use:
|
|
150
|
+
post.destroy_later
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## When NOT to Use
|
|
154
|
+
|
|
155
|
+
Keep separate job classes when:
|
|
156
|
+
- Job has complex logic beyond calling one method
|
|
157
|
+
- Job processes multiple records with custom batching
|
|
158
|
+
- Job needs to be triggered from multiple models
|
|
159
|
+
- Job has extensive retry/error handling configuration
|
|
160
|
+
|
|
161
|
+
## Migration Path
|
|
162
|
+
|
|
163
|
+
1. Identify anemic jobs (single method delegation)
|
|
164
|
+
2. Add `performs :method_name` to the model
|
|
165
|
+
3. Replace `SomeJob.perform_later(record)` with `record.method_name_later`
|
|
166
|
+
4. Delete the job file
|
|
167
|
+
5. Delete any `*_later` wrapper methods
|
|
168
|
+
|
|
169
|
+
## Layer Placement
|
|
170
|
+
|
|
171
|
+
The `performs` declaration belongs in the **Domain Layer** (model), keeping the async execution detail close to the domain logic it executes.
|
data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/alba.md
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# Alba
|
|
2
|
+
|
|
3
|
+
Fast JSON serialization with a clean DSL.
|
|
4
|
+
|
|
5
|
+
**GitHub**: https://github.com/okuramasafumi/alba
|
|
6
|
+
**Layer**: Presentation (API layer)
|
|
7
|
+
|
|
8
|
+
## Contents
|
|
9
|
+
|
|
10
|
+
- Installation
|
|
11
|
+
- Basic Usage
|
|
12
|
+
- Associations
|
|
13
|
+
- Conditional Attributes
|
|
14
|
+
- Computed Attributes
|
|
15
|
+
- Root Key
|
|
16
|
+
- Base Serializer
|
|
17
|
+
- Key Transformation
|
|
18
|
+
- TypeScript Generation
|
|
19
|
+
- Performance Tips
|
|
20
|
+
- Testing
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
# Gemfile
|
|
26
|
+
gem "alba"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Basic Usage
|
|
30
|
+
|
|
31
|
+
### Define Serializer
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
class UserSerializer
|
|
35
|
+
include Alba::Resource
|
|
36
|
+
|
|
37
|
+
attributes :id, :email
|
|
38
|
+
|
|
39
|
+
attribute :full_name do |user|
|
|
40
|
+
"#{user.first_name} #{user.last_name}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Serialize
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
user = User.find(1)
|
|
49
|
+
UserSerializer.new(user).serialize
|
|
50
|
+
# => '{"id":1,"email":"user@example.com","full_name":"John Doe"}'
|
|
51
|
+
|
|
52
|
+
# Collection
|
|
53
|
+
users = User.all
|
|
54
|
+
UserSerializer.new(users).serialize
|
|
55
|
+
# => '[{"id":1,...},{"id":2,...}]'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### In Controller
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
class UsersController < ApplicationController
|
|
62
|
+
def show
|
|
63
|
+
user = User.find(params[:id])
|
|
64
|
+
render json: UserSerializer.new(user)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def index
|
|
68
|
+
users = User.all
|
|
69
|
+
render json: UserSerializer.new(users)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Associations
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
class PostSerializer
|
|
78
|
+
include Alba::Resource
|
|
79
|
+
|
|
80
|
+
attributes :id, :title, :body
|
|
81
|
+
|
|
82
|
+
# Nested serializer
|
|
83
|
+
one :author, resource: UserSerializer
|
|
84
|
+
|
|
85
|
+
# Collection
|
|
86
|
+
many :comments, resource: CommentSerializer
|
|
87
|
+
|
|
88
|
+
# Inline definition
|
|
89
|
+
many :tags do
|
|
90
|
+
attributes :id, :name
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Conditional Attributes
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
class UserSerializer
|
|
99
|
+
include Alba::Resource
|
|
100
|
+
|
|
101
|
+
attributes :id, :name, :email
|
|
102
|
+
|
|
103
|
+
# Conditional attribute
|
|
104
|
+
attribute :admin_notes, if: proc { |user, context|
|
|
105
|
+
context[:current_user]&.admin?
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Method-based condition
|
|
109
|
+
attribute :phone, if: :show_phone?
|
|
110
|
+
|
|
111
|
+
def show_phone?(user)
|
|
112
|
+
user.phone_public?
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Pass context
|
|
117
|
+
UserSerializer.new(user, params: { current_user: current_user }).serialize
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Computed Attributes
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
class PostSerializer
|
|
124
|
+
include Alba::Resource
|
|
125
|
+
|
|
126
|
+
attributes :id, :title
|
|
127
|
+
|
|
128
|
+
attribute :is_published do |post|
|
|
129
|
+
post.published_at.present?
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
attribute :reading_time do |post|
|
|
133
|
+
(post.body.split.size / 200.0).ceil
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
attribute :url do |post|
|
|
137
|
+
Rails.application.routes.url_helpers.post_url(post)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Root Key
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
class UserSerializer
|
|
146
|
+
include Alba::Resource
|
|
147
|
+
|
|
148
|
+
root_key :user, :users # singular, plural
|
|
149
|
+
|
|
150
|
+
attributes :id, :name
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
UserSerializer.new(user).serialize
|
|
154
|
+
# => '{"user":{"id":1,"name":"John"}}'
|
|
155
|
+
|
|
156
|
+
UserSerializer.new(users).serialize
|
|
157
|
+
# => '{"users":[{"id":1,"name":"John"},...]}'
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Base Serializer
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
class ApplicationSerializer
|
|
164
|
+
include Alba::Resource
|
|
165
|
+
include Rails.application.routes.url_helpers
|
|
166
|
+
|
|
167
|
+
# Common configuration
|
|
168
|
+
transform_keys :lower_camel
|
|
169
|
+
|
|
170
|
+
# Helper available to all serializers
|
|
171
|
+
def current_time
|
|
172
|
+
Time.current.iso8601
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
class UserSerializer < ApplicationSerializer
|
|
177
|
+
attributes :id, :name
|
|
178
|
+
|
|
179
|
+
attribute :serialized_at do
|
|
180
|
+
current_time
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Key Transformation
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
class UserSerializer
|
|
189
|
+
include Alba::Resource
|
|
190
|
+
|
|
191
|
+
# Transform all keys
|
|
192
|
+
transform_keys :lower_camel # firstName
|
|
193
|
+
transform_keys :dash # first-name
|
|
194
|
+
|
|
195
|
+
attributes :first_name, :last_name # becomes firstName, lastName
|
|
196
|
+
end
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## TypeScript Generation
|
|
200
|
+
|
|
201
|
+
With typelizer:
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
# Gemfile
|
|
205
|
+
gem "typelizer"
|
|
206
|
+
|
|
207
|
+
class ApplicationSerializer
|
|
208
|
+
include Alba::Resource
|
|
209
|
+
include Typelizer::DSL
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
class UserSerializer < ApplicationSerializer
|
|
213
|
+
typelize_from User
|
|
214
|
+
|
|
215
|
+
attributes :id, :email
|
|
216
|
+
attribute :is_admin, &:admin?
|
|
217
|
+
end
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Generated TypeScript:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
interface User {
|
|
224
|
+
id: number;
|
|
225
|
+
email: string;
|
|
226
|
+
is_admin: boolean;
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Performance Tips
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
# Use select to avoid loading unused columns
|
|
234
|
+
users = User.select(:id, :name, :email)
|
|
235
|
+
UserSerializer.new(users).serialize
|
|
236
|
+
|
|
237
|
+
# Preload associations
|
|
238
|
+
posts = Post.includes(:author, :comments)
|
|
239
|
+
PostSerializer.new(posts).serialize
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Testing
|
|
243
|
+
|
|
244
|
+
```ruby
|
|
245
|
+
RSpec.describe UserSerializer do
|
|
246
|
+
let(:user) { create(:user, first_name: "John", last_name: "Doe") }
|
|
247
|
+
let(:serialized) { JSON.parse(described_class.new(user).serialize) }
|
|
248
|
+
|
|
249
|
+
it "includes expected attributes" do
|
|
250
|
+
expect(serialized).to include(
|
|
251
|
+
"id" => user.id,
|
|
252
|
+
"email" => user.email,
|
|
253
|
+
"full_name" => "John Doe"
|
|
254
|
+
)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
```
|