plutonium 0.23.4 → 0.24.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/plutonium.css +2 -2
  3. data/config/initializers/rabl.rb +17 -0
  4. data/config/initializers/sqlite_json_alias.rb +1 -1
  5. data/docs/.vitepress/config.ts +60 -19
  6. data/docs/guide/cursor-rules.md +75 -0
  7. data/docs/guide/deep-dive/authorization.md +189 -0
  8. data/docs/guide/{getting-started → deep-dive}/resources.md +137 -0
  9. data/docs/guide/getting-started/{installation.md → 01-installation.md} +0 -105
  10. data/docs/guide/index.md +28 -0
  11. data/docs/guide/introduction/02-core-concepts.md +440 -0
  12. data/docs/guide/tutorial/01-project-setup.md +75 -0
  13. data/docs/guide/tutorial/02-creating-a-feature-package.md +45 -0
  14. data/docs/guide/tutorial/03-defining-resources.md +90 -0
  15. data/docs/guide/tutorial/04-creating-a-portal.md +101 -0
  16. data/docs/guide/tutorial/05-customizing-the-ui.md +128 -0
  17. data/docs/guide/tutorial/06-adding-custom-actions.md +101 -0
  18. data/docs/guide/tutorial/07-implementing-authorization.md +90 -0
  19. data/docs/index.md +24 -31
  20. data/docs/modules/action.md +190 -0
  21. data/docs/modules/authentication.md +236 -0
  22. data/docs/modules/configuration.md +599 -0
  23. data/docs/modules/controller.md +398 -0
  24. data/docs/modules/core.md +316 -0
  25. data/docs/modules/definition.md +876 -0
  26. data/docs/modules/display.md +759 -0
  27. data/docs/modules/form.md +465 -0
  28. data/docs/modules/generator.md +288 -0
  29. data/docs/modules/index.md +167 -0
  30. data/docs/modules/interaction.md +470 -0
  31. data/docs/modules/package.md +151 -0
  32. data/docs/modules/policy.md +176 -0
  33. data/docs/modules/portal.md +710 -0
  34. data/docs/modules/query.md +287 -0
  35. data/docs/modules/resource_record.md +618 -0
  36. data/docs/modules/routing.md +641 -0
  37. data/docs/modules/table.md +293 -0
  38. data/docs/modules/ui.md +631 -0
  39. data/docs/public/plutonium.mdc +667 -0
  40. data/lib/generators/pu/core/assets/assets_generator.rb +0 -5
  41. data/lib/plutonium/definition/nested_inputs.rb +0 -8
  42. data/lib/plutonium/resource/controller.rb +1 -1
  43. data/lib/plutonium/ui/display/resource.rb +7 -2
  44. data/lib/plutonium/ui/table/resource.rb +8 -3
  45. data/lib/plutonium/version.rb +1 -1
  46. metadata +36 -9
  47. data/docs/guide/getting-started/authorization.md +0 -296
  48. data/docs/guide/getting-started/core-concepts.md +0 -432
  49. data/docs/guide/getting-started/index.md +0 -21
  50. data/docs/guide/tutorial.md +0 -401
  51. /data/docs/guide/{what-is-plutonium.md → introduction/01-what-is-plutonium.md} +0 -0
@@ -0,0 +1,470 @@
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_entity)
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_entity
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
+ ### Controller Integration
318
+
319
+ Controllers can call interactions directly, but this requires manual setup:
320
+
321
+ ```ruby
322
+ class PostsController < ApplicationController
323
+ include Plutonium::Resource::Controller
324
+
325
+ # Manual controller action (requires custom routing)
326
+ def bulk_publish
327
+ outcome = BulkPublishPostsInteraction.call(
328
+ view_context: view_context,
329
+ post_ids: params[:post_ids],
330
+ published_at: params[:published_at]
331
+ )
332
+
333
+ # Manual response handling
334
+ if outcome.success?
335
+ redirect_to posts_path, notice: outcome.messages.first&.first
336
+ else
337
+ redirect_back(fallback_location: posts_path, alert: "Failed to publish posts")
338
+ end
339
+ end
340
+ end
341
+ ```
342
+
343
+ **Note**: For automatic integration without manual setup, define actions in resource definitions instead:
344
+
345
+ ```ruby
346
+ class PostDefinition < Plutonium::Resource::Definition
347
+ # This automatically handles routing, UI, and response processing
348
+ action :bulk_publish, interaction: BulkPublishPostsInteraction
349
+ end
350
+ ```
351
+
352
+ ### Form Integration
353
+
354
+ ```ruby
355
+ class PostDefinition < Plutonium::Resource::Definition
356
+ # Form submission automatically uses interactions
357
+ action :create, interaction: CreatePostInteraction
358
+ action :update, interaction: UpdatePostInteraction
359
+ end
360
+ ```
361
+
362
+ ## Best Practices
363
+
364
+ ### Interaction Design
365
+
366
+ 1. **Single Responsibility**: Each interaction should handle one business operation
367
+ 2. **Clear Naming**: Use descriptive names that indicate the business action
368
+ 3. **Validation**: Validate inputs using ActiveModel validations
369
+ 4. **Error Handling**: Return meaningful error messages
370
+ 5. **Idempotency**: Design interactions to be safely re-runnable when possible
371
+
372
+ ### Outcome Handling
373
+
374
+ 1. **Consistent Responses**: Use appropriate response types for different scenarios
375
+ 2. **Meaningful Messages**: Provide clear success/failure messages
376
+ 3. **Proper Chaining**: Use `and_then` for sequential operations
377
+ 4. **Error Propagation**: Let failures bubble up through chains
378
+
379
+ ### Testing Strategy
380
+
381
+ 1. **Unit Test Interactions**: Test business logic in isolation
382
+ 2. **Mock External Services**: Use mocks for external dependencies
383
+ 3. **Test Both Paths**: Cover both success and failure scenarios
384
+ 4. **Integration Tests**: Test controller integration with system tests
385
+
386
+ ### Performance Considerations
387
+
388
+ 1. **Database Transactions**: Use transactions for multi-step operations
389
+ 2. **Background Jobs**: Move slow operations to background jobs
390
+ 3. **Caching**: Cache expensive computations when appropriate
391
+ 4. **Batch Operations**: Use batch processing for bulk operations
392
+
393
+ ## Advanced Features
394
+
395
+ ### Custom Response Types
396
+
397
+ ```ruby
398
+ class JsonResponse < Plutonium::Interaction::Response::Base
399
+ def initialize(data, status: :ok)
400
+ super()
401
+ @data = data
402
+ @status = status
403
+ end
404
+
405
+ private
406
+
407
+ def execute(controller, &)
408
+ controller.render json: @data, status: @status
409
+ end
410
+ end
411
+
412
+ # Usage
413
+ succeed(user).with_response(JsonResponse.new(user.as_json))
414
+ ```
415
+
416
+ ### Conditional Execution
417
+
418
+ ```ruby
419
+ class ConditionalInteraction < Plutonium::Interaction::Base
420
+ attribute :condition, :boolean
421
+ attribute :data, :hash
422
+
423
+ private
424
+
425
+ def execute
426
+ return succeed(nil) unless condition
427
+
428
+ # Only execute if condition is true
429
+ result = expensive_operation(data)
430
+ succeed(result)
431
+ end
432
+ end
433
+ ```
434
+
435
+ ### Error Recovery
436
+
437
+ ```ruby
438
+ class ResilientInteraction < Plutonium::Interaction::Base
439
+ private
440
+
441
+ def execute
442
+ primary_service_call
443
+ .or_else { fallback_service_call }
444
+ .or_else { failed("All services unavailable") }
445
+ end
446
+
447
+ def primary_service_call
448
+ # Try primary service
449
+ result = PrimaryService.call(attributes)
450
+ result.success? ? succeed(result.data) : failed(result.errors)
451
+ rescue StandardError => e
452
+ failed("Primary service error: #{e.message}")
453
+ end
454
+
455
+ def fallback_service_call
456
+ # Try fallback service
457
+ result = FallbackService.call(attributes)
458
+ result.success? ? succeed(result.data) : failed(result.errors)
459
+ rescue StandardError => e
460
+ failed("Fallback service error: #{e.message}")
461
+ end
462
+ end
463
+ ```
464
+
465
+ ## Related Modules
466
+
467
+ - **[Resource Record](./resource_record.md)** - Resource definitions and CRUD operations
468
+ - **[Definition](./definition.md)** - Resource definition DSL
469
+ - **[Core](./core.md)** - Base controller functionality
470
+ - **[Action](./action.md)** - Custom actions and operations
@@ -0,0 +1,151 @@
1
+ ---
2
+ title: Package Module
3
+ ---
4
+
5
+ # Package Module
6
+
7
+ The Package module provides the foundation for modular application organization in Plutonium. It enables Rails engines to work within the Plutonium ecosystem by providing specialized engine configuration and view path management.
8
+
9
+ ::: tip
10
+ The core of the Package module is `Plutonium::Package::Engine`, which should be included in your package's `engine.rb` file.
11
+ :::
12
+
13
+ ## Overview
14
+
15
+ - **Engine Foundation**: Provides base functionality for all Plutonium packages (which are Rails Engines).
16
+ - **View Path Control**: Manages view lookups for proper isolation between packages.
17
+ - **Migration Management**: Automatically includes package migrations in the application's migration path.
18
+
19
+ ## Usage
20
+
21
+ The primary way to use the Package module is by including `Plutonium::Package::Engine` in your package's engine file. This is handled automatically by the generators.
22
+
23
+ ::: code-group
24
+ ```bash [Generate a Feature Package]
25
+ rails generate pu:pkg:package blogging
26
+ ```
27
+ ```ruby [packages/blogging/lib/engine.rb]
28
+ module Blogging
29
+ class Engine < Rails::Engine
30
+ # This inclusion provides the core package functionality.
31
+ include Plutonium::Package::Engine
32
+ end
33
+ end
34
+ ```
35
+ :::
36
+
37
+ ::: code-group
38
+ ```bash [Generate a Portal Package]
39
+ rails generate pu:pkg:portal admin
40
+ ```
41
+ ```ruby [packages/admin_portal/lib/engine.rb]
42
+ module AdminPortal
43
+ class Engine < Rails::Engine
44
+ # Portal::Engine includes Package::Engine, so you get both.
45
+ include Plutonium::Portal::Engine
46
+ end
47
+ end
48
+ ```
49
+ :::
50
+
51
+ ## Key Features
52
+
53
+ ### View Path Management
54
+
55
+ The Package module intentionally prevents Rails from automatically adding a package's view paths to the global lookup. Instead, view resolution is handled at the controller level by Plutonium's `Bootable` concern. This provides finer-grained control and ensures that packages remain isolated.
56
+
57
+ ### Migration Integration
58
+
59
+ Package migrations are automatically detected and added to the application's main `db/migrate` path. This allows you to run `rails db:migrate` from your application root, and it will correctly process migrations from all your packages.
60
+
61
+ ::: details Package Engine Implementation
62
+ The `Plutonium::Package::Engine` concern handles this automatically.
63
+ ```ruby
64
+ # lib/plutonium/package/engine.rb
65
+ module Plutonium
66
+ module Package
67
+ module Engine
68
+ extend ActiveSupport::Concern
69
+
70
+ included do
71
+ # This block hijacks the default Rails view path initializer
72
+ # and replaces it with an empty one, giving Plutonium control.
73
+ config.before_configuration do
74
+ # ... logic to find and disable the default `add_view_paths` initializer
75
+ end
76
+
77
+ # This initializer appends the package's migrations to the host app.
78
+ initializer :append_migrations do |app|
79
+ unless app.root.to_s.match root.to_s
80
+ config.paths["db/migrate"].expanded.each do |expanded_path|
81
+ app.config.paths["db/migrate"] << expanded_path
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ ```
90
+ :::
91
+
92
+ ## Package Loading
93
+
94
+ Packages are loaded automatically via `config/packages.rb`, which is created by the `pu:core:install` generator. This file simply finds and loads all `engine.rb` files within your `packages/` directory.
95
+
96
+ ```ruby
97
+ # config/packages.rb
98
+ # This file is required in `config/application.rb`
99
+
100
+ Dir.glob(File.expand_path("../packages/**/lib/engine.rb", __dir__)) do |package|
101
+ load package
102
+ end
103
+ ```
104
+
105
+ ::: tip
106
+ You can package and ship your packages as gems.
107
+ :::
108
+
109
+ ## Generator Integration
110
+
111
+ ### Package Generator
112
+
113
+ ```bash
114
+ # Create a feature package
115
+ rails generate pu:pkg:package blogging
116
+ ```
117
+
118
+ Generates the basic structure with `Plutonium::Package::Engine` included.
119
+
120
+ ### Portal Generator
121
+
122
+ ```bash
123
+ # Create a portal package
124
+ rails generate pu:pkg:portal admin
125
+ ```
126
+
127
+ Generates a portal structure with `Plutonium::Portal::Engine` (which includes Package::Engine).
128
+
129
+ ## Package Types
130
+
131
+ The package system supports two main types:
132
+
133
+ 1. **Feature Packages**: Business logic packages that include `Plutonium::Package::Engine`
134
+ 2. **Portal Packages**: User interface packages that include `Plutonium::Portal::Engine`
135
+
136
+ Both types benefit from the foundational features provided by the Package module.
137
+
138
+ ## Best Practices
139
+
140
+ 1. **Use Generators**: Always use `pu:pkg:package` or `pu:pkg:portal` generators
141
+ 2. **Namespace Consistency**: Keep package names consistent with their directory structure
142
+ 3. **Migration Organization**: Place package-specific migrations in the package's `db/migrate` directory
143
+ 4. **Engine Simplicity**: Keep engine.rb files minimal - they're just configuration points
144
+
145
+ ## Integration with Other Modules
146
+
147
+ The Package module works closely with:
148
+ - **Portal Module**: Provides the foundation for portal functionality
149
+ - **Generator Module**: Scaffolds package structure
150
+ - **Core Module**: Integrates with controller bootable system
151
+ - **Routing Module**: Supports resource registration within packages