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.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/# Plutonium: The pre-alpha demo.md +4 -2
  3. data/.claude/skills/assets/SKILL.md +416 -0
  4. data/.claude/skills/connect-resource/SKILL.md +112 -0
  5. data/.claude/skills/controller/SKILL.md +302 -0
  6. data/.claude/skills/create-resource/SKILL.md +240 -0
  7. data/.claude/skills/definition/SKILL.md +218 -0
  8. data/.claude/skills/definition-actions/SKILL.md +386 -0
  9. data/.claude/skills/definition-fields/SKILL.md +474 -0
  10. data/.claude/skills/definition-query/SKILL.md +334 -0
  11. data/.claude/skills/forms/SKILL.md +439 -0
  12. data/.claude/skills/installation/SKILL.md +300 -0
  13. data/.claude/skills/interaction/SKILL.md +382 -0
  14. data/.claude/skills/model/SKILL.md +267 -0
  15. data/.claude/skills/model-features/SKILL.md +286 -0
  16. data/.claude/skills/nested-resources/SKILL.md +274 -0
  17. data/.claude/skills/package/SKILL.md +191 -0
  18. data/.claude/skills/policy/SKILL.md +352 -0
  19. data/.claude/skills/portal/SKILL.md +400 -0
  20. data/.claude/skills/resource/SKILL.md +281 -0
  21. data/.claude/skills/rodauth/SKILL.md +452 -0
  22. data/.claude/skills/views/SKILL.md +563 -0
  23. data/Appraisals +46 -4
  24. data/CHANGELOG.md +32 -1
  25. data/app/assets/plutonium.css +2 -2
  26. data/config/brakeman.ignore +239 -0
  27. data/config/initializers/action_policy.rb +1 -1
  28. data/docs/.vitepress/config.ts +132 -47
  29. data/docs/concepts/architecture.md +226 -0
  30. data/docs/concepts/auto-detection.md +254 -0
  31. data/docs/concepts/index.md +61 -0
  32. data/docs/concepts/packages-portals.md +304 -0
  33. data/docs/concepts/resources.md +224 -0
  34. data/docs/cookbook/blog.md +412 -0
  35. data/docs/cookbook/index.md +289 -0
  36. data/docs/cookbook/saas.md +481 -0
  37. data/docs/getting-started/index.md +56 -0
  38. data/docs/getting-started/installation.md +146 -0
  39. data/docs/getting-started/tutorial/01-setup.md +118 -0
  40. data/docs/getting-started/tutorial/02-first-resource.md +180 -0
  41. data/docs/getting-started/tutorial/03-authentication.md +246 -0
  42. data/docs/getting-started/tutorial/04-authorization.md +170 -0
  43. data/docs/getting-started/tutorial/05-custom-actions.md +202 -0
  44. data/docs/getting-started/tutorial/06-nested-resources.md +147 -0
  45. data/docs/getting-started/tutorial/07-customizing-ui.md +254 -0
  46. data/docs/getting-started/tutorial/index.md +64 -0
  47. data/docs/guides/adding-resources.md +420 -0
  48. data/docs/guides/authentication.md +551 -0
  49. data/docs/guides/authorization.md +468 -0
  50. data/docs/guides/creating-packages.md +380 -0
  51. data/docs/guides/custom-actions.md +523 -0
  52. data/docs/guides/index.md +45 -0
  53. data/docs/guides/multi-tenancy.md +302 -0
  54. data/docs/guides/nested-resources.md +411 -0
  55. data/docs/guides/search-filtering.md +266 -0
  56. data/docs/guides/theming.md +321 -0
  57. data/docs/index.md +67 -26
  58. data/docs/public/CLAUDE.md +64 -21
  59. data/docs/reference/assets/index.md +496 -0
  60. data/docs/reference/controller/index.md +363 -0
  61. data/docs/reference/definition/actions.md +400 -0
  62. data/docs/reference/definition/fields.md +350 -0
  63. data/docs/reference/definition/index.md +252 -0
  64. data/docs/reference/definition/query.md +342 -0
  65. data/docs/reference/generators/index.md +469 -0
  66. data/docs/reference/index.md +49 -0
  67. data/docs/reference/interaction/index.md +445 -0
  68. data/docs/reference/model/features.md +248 -0
  69. data/docs/reference/model/index.md +219 -0
  70. data/docs/reference/policy/index.md +385 -0
  71. data/docs/reference/portal/index.md +382 -0
  72. data/docs/reference/views/forms.md +396 -0
  73. data/docs/reference/views/index.md +479 -0
  74. data/gemfiles/rails_7.gemfile +9 -2
  75. data/gemfiles/rails_7.gemfile.lock +146 -111
  76. data/gemfiles/rails_8.0.gemfile +20 -0
  77. data/gemfiles/rails_8.0.gemfile.lock +417 -0
  78. data/gemfiles/rails_8.1.gemfile +20 -0
  79. data/gemfiles/rails_8.1.gemfile.lock +419 -0
  80. data/lib/generators/pu/gem/dotenv/templates/.env +2 -0
  81. data/lib/generators/pu/gem/dotenv/templates/config/initializers/001_ensure_required_env.rb +3 -1
  82. data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +13 -16
  83. data/lib/generators/pu/pkg/portal/USAGE +65 -0
  84. data/lib/generators/pu/pkg/portal/portal_generator.rb +22 -9
  85. data/lib/generators/pu/res/conn/USAGE +71 -0
  86. data/lib/generators/pu/res/model/USAGE +106 -110
  87. data/lib/generators/pu/res/model/templates/model.rb.tt +6 -2
  88. data/lib/generators/pu/res/scaffold/USAGE +85 -0
  89. data/lib/generators/pu/rodauth/install_generator.rb +2 -6
  90. data/lib/generators/pu/rodauth/templates/config/initializers/url_options.rb +17 -0
  91. data/lib/generators/pu/skills/sync/USAGE +14 -0
  92. data/lib/generators/pu/skills/sync/sync_generator.rb +66 -0
  93. data/lib/plutonium/action_policy/sti_policy_lookup.rb +1 -1
  94. data/lib/plutonium/core/controller.rb +2 -2
  95. data/lib/plutonium/interaction/base.rb +1 -0
  96. data/lib/plutonium/package/engine.rb +2 -2
  97. data/lib/plutonium/query/adhoc_block.rb +6 -2
  98. data/lib/plutonium/query/model_scope.rb +1 -1
  99. data/lib/plutonium/railtie.rb +4 -0
  100. data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +1 -1
  101. data/lib/plutonium/resource/query_object.rb +38 -8
  102. data/lib/plutonium/ui/table/components/scopes_bar.rb +39 -34
  103. data/lib/plutonium/version.rb +1 -1
  104. data/lib/tasks/release.rake +19 -4
  105. data/package.json +1 -1
  106. metadata +76 -39
  107. data/brakeman.ignore +0 -28
  108. data/docs/api-examples.md +0 -49
  109. data/docs/guide/claude-code-guide.md +0 -74
  110. data/docs/guide/deep-dive/authorization.md +0 -189
  111. data/docs/guide/deep-dive/multitenancy.md +0 -256
  112. data/docs/guide/deep-dive/resources.md +0 -390
  113. data/docs/guide/getting-started/01-installation.md +0 -165
  114. data/docs/guide/index.md +0 -28
  115. data/docs/guide/introduction/01-what-is-plutonium.md +0 -211
  116. data/docs/guide/introduction/02-core-concepts.md +0 -440
  117. data/docs/guide/tutorial/01-project-setup.md +0 -75
  118. data/docs/guide/tutorial/02-creating-a-feature-package.md +0 -45
  119. data/docs/guide/tutorial/03-defining-resources.md +0 -90
  120. data/docs/guide/tutorial/04-creating-a-portal.md +0 -101
  121. data/docs/guide/tutorial/05-customizing-the-ui.md +0 -128
  122. data/docs/guide/tutorial/06-adding-custom-actions.md +0 -101
  123. data/docs/guide/tutorial/07-implementing-authorization.md +0 -90
  124. data/docs/markdown-examples.md +0 -85
  125. data/docs/modules/action.md +0 -244
  126. data/docs/modules/authentication.md +0 -236
  127. data/docs/modules/configuration.md +0 -599
  128. data/docs/modules/controller.md +0 -443
  129. data/docs/modules/core.md +0 -316
  130. data/docs/modules/definition.md +0 -1308
  131. data/docs/modules/display.md +0 -759
  132. data/docs/modules/form.md +0 -495
  133. data/docs/modules/generator.md +0 -400
  134. data/docs/modules/index.md +0 -167
  135. data/docs/modules/interaction.md +0 -642
  136. data/docs/modules/package.md +0 -151
  137. data/docs/modules/policy.md +0 -176
  138. data/docs/modules/portal.md +0 -710
  139. data/docs/modules/query.md +0 -297
  140. data/docs/modules/resource_record.md +0 -618
  141. data/docs/modules/routing.md +0 -690
  142. data/docs/modules/table.md +0 -301
  143. data/docs/modules/ui.md +0 -631
@@ -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