plutonium 0.33.1 → 0.34.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 +4 -4
- data/# Plutonium: The pre-alpha demo.md +4 -2
- data/.claude/skills/assets/SKILL.md +416 -0
- data/.claude/skills/connect-resource/SKILL.md +112 -0
- data/.claude/skills/controller/SKILL.md +302 -0
- data/.claude/skills/create-resource/SKILL.md +240 -0
- data/.claude/skills/definition/SKILL.md +218 -0
- data/.claude/skills/definition-actions/SKILL.md +386 -0
- data/.claude/skills/definition-fields/SKILL.md +474 -0
- data/.claude/skills/definition-query/SKILL.md +334 -0
- data/.claude/skills/forms/SKILL.md +439 -0
- data/.claude/skills/installation/SKILL.md +300 -0
- data/.claude/skills/interaction/SKILL.md +382 -0
- data/.claude/skills/model/SKILL.md +267 -0
- data/.claude/skills/model-features/SKILL.md +286 -0
- data/.claude/skills/nested-resources/SKILL.md +274 -0
- data/.claude/skills/package/SKILL.md +191 -0
- data/.claude/skills/policy/SKILL.md +352 -0
- data/.claude/skills/portal/SKILL.md +400 -0
- data/.claude/skills/resource/SKILL.md +281 -0
- data/.claude/skills/rodauth/SKILL.md +452 -0
- data/.claude/skills/views/SKILL.md +563 -0
- data/Appraisals +46 -4
- data/CHANGELOG.md +32 -1
- data/app/assets/plutonium.css +2 -2
- data/config/brakeman.ignore +239 -0
- data/config/initializers/action_policy.rb +1 -1
- data/docs/.vitepress/config.ts +132 -47
- data/docs/concepts/architecture.md +226 -0
- data/docs/concepts/auto-detection.md +254 -0
- data/docs/concepts/index.md +61 -0
- data/docs/concepts/packages-portals.md +304 -0
- data/docs/concepts/resources.md +224 -0
- data/docs/cookbook/blog.md +412 -0
- data/docs/cookbook/index.md +289 -0
- data/docs/cookbook/saas.md +481 -0
- data/docs/getting-started/index.md +56 -0
- data/docs/getting-started/installation.md +146 -0
- data/docs/getting-started/tutorial/01-setup.md +118 -0
- data/docs/getting-started/tutorial/02-first-resource.md +180 -0
- data/docs/getting-started/tutorial/03-authentication.md +246 -0
- data/docs/getting-started/tutorial/04-authorization.md +170 -0
- data/docs/getting-started/tutorial/05-custom-actions.md +202 -0
- data/docs/getting-started/tutorial/06-nested-resources.md +147 -0
- data/docs/getting-started/tutorial/07-customizing-ui.md +254 -0
- data/docs/getting-started/tutorial/index.md +64 -0
- data/docs/guides/adding-resources.md +420 -0
- data/docs/guides/authentication.md +551 -0
- data/docs/guides/authorization.md +468 -0
- data/docs/guides/creating-packages.md +380 -0
- data/docs/guides/custom-actions.md +523 -0
- data/docs/guides/index.md +45 -0
- data/docs/guides/multi-tenancy.md +302 -0
- data/docs/guides/nested-resources.md +411 -0
- data/docs/guides/search-filtering.md +266 -0
- data/docs/guides/theming.md +321 -0
- data/docs/index.md +67 -26
- data/docs/public/CLAUDE.md +64 -21
- data/docs/reference/assets/index.md +496 -0
- data/docs/reference/controller/index.md +363 -0
- data/docs/reference/definition/actions.md +400 -0
- data/docs/reference/definition/fields.md +350 -0
- data/docs/reference/definition/index.md +252 -0
- data/docs/reference/definition/query.md +342 -0
- data/docs/reference/generators/index.md +469 -0
- data/docs/reference/index.md +49 -0
- data/docs/reference/interaction/index.md +445 -0
- data/docs/reference/model/features.md +248 -0
- data/docs/reference/model/index.md +219 -0
- data/docs/reference/policy/index.md +385 -0
- data/docs/reference/portal/index.md +382 -0
- data/docs/reference/views/forms.md +396 -0
- data/docs/reference/views/index.md +479 -0
- data/gemfiles/rails_7.gemfile +9 -2
- data/gemfiles/rails_7.gemfile.lock +146 -111
- data/gemfiles/rails_8.0.gemfile +20 -0
- data/gemfiles/rails_8.0.gemfile.lock +417 -0
- data/gemfiles/rails_8.1.gemfile +20 -0
- data/gemfiles/rails_8.1.gemfile.lock +419 -0
- data/lib/generators/pu/gem/dotenv/templates/.env +2 -0
- data/lib/generators/pu/gem/dotenv/templates/config/initializers/001_ensure_required_env.rb +3 -1
- data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +13 -16
- data/lib/generators/pu/pkg/portal/USAGE +65 -0
- data/lib/generators/pu/pkg/portal/portal_generator.rb +22 -9
- data/lib/generators/pu/res/conn/USAGE +71 -0
- data/lib/generators/pu/res/model/USAGE +106 -110
- data/lib/generators/pu/res/model/templates/model.rb.tt +6 -2
- data/lib/generators/pu/res/scaffold/USAGE +85 -0
- data/lib/generators/pu/rodauth/install_generator.rb +2 -6
- data/lib/generators/pu/rodauth/templates/config/initializers/url_options.rb +17 -0
- data/lib/generators/pu/skills/sync/USAGE +14 -0
- data/lib/generators/pu/skills/sync/sync_generator.rb +66 -0
- data/lib/plutonium/action_policy/sti_policy_lookup.rb +1 -1
- data/lib/plutonium/core/controller.rb +2 -2
- data/lib/plutonium/interaction/base.rb +1 -0
- data/lib/plutonium/package/engine.rb +2 -2
- data/lib/plutonium/query/adhoc_block.rb +6 -2
- data/lib/plutonium/query/model_scope.rb +1 -1
- data/lib/plutonium/railtie.rb +4 -0
- data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +1 -1
- data/lib/plutonium/resource/query_object.rb +38 -8
- data/lib/plutonium/ui/table/components/scopes_bar.rb +39 -34
- data/lib/plutonium/version.rb +1 -1
- data/lib/tasks/release.rake +19 -4
- data/package.json +1 -1
- metadata +76 -39
- data/brakeman.ignore +0 -28
- data/docs/api-examples.md +0 -49
- data/docs/guide/claude-code-guide.md +0 -74
- data/docs/guide/deep-dive/authorization.md +0 -189
- data/docs/guide/deep-dive/multitenancy.md +0 -256
- data/docs/guide/deep-dive/resources.md +0 -390
- data/docs/guide/getting-started/01-installation.md +0 -165
- data/docs/guide/index.md +0 -28
- data/docs/guide/introduction/01-what-is-plutonium.md +0 -211
- data/docs/guide/introduction/02-core-concepts.md +0 -440
- data/docs/guide/tutorial/01-project-setup.md +0 -75
- data/docs/guide/tutorial/02-creating-a-feature-package.md +0 -45
- data/docs/guide/tutorial/03-defining-resources.md +0 -90
- data/docs/guide/tutorial/04-creating-a-portal.md +0 -101
- data/docs/guide/tutorial/05-customizing-the-ui.md +0 -128
- data/docs/guide/tutorial/06-adding-custom-actions.md +0 -101
- data/docs/guide/tutorial/07-implementing-authorization.md +0 -90
- data/docs/markdown-examples.md +0 -85
- data/docs/modules/action.md +0 -244
- data/docs/modules/authentication.md +0 -236
- data/docs/modules/configuration.md +0 -599
- data/docs/modules/controller.md +0 -443
- data/docs/modules/core.md +0 -316
- data/docs/modules/definition.md +0 -1308
- data/docs/modules/display.md +0 -759
- data/docs/modules/form.md +0 -495
- data/docs/modules/generator.md +0 -400
- data/docs/modules/index.md +0 -167
- data/docs/modules/interaction.md +0 -642
- data/docs/modules/package.md +0 -151
- data/docs/modules/policy.md +0 -176
- data/docs/modules/portal.md +0 -710
- data/docs/modules/query.md +0 -297
- data/docs/modules/resource_record.md +0 -618
- data/docs/modules/routing.md +0 -690
- data/docs/modules/table.md +0 -301
- data/docs/modules/ui.md +0 -631
data/docs/modules/interaction.md
DELETED
|
@@ -1,642 +0,0 @@
|
|
|
1
|
-
# Interaction Module
|
|
2
|
-
|
|
3
|
-
The Interaction module provides a powerful architectural pattern for organizing business logic around user interactions and business actions. It builds upon the traditional MVC pattern by introducing additional layers that encapsulate business logic and improve separation of concerns.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
The Interaction module is located in `lib/plutonium/interaction/` and provides:
|
|
8
|
-
|
|
9
|
-
- Business logic encapsulation separate from controllers
|
|
10
|
-
- Consistent handling of success and failure cases
|
|
11
|
-
- Flexible and expressive operation chaining
|
|
12
|
-
- Integration with ActiveModel for validation
|
|
13
|
-
- Response handling for controller actions
|
|
14
|
-
- Outcome-based result handling
|
|
15
|
-
|
|
16
|
-
## Key Benefits
|
|
17
|
-
|
|
18
|
-
- Clear separation of business logic from controllers
|
|
19
|
-
- Improved testability of business operations
|
|
20
|
-
- Consistent handling of success and failure cases
|
|
21
|
-
- Flexible and expressive way to chain operations
|
|
22
|
-
- Enhanced maintainability and readability of complex business processes
|
|
23
|
-
- Improved code organization and discoverability of business logic
|
|
24
|
-
|
|
25
|
-
## Core Components
|
|
26
|
-
|
|
27
|
-
### Interaction Base (`lib/plutonium/interaction/base.rb`)
|
|
28
|
-
|
|
29
|
-
The foundation for all interactions, integrating with ActiveModel for attributes and validations.
|
|
30
|
-
|
|
31
|
-
```ruby
|
|
32
|
-
class CreateUserInteraction < Plutonium::Interaction::Base
|
|
33
|
-
attribute :first_name, :string
|
|
34
|
-
attribute :last_name, :string
|
|
35
|
-
attribute :email, :string
|
|
36
|
-
|
|
37
|
-
validates :first_name, :last_name, presence: true
|
|
38
|
-
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
39
|
-
|
|
40
|
-
private
|
|
41
|
-
|
|
42
|
-
def execute
|
|
43
|
-
user = User.new(attributes)
|
|
44
|
-
if user.save
|
|
45
|
-
succeed(user)
|
|
46
|
-
.with_redirect_response(user_path(user))
|
|
47
|
-
.with_message("User was successfully created.")
|
|
48
|
-
else
|
|
49
|
-
failed(user.errors)
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
#### Key Methods
|
|
56
|
-
|
|
57
|
-
- `call(view_context:, **attributes)` - Class method to execute the interaction
|
|
58
|
-
- `succeed(value)` - Create a successful outcome (aliased as `success`)
|
|
59
|
-
- `failed(errors)` - Create a failed outcome
|
|
60
|
-
- `attributes` - Access to all defined attributes
|
|
61
|
-
- `valid?` / `invalid?` - ActiveModel validation methods
|
|
62
|
-
|
|
63
|
-
### Outcome (`lib/plutonium/interaction/outcome.rb`)
|
|
64
|
-
|
|
65
|
-
Encapsulates the result of an interaction with success/failure state and optional response.
|
|
66
|
-
|
|
67
|
-
#### Success Outcome
|
|
68
|
-
|
|
69
|
-
```ruby
|
|
70
|
-
# Creating a success outcome
|
|
71
|
-
outcome = succeed(user)
|
|
72
|
-
.with_message("User created successfully")
|
|
73
|
-
.with_redirect_response(user_path(user))
|
|
74
|
-
|
|
75
|
-
# Checking outcome
|
|
76
|
-
outcome.success? # => true
|
|
77
|
-
outcome.failure? # => false
|
|
78
|
-
outcome.value # => user object
|
|
79
|
-
outcome.messages # => [["User created successfully", :notice]]
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
#### Failure Outcome
|
|
83
|
-
|
|
84
|
-
```ruby
|
|
85
|
-
# Creating a failure outcome
|
|
86
|
-
outcome = failed(user.errors)
|
|
87
|
-
.with_message("Failed to create user", :error)
|
|
88
|
-
|
|
89
|
-
# Checking outcome
|
|
90
|
-
outcome.success? # => false
|
|
91
|
-
outcome.failure? # => true
|
|
92
|
-
outcome.messages # => [["Failed to create user", :error]]
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
#### Outcome Chaining
|
|
96
|
-
|
|
97
|
-
```ruby
|
|
98
|
-
def execute
|
|
99
|
-
CreateUserInteraction.call(view_context: view_context, **user_params)
|
|
100
|
-
.and_then { |user| SendWelcomeEmailInteraction.call(view_context: view_context, user: user) }
|
|
101
|
-
.and_then { |result| LogUserCreationInteraction.call(view_context: view_context, user: result.value) }
|
|
102
|
-
.with_redirect_response(dashboard_path)
|
|
103
|
-
.with_message("Welcome! Your account has been created.")
|
|
104
|
-
end
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### Response System (`lib/plutonium/interaction/response/`)
|
|
108
|
-
|
|
109
|
-
Handles controller responses after successful interactions.
|
|
110
|
-
|
|
111
|
-
#### Built-in Response Types
|
|
112
|
-
|
|
113
|
-
**Redirect Response**
|
|
114
|
-
```ruby
|
|
115
|
-
.with_redirect_response(user_path(user))
|
|
116
|
-
.with_redirect_response(posts_path, notice: "Post created")
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
**Render Response**
|
|
120
|
-
```ruby
|
|
121
|
-
.with_render_response(:show, locals: { user: user })
|
|
122
|
-
.with_render_response(:edit, status: :unprocessable_content)
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
**File Response**
|
|
126
|
-
```ruby
|
|
127
|
-
.with_file_response(file_path, filename: "report.pdf")
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
**Null Response**
|
|
131
|
-
```ruby
|
|
132
|
-
# Default response when no specific response is set
|
|
133
|
-
# Allows controller to handle response manually
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
#### Processing Responses in Controllers
|
|
137
|
-
|
|
138
|
-
```ruby
|
|
139
|
-
class ApplicationController < ActionController::Base
|
|
140
|
-
private
|
|
141
|
-
|
|
142
|
-
def handle_interaction_outcome(outcome)
|
|
143
|
-
if outcome.success?
|
|
144
|
-
outcome.to_response.process(self) do |value|
|
|
145
|
-
# Default response if no specific response is set
|
|
146
|
-
render json: { success: true, data: value }
|
|
147
|
-
end
|
|
148
|
-
else
|
|
149
|
-
outcome.messages.each { |msg, type| flash.now[type || :error] = msg }
|
|
150
|
-
render json: { errors: outcome.errors }, status: :unprocessable_content
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
### Nested Attributes (`lib/plutonium/interaction/nested_attributes.rb`)
|
|
157
|
-
|
|
158
|
-
Handle nested resource attributes in interactions.
|
|
159
|
-
|
|
160
|
-
```ruby
|
|
161
|
-
class CreatePostWithTagsInteraction < Plutonium::Interaction::Base
|
|
162
|
-
include Plutonium::Interaction::NestedAttributes
|
|
163
|
-
|
|
164
|
-
attribute :title, :string
|
|
165
|
-
attribute :content, :text
|
|
166
|
-
attribute :tags_attributes, :array
|
|
167
|
-
|
|
168
|
-
validates :title, :content, presence: true
|
|
169
|
-
|
|
170
|
-
private
|
|
171
|
-
|
|
172
|
-
def execute
|
|
173
|
-
post = Post.new(title: title, content: content)
|
|
174
|
-
|
|
175
|
-
if post.save
|
|
176
|
-
process_nested_attributes(post, :tags, tags_attributes)
|
|
177
|
-
succeed(post)
|
|
178
|
-
else
|
|
179
|
-
failed(post.errors)
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
## Usage Patterns
|
|
186
|
-
|
|
187
|
-
### Basic Interaction
|
|
188
|
-
|
|
189
|
-
```ruby
|
|
190
|
-
# Define the interaction
|
|
191
|
-
class PublishPostInteraction < Plutonium::Interaction::Base
|
|
192
|
-
attribute :post_id, :integer
|
|
193
|
-
attribute :published_at, :datetime, default: -> { Time.current }
|
|
194
|
-
|
|
195
|
-
validates :post_id, presence: true
|
|
196
|
-
|
|
197
|
-
private
|
|
198
|
-
|
|
199
|
-
def execute
|
|
200
|
-
post = Post.find(post_id)
|
|
201
|
-
|
|
202
|
-
if post.update(published: true, published_at: published_at)
|
|
203
|
-
succeed(post)
|
|
204
|
-
.with_message("Post published successfully")
|
|
205
|
-
.with_redirect_response(post_path(post))
|
|
206
|
-
else
|
|
207
|
-
failed(post.errors)
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
# Use in controller
|
|
213
|
-
class PostsController < ApplicationController
|
|
214
|
-
def publish
|
|
215
|
-
outcome = PublishPostInteraction.call(
|
|
216
|
-
view_context: view_context,
|
|
217
|
-
post_id: params[:id]
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
handle_interaction_outcome(outcome)
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
### Complex Business Logic
|
|
226
|
-
|
|
227
|
-
```ruby
|
|
228
|
-
class ProcessOrderInteraction < Plutonium::Interaction::Base
|
|
229
|
-
attribute :order_id, :integer
|
|
230
|
-
attribute :payment_method, :string
|
|
231
|
-
attribute :shipping_address, :string
|
|
232
|
-
|
|
233
|
-
validates :order_id, :payment_method, :shipping_address, presence: true
|
|
234
|
-
|
|
235
|
-
private
|
|
236
|
-
|
|
237
|
-
def execute
|
|
238
|
-
order = Order.find(order_id)
|
|
239
|
-
|
|
240
|
-
# Validate order can be processed
|
|
241
|
-
return failed("Order already processed") if order.processed?
|
|
242
|
-
return failed("Insufficient inventory") unless check_inventory(order)
|
|
243
|
-
|
|
244
|
-
# Process payment
|
|
245
|
-
payment_result = process_payment(order)
|
|
246
|
-
return failed(payment_result.errors) unless payment_result.success?
|
|
247
|
-
|
|
248
|
-
# Update order
|
|
249
|
-
order.update!(
|
|
250
|
-
status: 'processing',
|
|
251
|
-
payment_method: payment_method,
|
|
252
|
-
shipping_address: shipping_address,
|
|
253
|
-
processed_at: Time.current
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
# Send notifications
|
|
257
|
-
OrderMailer.confirmation_email(order).deliver_later
|
|
258
|
-
NotifyWarehouseJob.perform_later(order)
|
|
259
|
-
|
|
260
|
-
succeed(order)
|
|
261
|
-
.with_message("Order processed successfully")
|
|
262
|
-
.with_redirect_response(order_path(order))
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
def check_inventory(order)
|
|
266
|
-
order.line_items.all? { |item| item.product.stock >= item.quantity }
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
def process_payment(order)
|
|
270
|
-
PaymentService.charge(
|
|
271
|
-
amount: order.total,
|
|
272
|
-
method: payment_method,
|
|
273
|
-
order_id: order.id
|
|
274
|
-
)
|
|
275
|
-
end
|
|
276
|
-
end
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
### Interaction Composition
|
|
280
|
-
|
|
281
|
-
```ruby
|
|
282
|
-
class CompleteUserOnboardingInteraction < Plutonium::Interaction::Base
|
|
283
|
-
attribute :user_id, :integer
|
|
284
|
-
attribute :profile_data, :hash
|
|
285
|
-
attribute :preferences, :hash
|
|
286
|
-
|
|
287
|
-
private
|
|
288
|
-
|
|
289
|
-
def execute
|
|
290
|
-
user = User.find(user_id)
|
|
291
|
-
|
|
292
|
-
# Chain multiple interactions
|
|
293
|
-
UpdateUserProfileInteraction.call(view_context: view_context, user: user, **profile_data)
|
|
294
|
-
.and_then { |result| SetUserPreferencesInteraction.call(view_context: view_context, user: result.value, **preferences) }
|
|
295
|
-
.and_then { |result| SendWelcomeEmailInteraction.call(view_context: view_context, user: result.value) }
|
|
296
|
-
.and_then { |result| CreateDefaultDashboardInteraction.call(view_context: view_context, user: result.value) }
|
|
297
|
-
.with_message("Welcome! Your account setup is complete.")
|
|
298
|
-
.with_redirect_response(dashboard_path)
|
|
299
|
-
end
|
|
300
|
-
end
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
## Integration with Plutonium
|
|
304
|
-
|
|
305
|
-
### Resource Actions
|
|
306
|
-
|
|
307
|
-
Interactions integrate seamlessly with resource definitions:
|
|
308
|
-
|
|
309
|
-
```ruby
|
|
310
|
-
class PostDefinition < Plutonium::Resource::Definition
|
|
311
|
-
action :publish, interaction: PublishPostInteraction
|
|
312
|
-
action :archive, interaction: ArchivePostInteraction
|
|
313
|
-
action :feature, interaction: FeaturePostInteraction
|
|
314
|
-
end
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
### Dynamic Route Actions
|
|
318
|
-
|
|
319
|
-
For actions that need dynamic URL generation, combine interactions with custom route options:
|
|
320
|
-
|
|
321
|
-
```ruby
|
|
322
|
-
class PostDefinition < Plutonium::Resource::Definition
|
|
323
|
-
# Simple interaction with static route
|
|
324
|
-
action :publish, interaction: PublishPostInteraction
|
|
325
|
-
|
|
326
|
-
# Complex action with dynamic route generation using RouteOptions
|
|
327
|
-
action :create_deployment,
|
|
328
|
-
label: "Create Deployment",
|
|
329
|
-
icon: Phlex::TablerIcons::Rocket,
|
|
330
|
-
record_action: true,
|
|
331
|
-
interaction: CreateDeploymentInteraction,
|
|
332
|
-
route_options: Plutonium::Action::RouteOptions.new(
|
|
333
|
-
url_resolver: ->(subject) {
|
|
334
|
-
resource_url_for(Deployment, action: :new, parent: subject)
|
|
335
|
-
}
|
|
336
|
-
)
|
|
337
|
-
|
|
338
|
-
# Conditional routing based on user permissions
|
|
339
|
-
action :manage_advanced_settings,
|
|
340
|
-
label: "Advanced Settings",
|
|
341
|
-
resource_action: true,
|
|
342
|
-
interaction: ManageAdvancedSettingsInteraction,
|
|
343
|
-
route_options: Plutonium::Action::RouteOptions.new(
|
|
344
|
-
url_resolver: ->(subject) {
|
|
345
|
-
if current_user.admin?
|
|
346
|
-
admin_settings_path(subject)
|
|
347
|
-
else
|
|
348
|
-
basic_settings_path(subject)
|
|
349
|
-
end
|
|
350
|
-
}
|
|
351
|
-
)
|
|
352
|
-
|
|
353
|
-
# External system integration with dynamic URLs
|
|
354
|
-
action :sync_with_external,
|
|
355
|
-
label: "Sync External",
|
|
356
|
-
record_action: true,
|
|
357
|
-
interaction: SyncExternalInteraction,
|
|
358
|
-
route_options: Plutonium::Action::RouteOptions.new(
|
|
359
|
-
url_resolver: ->(subject) {
|
|
360
|
-
"https://api.external-system.com/sync/#{subject.external_id}"
|
|
361
|
-
}
|
|
362
|
-
)
|
|
363
|
-
end
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
The `url_resolver` lambda provides powerful flexibility:
|
|
367
|
-
- **Record Actions**: Receive the current record as `subject`
|
|
368
|
-
- **Resource Actions**: Receive the resource class as `subject`
|
|
369
|
-
- **Bulk Actions**: Receive the resource class with selected records available in params
|
|
370
|
-
- **Context Access**: Full access to controller context including `current_user`, helper methods, etc.
|
|
371
|
-
|
|
372
|
-
### Advanced Dynamic Routing Examples
|
|
373
|
-
|
|
374
|
-
```ruby
|
|
375
|
-
class ProjectDefinition < Plutonium::Resource::Definition
|
|
376
|
-
# Multi-step workflow routing
|
|
377
|
-
action :start_workflow,
|
|
378
|
-
label: "Start Workflow",
|
|
379
|
-
record_action: true,
|
|
380
|
-
interaction: StartWorkflowInteraction,
|
|
381
|
-
route_options: Plutonium::Action::RouteOptions.new(
|
|
382
|
-
url_resolver: ->(subject) {
|
|
383
|
-
case subject.status
|
|
384
|
-
when 'draft'
|
|
385
|
-
new_project_review_path(subject)
|
|
386
|
-
when 'review'
|
|
387
|
-
project_approval_path(subject)
|
|
388
|
-
else
|
|
389
|
-
project_path(subject)
|
|
390
|
-
end
|
|
391
|
-
}
|
|
392
|
-
)
|
|
393
|
-
|
|
394
|
-
# Dynamic nested resource creation
|
|
395
|
-
action :add_team_member,
|
|
396
|
-
label: "Add Team Member",
|
|
397
|
-
record_action: true,
|
|
398
|
-
interaction: AddTeamMemberInteraction,
|
|
399
|
-
route_options: Plutonium::Action::RouteOptions.new(
|
|
400
|
-
url_resolver: ->(subject) {
|
|
401
|
-
if subject.team.full?
|
|
402
|
-
project_team_waitlist_path(subject)
|
|
403
|
-
else
|
|
404
|
-
new_project_team_member_path(subject)
|
|
405
|
-
end
|
|
406
|
-
}
|
|
407
|
-
)
|
|
408
|
-
|
|
409
|
-
# Conditional external redirects
|
|
410
|
-
action :open_in_ide,
|
|
411
|
-
label: "Open in IDE",
|
|
412
|
-
record_action: true,
|
|
413
|
-
interaction: OpenInIDEInteraction,
|
|
414
|
-
route_options: Plutonium::Action::RouteOptions.new(
|
|
415
|
-
url_resolver: ->(subject) {
|
|
416
|
-
if subject.repository_url.present?
|
|
417
|
-
"vscode://vscode.git/clone?url=#{subject.repository_url}"
|
|
418
|
-
else
|
|
419
|
-
project_repository_setup_path(subject)
|
|
420
|
-
end
|
|
421
|
-
}
|
|
422
|
-
)
|
|
423
|
-
end
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
### Custom Interaction with Dynamic Routing
|
|
427
|
-
|
|
428
|
-
```ruby
|
|
429
|
-
class CreateChildResourceInteraction < Plutonium::Interaction::Base
|
|
430
|
-
attribute :parent_id, :integer
|
|
431
|
-
attribute :resource_type, :string
|
|
432
|
-
attribute :attributes, :hash
|
|
433
|
-
|
|
434
|
-
validates :parent_id, :resource_type, presence: true
|
|
435
|
-
|
|
436
|
-
private
|
|
437
|
-
|
|
438
|
-
def execute
|
|
439
|
-
parent = find_parent_resource
|
|
440
|
-
child_class = resource_type.constantize
|
|
441
|
-
child = child_class.new(attributes.merge(parent_key => parent))
|
|
442
|
-
|
|
443
|
-
if child.save
|
|
444
|
-
succeed(child)
|
|
445
|
-
.with_message("#{resource_type} created successfully")
|
|
446
|
-
.with_redirect_response(resource_url_for(child, parent: parent))
|
|
447
|
-
else
|
|
448
|
-
failed(child.errors)
|
|
449
|
-
end
|
|
450
|
-
end
|
|
451
|
-
|
|
452
|
-
def find_parent_resource
|
|
453
|
-
# Dynamic parent resolution based on context
|
|
454
|
-
case resource_type
|
|
455
|
-
when 'Deployment'
|
|
456
|
-
Project.find(parent_id)
|
|
457
|
-
when 'Task'
|
|
458
|
-
Project.find(parent_id)
|
|
459
|
-
else
|
|
460
|
-
raise ArgumentError, "Unknown resource type: #{resource_type}"
|
|
461
|
-
end
|
|
462
|
-
end
|
|
463
|
-
|
|
464
|
-
def parent_key
|
|
465
|
-
case resource_type
|
|
466
|
-
when 'Deployment', 'Task'
|
|
467
|
-
:project_id
|
|
468
|
-
else
|
|
469
|
-
raise ArgumentError, "Unknown parent key for: #{resource_type}"
|
|
470
|
-
end
|
|
471
|
-
end
|
|
472
|
-
end
|
|
473
|
-
|
|
474
|
-
# Usage in resource definition
|
|
475
|
-
class ProjectDefinition < Plutonium::Resource::Definition
|
|
476
|
-
action :create_deployment,
|
|
477
|
-
label: "Create Deployment",
|
|
478
|
-
record_action: true,
|
|
479
|
-
interaction: CreateChildResourceInteraction,
|
|
480
|
-
route_options: Plutonium::Action::RouteOptions.new(
|
|
481
|
-
url_resolver: ->(subject) {
|
|
482
|
-
# The subject here will be the Project record
|
|
483
|
-
new_deployment_path(project_id: subject.id)
|
|
484
|
-
}
|
|
485
|
-
)
|
|
486
|
-
end
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
### Controller Integration
|
|
490
|
-
|
|
491
|
-
Controllers can call interactions directly, but this requires manual setup:
|
|
492
|
-
|
|
493
|
-
```ruby
|
|
494
|
-
class PostsController < ApplicationController
|
|
495
|
-
include Plutonium::Resource::Controller
|
|
496
|
-
|
|
497
|
-
# Manual controller action (requires custom routing)
|
|
498
|
-
def bulk_publish
|
|
499
|
-
outcome = BulkPublishPostsInteraction.call(
|
|
500
|
-
view_context: view_context,
|
|
501
|
-
post_ids: params[:post_ids],
|
|
502
|
-
published_at: params[:published_at]
|
|
503
|
-
)
|
|
504
|
-
|
|
505
|
-
# Manual response handling
|
|
506
|
-
if outcome.success?
|
|
507
|
-
redirect_to posts_path, notice: outcome.messages.first&.first
|
|
508
|
-
else
|
|
509
|
-
redirect_back(fallback_location: posts_path, alert: "Failed to publish posts")
|
|
510
|
-
end
|
|
511
|
-
end
|
|
512
|
-
end
|
|
513
|
-
```
|
|
514
|
-
|
|
515
|
-
**Note**: For automatic integration without manual setup, define actions in resource definitions instead:
|
|
516
|
-
|
|
517
|
-
```ruby
|
|
518
|
-
class PostDefinition < Plutonium::Resource::Definition
|
|
519
|
-
# This automatically handles routing, UI, and response processing
|
|
520
|
-
action :bulk_publish, interaction: BulkPublishPostsInteraction
|
|
521
|
-
end
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
### Form Integration
|
|
525
|
-
|
|
526
|
-
```ruby
|
|
527
|
-
class PostDefinition < Plutonium::Resource::Definition
|
|
528
|
-
# Form submission automatically uses interactions
|
|
529
|
-
action :create, interaction: CreatePostInteraction
|
|
530
|
-
action :update, interaction: UpdatePostInteraction
|
|
531
|
-
end
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
## Best Practices
|
|
535
|
-
|
|
536
|
-
### Interaction Design
|
|
537
|
-
|
|
538
|
-
1. **Single Responsibility**: Each interaction should handle one business operation
|
|
539
|
-
2. **Clear Naming**: Use descriptive names that indicate the business action
|
|
540
|
-
3. **Validation**: Validate inputs using ActiveModel validations
|
|
541
|
-
4. **Error Handling**: Return meaningful error messages
|
|
542
|
-
5. **Idempotency**: Design interactions to be safely re-runnable when possible
|
|
543
|
-
|
|
544
|
-
### Outcome Handling
|
|
545
|
-
|
|
546
|
-
1. **Consistent Responses**: Use appropriate response types for different scenarios
|
|
547
|
-
2. **Meaningful Messages**: Provide clear success/failure messages
|
|
548
|
-
3. **Proper Chaining**: Use `and_then` for sequential operations
|
|
549
|
-
4. **Error Propagation**: Let failures bubble up through chains
|
|
550
|
-
|
|
551
|
-
### Testing Strategy
|
|
552
|
-
|
|
553
|
-
1. **Unit Test Interactions**: Test business logic in isolation
|
|
554
|
-
2. **Mock External Services**: Use mocks for external dependencies
|
|
555
|
-
3. **Test Both Paths**: Cover both success and failure scenarios
|
|
556
|
-
4. **Integration Tests**: Test controller integration with system tests
|
|
557
|
-
|
|
558
|
-
### Performance Considerations
|
|
559
|
-
|
|
560
|
-
1. **Database Transactions**: Use transactions for multi-step operations
|
|
561
|
-
2. **Background Jobs**: Move slow operations to background jobs
|
|
562
|
-
3. **Caching**: Cache expensive computations when appropriate
|
|
563
|
-
4. **Batch Operations**: Use batch processing for bulk operations
|
|
564
|
-
|
|
565
|
-
## Advanced Features
|
|
566
|
-
|
|
567
|
-
### Custom Response Types
|
|
568
|
-
|
|
569
|
-
```ruby
|
|
570
|
-
class JsonResponse < Plutonium::Interaction::Response::Base
|
|
571
|
-
def initialize(data, status: :ok)
|
|
572
|
-
super()
|
|
573
|
-
@data = data
|
|
574
|
-
@status = status
|
|
575
|
-
end
|
|
576
|
-
|
|
577
|
-
private
|
|
578
|
-
|
|
579
|
-
def execute(controller, &)
|
|
580
|
-
controller.render json: @data, status: @status
|
|
581
|
-
end
|
|
582
|
-
end
|
|
583
|
-
|
|
584
|
-
# Usage
|
|
585
|
-
succeed(user).with_response(JsonResponse.new(user.as_json))
|
|
586
|
-
```
|
|
587
|
-
|
|
588
|
-
### Conditional Execution
|
|
589
|
-
|
|
590
|
-
```ruby
|
|
591
|
-
class ConditionalInteraction < Plutonium::Interaction::Base
|
|
592
|
-
attribute :condition, :boolean
|
|
593
|
-
attribute :data, :hash
|
|
594
|
-
|
|
595
|
-
private
|
|
596
|
-
|
|
597
|
-
def execute
|
|
598
|
-
return succeed(nil) unless condition
|
|
599
|
-
|
|
600
|
-
# Only execute if condition is true
|
|
601
|
-
result = expensive_operation(data)
|
|
602
|
-
succeed(result)
|
|
603
|
-
end
|
|
604
|
-
end
|
|
605
|
-
```
|
|
606
|
-
|
|
607
|
-
### Error Recovery
|
|
608
|
-
|
|
609
|
-
```ruby
|
|
610
|
-
class ResilientInteraction < Plutonium::Interaction::Base
|
|
611
|
-
private
|
|
612
|
-
|
|
613
|
-
def execute
|
|
614
|
-
primary_service_call
|
|
615
|
-
.or_else { fallback_service_call }
|
|
616
|
-
.or_else { failed("All services unavailable") }
|
|
617
|
-
end
|
|
618
|
-
|
|
619
|
-
def primary_service_call
|
|
620
|
-
# Try primary service
|
|
621
|
-
result = PrimaryService.call(attributes)
|
|
622
|
-
result.success? ? succeed(result.data) : failed(result.errors)
|
|
623
|
-
rescue StandardError => e
|
|
624
|
-
failed("Primary service error: #{e.message}")
|
|
625
|
-
end
|
|
626
|
-
|
|
627
|
-
def fallback_service_call
|
|
628
|
-
# Try fallback service
|
|
629
|
-
result = FallbackService.call(attributes)
|
|
630
|
-
result.success? ? succeed(result.data) : failed(result.errors)
|
|
631
|
-
rescue StandardError => e
|
|
632
|
-
failed("Fallback service error: #{e.message}")
|
|
633
|
-
end
|
|
634
|
-
end
|
|
635
|
-
```
|
|
636
|
-
|
|
637
|
-
## Related Modules
|
|
638
|
-
|
|
639
|
-
- **[Resource Record](./resource_record.md)** - Resource definitions and CRUD operations
|
|
640
|
-
- **[Definition](./definition.md)** - Resource definition DSL
|
|
641
|
-
- **[Core](./core.md)** - Base controller functionality
|
|
642
|
-
- **[Action](./action.md)** - Custom actions and operations
|