plutonium 0.26.8 → 0.26.9

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.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plutonium
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.26.8
4
+ version: 0.26.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-08-11 00:00:00.000000000 Z
11
+ date: 2025-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: zeitwerk
@@ -495,7 +495,7 @@ files:
495
495
  - docs/.vitepress/theme/custom.css
496
496
  - docs/.vitepress/theme/index.ts
497
497
  - docs/api-examples.md
498
- - docs/guide/cursor-rules.md
498
+ - docs/guide/claude-code-guide.md
499
499
  - docs/guide/deep-dive/authorization.md
500
500
  - docs/guide/deep-dive/multitenancy.md
501
501
  - docs/guide/deep-dive/resources.md
@@ -531,13 +531,13 @@ files:
531
531
  - docs/modules/routing.md
532
532
  - docs/modules/table.md
533
533
  - docs/modules/ui.md
534
+ - docs/public/CLAUDE.md
534
535
  - docs/public/android-chrome-192x192.png
535
536
  - docs/public/android-chrome-512x512.png
536
537
  - docs/public/apple-touch-icon.png
537
538
  - docs/public/favicon-16x16.png
538
539
  - docs/public/favicon-32x32.png
539
540
  - docs/public/favicon.ico
540
- - docs/public/plutonium.mdc
541
541
  - docs/public/plutonium.png
542
542
  - docs/public/site.webmanifest
543
543
  - docs/public/templates/base.rb
@@ -1,565 +0,0 @@
1
- ---
2
- description: Expert Plutonium framework guidelines for AI-assisted Rails development with auto-detection, resource architecture, component-based UI, and RAD patterns
3
- globs:
4
- alwaysApply: true
5
- ---
6
- You are an expert Ruby on Rails developer specializing in the Plutonium framework. This cursor rule compiles comprehensive guidelines for building robust, maintainable Plutonium applications.
7
-
8
- # Plutonium Framework Development Guidelines
9
-
10
- ## Framework Overview
11
-
12
- Plutonium is a Rapid Application Development (RAD) toolkit that extends Rails with powerful conventions, patterns, and tools. It provides:
13
- - Resource-oriented architecture with declarative definitions
14
- - Modular packaging system (Feature Packages and Portal Packages)
15
- - Built-in authentication, authorization, and multi-tenancy
16
- - Component-based UI system built on Phlex
17
- - Business logic encapsulation through Interactions
18
- - Query objects for filtering and searching
19
-
20
- ## Project Structure & Setup
21
-
22
- ### Initial Setup
23
- - Use Ruby 3.2.2+ and Rails 7.1+
24
- - Create new apps with: `rails new app_name -a propshaft -j esbuild -c tailwind -m https://radioactive-labs.github.io/plutonium-core/templates/plutonium.rb`
25
- - For existing apps: `bin/rails app:template LOCATION=https://radioactive-labs.github.io/plutonium-core/templates/base.rb`
26
- - Run `rails generate pu:core:install` after adding the gem
27
-
28
- ### Directory Structure
29
- ```
30
- app/
31
- ├── controllers/
32
- │ ├── plutonium_controller.rb # Base controller
33
- │ └── resource_controller.rb # Base for resources
34
- ├── definitions/ # Resource definitions
35
- ├── interactions/ # Business logic
36
- ├── models/
37
- └── policies/ # Authorization policies
38
- config/
39
- ├── initializers/plutonium.rb # Main configuration
40
- └── packages.rb # Package registration
41
- packages/ # Modular features
42
- ```
43
-
44
- ## Resource Architecture
45
-
46
- ### Resource Components
47
- Every resource consists of 4 core components:
48
- 1. **Model** - ActiveRecord model with `include Plutonium::Resource::Record`
49
- 2. **Definition** - Declarative UI and behavior configuration
50
- 3. **Policy** - Authorization and access control
51
- 4. **Controller** - Auto-generated CRUD operations
52
-
53
- ### Creating Resources
54
- ```bash
55
- # Scaffold a complete resource
56
- rails generate pu:res:scaffold Post user:belongs_to title:string content:text 'published_at:datetime?'
57
-
58
- # Individual components
59
- rails generate pu:res:model Post title:string content:text
60
- rails generate pu:res:definition Post
61
- rails generate pu:res:policy Post
62
- ```
63
-
64
- ### Resource Models
65
- ```ruby
66
- class Post < ApplicationRecord
67
- include Plutonium::Resource::Record
68
-
69
- belongs_to :user
70
- has_many :comments, dependent: :destroy
71
-
72
- validates :title, :content, presence: true
73
-
74
- scope :published, -> { where.not(published_at: nil) }
75
- scope :drafts, -> { where(published_at: nil) }
76
- end
77
- ```
78
-
79
- ### Resource Definitions
80
- ```ruby
81
- class PostDefinition < Plutonium::Resource::Definition
82
- # Field declarations are OPTIONAL - all attributes are auto-detected from the model
83
- # You only need to declare fields when overriding auto-detected behavior
84
-
85
- # These would be auto-detected from your Post model:
86
- # - :title (string column) → :string field
87
- # - :content (text column) → :text field
88
- # - :published_at (datetime column) → :datetime field
89
- # - :author (belongs_to association) → :association field
90
-
91
- # Only declare fields when you want to override defaults:
92
- field :content, as: :rich_text # Override text → rich_text
93
- field :status, as: :select, choices: %w[draft published archived]
94
-
95
- # Display customization (show/index pages)
96
- display :title, as: :string
97
- display :content, as: :markdown
98
- display :status, as: :string
99
- display :author, as: :association
100
-
101
- # Conditional displays (for table columns and show pages)
102
- # Use for COSMETIC/STATE-BASED logic only, NOT authorization (use policies for that)
103
- display :debug_info, condition: -> { Rails.env.development? }
104
- display :internal_notes, condition: -> { object.status == 'draft' }
105
- display :parent_category, condition: -> { current_parent.present? }
106
-
107
- # Custom display with block (for complex rendering)
108
- display :custom_field do |field|
109
- # Return component instances directly in definition blocks (no 'render' needed)
110
- CustomComponent.new(value: field.value)
111
- end
112
-
113
- # Advanced custom display with phlexi_tag (use sparingly)
114
- display :status, as: :phlexi_tag, with: ->(value, attrs) {
115
- # SGML methods available here because proc is evaluated in rendering context
116
- span(class: "badge badge-#{value}") { value.humanize }
117
- }
118
-
119
- # Input configuration (new/edit forms)
120
- input :title, placeholder: "Enter post title" # No need for as: :string (auto-detected)
121
- input :content, as: :rich_text
122
- input :category, as: :select, choices: %w[Tech Business Lifestyle]
123
-
124
- # New: Custom component classes
125
- input :color_picker, as: ColorPickerComponent
126
- display :status_badge, as: StatusBadgeComponent
127
-
128
- # Input blocks for custom logic (must use form builder methods)
129
- input :birth_date do |f|
130
- case object.age_category
131
- when 'adult'
132
- f.date_tag(min: 18.years.ago.to_date)
133
- else
134
- f.date_tag
135
- end
136
- end
137
-
138
- # Conditional inputs (for forms only)
139
- # Use for COSMETIC/STATE-BASED logic only, NOT authorization (use policies for that)
140
- input :debug_info, condition: -> { Rails.env.development? }
141
- input :internal_notes, condition: -> { object.status == 'draft' }
142
- input :parent_category, condition: -> { current_parent.present? }
143
- input :team_projects, condition: -> { current_user.team_lead? }
144
-
145
- # Search functionality
146
- search do |scope, query|
147
- scope.where("title ILIKE ? OR content ILIKE ?", "%#{query}%", "%#{query}%")
148
- end
149
-
150
- # Filters
151
- filter :published, with: Plutonium::Query::Filters::Text, predicate: :eq
152
- filter :category, with: Plutonium::Query::Filters::Text, predicate: :contains
153
- filter :title, with: Plutonium::Query::Filters::Text, predicate: :contains
154
-
155
- # Scopes (appear as buttons)
156
- scope :published
157
- scope :drafts
158
- scope :recent, -> { where('created_at > ?', 1.week.ago) }
159
-
160
- # Custom actions
161
- action :publish, interaction: PublishPostInteraction, icon: Phlex::TablerIcons::Send
162
- action :archive, interaction: ArchivePostInteraction, color: :warning
163
-
164
- # Page customization
165
- index_page_title "All Posts"
166
- show_page_title "Post Details"
167
- end
168
- ```
169
-
170
- ### Available Field Types (Auto-Detected from Model)
171
- - Text: `:string`, `:text`, `:rich_text`, `:markdown`
172
- - Numeric: `:number`, `:integer`, `:decimal`
173
- - Boolean: `:boolean`
174
- - Date/Time: `:date`, `:datetime`, `:time`
175
- - Selection: `:select`, `:radio_buttons`, `:check_boxes`
176
- - Files: `:file` (with `multiple: true` for multiple files)
177
- - Associations: `:association`
178
- - Special: `:hidden`, `:email`, `:url`, `:color`
179
-
180
- ### Conditional Rendering Guidelines
181
-
182
- **IMPORTANT**: Use conditions for COSMETIC/STATE-BASED logic only, NOT for authorization.
183
-
184
- #### ✅ Appropriate Uses
185
- - **Environment-based**: Show debug info in development only
186
- - **Object state-based**: Show fields based on record status
187
- - **Context-based**: Show fields based on parent presence
188
- - **Dynamic behavior**: Show dependent fields conditionally
189
-
190
- ```ruby
191
- # Environment-based visibility
192
- display :debug_info, condition: -> { Rails.env.development? }
193
-
194
- # Object state-based visibility
195
- display :approval_date, condition: -> { object.approved? }
196
- input :rejection_reason, condition: -> { object.rejected? }
197
-
198
- # Parent context-based visibility
199
- display :parent_category, condition: -> { current_parent.present? }
200
-
201
- # Dynamic form behavior
202
- input :end_date, condition: -> { object.start_date.present? }
203
- ```
204
-
205
- #### ❌ Inappropriate Uses
206
- Authorization logic belongs in policies, not conditions:
207
-
208
- ```ruby
209
- # DON'T use conditions for role-based authorization
210
- display :admin_notes, condition: -> { current_user&.admin? } # Use policy instead
211
- input :sensitive_data, condition: -> { current_user&.can_edit_sensitive? } # Use policy instead
212
- ```
213
-
214
- ### Available Configuration Options
215
-
216
- #### Field Options
217
- ```ruby
218
- field :name, as: :string, class: "custom-class", wrapper: {class: "field-wrapper"}
219
- ```
220
-
221
- #### Input Options
222
- ```ruby
223
- input :title,
224
- as: :string,
225
- placeholder: "Enter title",
226
- required: true,
227
- class: "custom-input",
228
- wrapper: {class: "input-wrapper"},
229
- data: {controller: "custom"},
230
- condition: -> { object.status == 'draft' } # Cosmetic condition only
231
- ```
232
-
233
- #### Display Options
234
- ```ruby
235
- display :content,
236
- as: :markdown,
237
- class: "prose",
238
- wrapper: {class: "content-wrapper"},
239
- condition: -> { Rails.env.development? } # Cosmetic condition only
240
- ```
241
-
242
- #### Choices Options (for selects)
243
- ```ruby
244
- # Static choices
245
- input :category, as: :select, choices: %w[Tech Business Lifestyle]
246
-
247
- # Dynamic choices require block form
248
- input :author do |f|
249
- choices = User.active.pluck(:name, :id)
250
- f.select_tag choices: choices
251
- end
252
-
253
- # Dynamic choices with access to context
254
- input :team_members do |f|
255
- choices = current_user.organization.users.active.pluck(:name, :id)
256
- f.select_tag choices: choices
257
- end
258
-
259
- # Dynamic choices based on object state
260
- input :related_posts do |f|
261
- choices = if object.persisted?
262
- Post.where.not(id: object.id).published.pluck(:title, :id)
263
- else
264
- []
265
- end
266
- f.select_tag choices: choices
267
- end
268
- ```
269
-
270
- #### File Upload Options
271
- ```ruby
272
- input :avatar, as: :file, multiple: false
273
- input :documents, as: :file, multiple: true
274
- # Note: Advanced file options like allowed_file_types and max_file_size
275
- # are not currently supported by the framework
276
- ```
277
-
278
- ### Resource Policies
279
-
280
- Policies control authorization and data access. They define who can perform what actions and what data users can see.
281
-
282
- ```ruby
283
- class PostPolicy < Plutonium::Resource::Policy
284
- # CRUD permissions
285
- def index?
286
- true # Everyone can view list
287
- end
288
-
289
- def show?
290
- record.published? || record.author == user || user.admin?
291
- end
292
-
293
- def create?
294
- user.present?
295
- end
296
-
297
- def update?
298
- record.author == user || user.admin?
299
- end
300
-
301
- def destroy?
302
- user.admin?
303
- end
304
-
305
- # Custom action permissions
306
- def publish?
307
- update? && record.draft?
308
- end
309
-
310
- # Attribute permissions - control which fields are accessible
311
- def permitted_attributes_for_read
312
- attrs = [:title, :content, :category, :published_at]
313
- attrs << :internal_notes if user.admin?
314
- attrs
315
- end
316
-
317
- def permitted_attributes_for_create
318
- [:title, :content, :category]
319
- end
320
-
321
- def permitted_attributes_for_update
322
- attrs = permitted_attributes_for_create
323
- attrs << :slug if user.admin?
324
- attrs
325
- end
326
-
327
- # Collection scoping - filter what records users can see
328
- relation_scope do |relation|
329
- relation = super(relation) # Apply entity scoping first
330
-
331
- if user.admin?
332
- relation
333
- else
334
- # Users see only their posts or published posts
335
- relation.where(author: user).or(relation.where(published: true))
336
- end
337
- end
338
-
339
- # Association permissions
340
- def permitted_associations
341
- [:comments, :author, :tags]
342
- end
343
- end
344
- ```
345
-
346
- ## Business Logic with Interactions
347
-
348
- ### Interaction Structure
349
- ```ruby
350
- class PublishPostInteraction < Plutonium::Resource::Interaction
351
- # Metadata for UI
352
- presents label: "Publish Post",
353
- icon: Phlex::TablerIcons::Send,
354
- description: "Make this post visible to readers"
355
-
356
- # Attributes (form inputs)
357
- attribute :resource, class: Post
358
- attribute :publish_date, :datetime, default: -> { Time.current }
359
- attribute :notify_subscribers, :boolean, default: true
360
-
361
- # Validations
362
- validates :resource, presence: true
363
- validates :publish_date, presence: true
364
-
365
- private
366
-
367
- def execute
368
- # Business logic
369
- resource.transaction do
370
- resource.update!(
371
- published_at: publish_date,
372
- status: 'published'
373
- )
374
-
375
- # Side effects
376
- NotifySubscribersJob.perform_later(resource) if notify_subscribers
377
-
378
- # Return successful outcome
379
- succeed(resource)
380
- .with_message("Post published successfully!")
381
- .with_redirect_response(resource_path(resource))
382
- end
383
- rescue => error
384
- # Return failure outcome
385
- failed(error.message)
386
- end
387
- end
388
- ```
389
-
390
- ### Interaction Types (Auto-detected)
391
- - **Record Actions**: Have `attribute :resource` - work on single records
392
- - **Bulk Actions**: Have `attribute :resources` - work on multiple records
393
- - **Resource Actions**: Have neither - work at resource level (imports, etc.)
394
-
395
- ### Interaction Execution Patterns
396
- - **Immediate**: Only `resource`/`resources` attribute → executes on click
397
- - **Modal**: Additional attributes → shows form modal first
398
-
399
- ## Package Architecture
400
-
401
- ### Feature Packages
402
- Contain business logic and domain models:
403
- ```bash
404
- rails generate pu:pkg:package blogging
405
- ```
406
-
407
- Structure:
408
- ```
409
- packages/blogging/
410
- ├── app/
411
- │ ├── models/blogging/
412
- │ ├── definitions/blogging/
413
- │ ├── policies/blogging/
414
- │ ├── interactions/blogging/
415
- │ └── controllers/blogging/
416
- └── lib/engine.rb
417
- ```
418
-
419
- ### Portal Packages
420
- Provide web interfaces with routing and authentication:
421
- ```bash
422
- rails generate pu:pkg:portal admin
423
- ```
424
-
425
- Portal Engine:
426
- ```ruby
427
- module AdminPortal
428
- class Engine < ::Rails::Engine
429
- include Plutonium::Portal::Engine
430
-
431
- # Multi-tenancy
432
- scope_to_entity Organization, strategy: :path
433
- end
434
- end
435
- ```
436
-
437
- Portal Routes:
438
- ```ruby
439
- AdminPortal::Engine.routes.draw do
440
- register_resource Blog::Post
441
- register_resource Blog::Comment
442
- register_resource User
443
- end
444
- ```
445
-
446
- Portal Controller:
447
- ```ruby
448
- module AdminPortal
449
- module Concerns
450
- module Controller
451
- extend ActiveSupport::Concern
452
- include Plutonium::Portal::Controller
453
- include Plutonium::Auth::Rodauth(:admin)
454
-
455
- included do
456
- before_action :ensure_admin_access
457
- layout "admin_portal"
458
- end
459
-
460
- private
461
-
462
- def ensure_admin_access
463
- redirect_to root_path unless current_user&.admin?
464
- end
465
- end
466
- end
467
- end
468
- ```
469
-
470
- ## Authentication & Authorization
471
-
472
- ### Rodauth Setup
473
- ```bash
474
- # Install Rodauth
475
- rails generate pu:rodauth:install
476
-
477
- # Create user account
478
- rails generate pu:rodauth:account user
479
-
480
- # Create admin account with MFA
481
- rails generate pu:rodauth:account admin --no-defaults \
482
- --login --logout --remember --lockout \
483
- --create-account --verify-account --close-account \
484
- --change-password --reset-password --reset-password-notify \
485
- --active-sessions --password-grace-period --otp \
486
- --recovery-codes --audit-logging --internal-request
487
- ```
488
-
489
- ### Authentication Integration
490
- ```ruby
491
- # In portal controller concerns
492
- include Plutonium::Auth::Rodauth(:user) # For user authentication
493
- include Plutonium::Auth::Rodauth(:admin) # For admin authentication
494
- include Plutonium::Auth::Public # For public access
495
- ```
496
-
497
- ## Routing & Custom Routes
498
-
499
- ### Resource Registration
500
- ```ruby
501
- # Basic resource registration
502
- register_resource Post
503
-
504
- # Custom routes with member/collection actions
505
- register_resource Post do
506
- member do
507
- get :publish # /posts/1/publish
508
- post :archive # /posts/1/archive
509
- end
510
-
511
- collection do
512
- get :search # /posts/search
513
- end
514
- end
515
- ```
516
-
517
- ### Common Routing Errors
518
-
519
- #### "Unresolvable Association" Error
520
- When adding custom routes inside `register_resource` blocks, Plutonium may interpret them as nested resources and fail with:
521
- ```
522
- RuntimeError (Could not resolve the association between 'Model' and 'Model')
523
- ```
524
-
525
- **Problem**: Routes like this create nested resource interpretation:
526
- ```ruby
527
- register_resource ::UniversalFlow::Definition do
528
- get "editor", to: "universal_flow/definitions#editor"
529
- end
530
- ```
531
-
532
- **Solutions**:
533
-
534
- 1. **Add custom scope to model (Recommended)**:
535
- ```ruby
536
- class UniversalFlow::Definition < ApplicationRecord
537
- scope :associated_with_universal_flow_definition, ->(universal_flow_definition) {
538
- all # or specific logic for your use case
539
- }
540
- end
541
- ```
542
-
543
- 2. **Use member/collection routes**:
544
- ```ruby
545
- register_resource ::UniversalFlow::Definition do
546
- member do
547
- get :editor
548
- end
549
- end
550
- ```
551
-
552
- 3. **Create separate controller**:
553
- ```ruby
554
- # In routes file
555
- get 'universal_flow/editor/:id', to: 'universal_flow/editor#show'
556
-
557
- # Create dedicated controller
558
- class UniversalFlow::EditorController < CustomerPortal::ResourceController
559
- def show
560
- @definition = UniversalFlow::Definition.find(params[:id])
561
- authorize_current! @definition
562
- # ... custom logic
563
- end
564
- end
565
- ```