statesman_scaffold 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/llms/usage.md ADDED
@@ -0,0 +1,375 @@
1
+ # StatesmanScaffold -- Usage Patterns for LLMs
2
+
3
+ > Concrete, copy-pasteable examples covering every supported scenario.
4
+ > Optimised for LLM code generation — each section is self-contained.
5
+
6
+ ---
7
+
8
+ ## 1. Installation
9
+
10
+ ```bash
11
+ # Gemfile
12
+ gem "statesman"
13
+ gem "statesman_scaffold"
14
+
15
+ # Terminal
16
+ bundle install
17
+ rails statesman_scaffold:install # creates config/initializers/statesman_scaffold.rb
18
+ ```
19
+
20
+ The initializer auto-includes `StatesmanScaffold::Concern` into every
21
+ `ActiveRecord::Base` subclass and configures Statesman to use the ActiveRecord
22
+ adapter. No `include` is needed in individual models.
23
+
24
+ ---
25
+
26
+ ## 2. Generate state machine files
27
+
28
+ ```bash
29
+ rails "statesman_scaffold:generate[Project]"
30
+ ```
31
+
32
+ Creates three files:
33
+
34
+ ```
35
+ app/models/project/state_machine.rb # Project::StateMachine
36
+ app/models/project/transition.rb # Project::Transition
37
+ db/migrate/TIMESTAMP_create_project_transitions.rb
38
+ ```
39
+
40
+ Then run the migration:
41
+
42
+ ```bash
43
+ rails db:migrate
44
+ ```
45
+
46
+ ---
47
+
48
+ ## 3. Configure the StateMachine class
49
+
50
+ Edit the generated `app/models/project/state_machine.rb`:
51
+
52
+ ```ruby
53
+ # frozen_string_literal: true
54
+
55
+ class Project::StateMachine
56
+ include Statesman::Machine
57
+
58
+ state :active, initial: true
59
+ state :pending
60
+ state :skipped
61
+ state :cancelled
62
+ state :done
63
+
64
+ transition from: :active, to: %i[pending skipped cancelled done]
65
+ transition from: :pending, to: %i[skipped cancelled done]
66
+ transition from: :skipped, to: [:pending]
67
+
68
+ # Sync the status column after every transition
69
+ after_transition do |model, transition|
70
+ model.update!(status: transition.to_state)
71
+ end
72
+
73
+ # Optional: guard transitions
74
+ # guard_transition(to: :done) do |project|
75
+ # project.tasks_completed?
76
+ # end
77
+
78
+ # Optional: before/after hooks for specific transitions
79
+ # after_transition(to: :done) do |project, transition|
80
+ # ProjectMailer.completed(project).deliver_later
81
+ # end
82
+ end
83
+ ```
84
+
85
+ ---
86
+
87
+ ## 4. Configure the model
88
+
89
+ ```ruby
90
+ class Project < ApplicationRecord
91
+ STATUSES = Project::StateMachine.states
92
+
93
+ with_state_machine
94
+
95
+ attribute :status, :string, default: Project::StateMachine.initial_state
96
+ validates :status, inclusion: { in: STATUSES }, allow_nil: true
97
+ end
98
+ ```
99
+
100
+ The `with_state_machine` macro handles:
101
+ - `has_many :transitions` (with `dependent: :destroy`)
102
+ - `Statesman::Adapters::ActiveRecordQueries` (for `.in_state` / `.not_in_state` scopes)
103
+ - A `state_machine` instance method
104
+
105
+ ---
106
+
107
+ ## 5. Basic state machine usage
108
+
109
+ ```ruby
110
+ project = Project.create!(name: "Demo")
111
+
112
+ # Check current state
113
+ project.current_state # => "active"
114
+ project.status # => "active"
115
+
116
+ # Predicates
117
+ project.in_state?(:active) # => true
118
+ project.in_state?(:done, :active) # => true (matches any)
119
+ project.not_in_state?(:done) # => true
120
+ project.in_state?("active") # => true (strings work too)
121
+
122
+ # Check if transition is possible
123
+ project.can_transition_to?(:pending) # => true
124
+ project.can_transition_to?(:active) # => false (no self-transition)
125
+
126
+ # Transition (bang version — raises on failure)
127
+ project.transition_to!(:pending)
128
+ project.current_state # => "pending"
129
+ project.status # => "pending" (synced by after_transition)
130
+
131
+ # Transition (non-bang version — returns true/false)
132
+ project.transition_to(:done) # => true
133
+ project.transition_to(:active) # => false (not allowed)
134
+ ```
135
+
136
+ ---
137
+
138
+ ## 6. Query scopes
139
+
140
+ ```ruby
141
+ # Class-level scopes from Statesman::Adapters::ActiveRecordQueries
142
+ Project.in_state(:active)
143
+ Project.in_state(:active, :pending)
144
+ Project.not_in_state(:cancelled)
145
+
146
+ # Combine with other AR scopes
147
+ Project.where(team: "Engineering").in_state(:active)
148
+ ```
149
+
150
+ ---
151
+
152
+ ## 7. Transition history
153
+
154
+ ```ruby
155
+ project = Project.create!(name: "Demo")
156
+ project.transition_to!(:pending)
157
+ project.transition_to!(:done)
158
+
159
+ # Access transition records
160
+ project.transitions # => [#<Project::Transition ...>, ...]
161
+ project.transitions.count # => 2
162
+ project.transitions.last.to_state # => "done"
163
+
164
+ # Destroying the model destroys transitions
165
+ project.destroy! # cascades to project_transitions
166
+ ```
167
+
168
+ ---
169
+
170
+ ## 8. Namespaced models
171
+
172
+ ```bash
173
+ rails "statesman_scaffold:generate[Admin::Order]"
174
+ ```
175
+
176
+ Creates:
177
+ ```
178
+ app/models/admin/order/state_machine.rb # Admin::Order::StateMachine
179
+ app/models/admin/order/transition.rb # Admin::Order::Transition
180
+ db/migrate/TIMESTAMP_create_admin_order_transitions.rb
181
+ ```
182
+
183
+ The migration uses:
184
+ - Table: `admin_order_transitions`
185
+ - Foreign key: `{ to_table: :admin_orders }`
186
+ - `belongs_to :order` (demodulized)
187
+
188
+ Model:
189
+
190
+ ```ruby
191
+ class Admin::Order < ApplicationRecord
192
+ with_state_machine
193
+ end
194
+ ```
195
+
196
+ ---
197
+
198
+ ## 9. Handling failed transitions
199
+
200
+ ```ruby
201
+ # Bang version raises Statesman::TransitionFailedError
202
+ begin
203
+ project.transition_to!(:invalid_state)
204
+ rescue Statesman::TransitionFailedError => e
205
+ # handle error
206
+ end
207
+
208
+ # Non-bang version returns false
209
+ if project.transition_to(:pending)
210
+ # success
211
+ else
212
+ # transition not allowed
213
+ end
214
+ ```
215
+
216
+ ---
217
+
218
+ ## 10. Manual include (without the Rails initializer)
219
+
220
+ For non-standard setups or when you want explicit control:
221
+
222
+ ```ruby
223
+ class Project < ApplicationRecord
224
+ include StatesmanScaffold::Concern
225
+ with_state_machine
226
+ end
227
+ ```
228
+
229
+ You must also configure Statesman manually:
230
+
231
+ ```ruby
232
+ # config/initializers/statesman.rb
233
+ require "statesman"
234
+ Statesman.configure { storage_adapter(Statesman::Adapters::ActiveRecord) }
235
+ ```
236
+
237
+ ---
238
+
239
+ ## 11. Migration for the parent model
240
+
241
+ The generator creates the transitions table. The parent model table is your
242
+ responsibility:
243
+
244
+ ```ruby
245
+ class CreateProjects < ActiveRecord::Migration[8.0]
246
+ def change
247
+ create_table :projects do |t|
248
+ t.string :name, null: false
249
+ t.string :status, default: "active"
250
+ t.timestamps
251
+ end
252
+ end
253
+ end
254
+ ```
255
+
256
+ The `status` column is optional — it's a convenience for quick reads. The
257
+ source of truth is always the Statesman transition history.
258
+
259
+ ---
260
+
261
+ ## 12. Complete working example
262
+
263
+ ```ruby
264
+ # db/migrate/001_create_projects.rb
265
+ class CreateProjects < ActiveRecord::Migration[8.0]
266
+ def change
267
+ create_table :projects do |t|
268
+ t.string :name, null: false
269
+ t.string :status, default: "active"
270
+ t.timestamps
271
+ end
272
+ end
273
+ end
274
+
275
+ # app/models/project/state_machine.rb (generated)
276
+ class Project::StateMachine
277
+ include Statesman::Machine
278
+
279
+ state :active, initial: true
280
+ state :pending
281
+ state :done
282
+ state :cancelled
283
+
284
+ transition from: :active, to: %i[pending cancelled]
285
+ transition from: :pending, to: %i[done cancelled]
286
+
287
+ after_transition do |model, transition|
288
+ model.update!(status: transition.to_state)
289
+ end
290
+ end
291
+
292
+ # app/models/project/transition.rb (generated)
293
+ class Project::Transition < ApplicationRecord
294
+ self.table_name = "project_transitions"
295
+
296
+ belongs_to :project, class_name: "Project"
297
+
298
+ attribute :most_recent, :boolean, default: false
299
+ attribute :to_state, :string
300
+ attribute :sort_key, :integer
301
+ attribute :metadata, :json, default: {}
302
+
303
+ validates :to_state, inclusion: { in: Project::StateMachine.states }
304
+ end
305
+
306
+ # app/models/project.rb
307
+ class Project < ApplicationRecord
308
+ STATUSES = Project::StateMachine.states
309
+
310
+ with_state_machine
311
+
312
+ attribute :status, :string, default: Project::StateMachine.initial_state
313
+
314
+ validates :name, presence: true
315
+ validates :status, inclusion: { in: STATUSES }, allow_nil: true
316
+ end
317
+
318
+ # Usage
319
+ project = Project.create!(name: "Alpha")
320
+ project.current_state # => "active"
321
+ project.transition_to!(:pending)
322
+ project.status # => "pending"
323
+ Project.in_state(:pending).count # => 1
324
+ ```
325
+
326
+ ---
327
+
328
+ ## 13. Testing models with state machines
329
+
330
+ ```ruby
331
+ # Minitest
332
+ class ProjectTest < ActiveSupport::TestCase
333
+ test "starts in active state" do
334
+ project = Project.create!(name: "Test")
335
+ assert_equal "active", project.current_state
336
+ assert project.in_state?(:active)
337
+ end
338
+
339
+ test "can transition from active to pending" do
340
+ project = Project.create!(name: "Test")
341
+ assert project.can_transition_to?(:pending)
342
+ project.transition_to!(:pending)
343
+ assert_equal "pending", project.current_state
344
+ assert_equal "pending", project.status
345
+ end
346
+
347
+ test "cannot transition to invalid state" do
348
+ project = Project.create!(name: "Test")
349
+ refute project.can_transition_to?(:active)
350
+ assert_raises(Statesman::TransitionFailedError) do
351
+ project.transition_to!(:active)
352
+ end
353
+ end
354
+
355
+ test "records transitions in the database" do
356
+ project = Project.create!(name: "Test")
357
+ project.transition_to!(:pending)
358
+ assert_equal 1, project.transitions.count
359
+ assert_equal "pending", project.transitions.last.to_state
360
+ end
361
+ end
362
+ ```
363
+
364
+ ---
365
+
366
+ ## 14. Error reference
367
+
368
+ | Error | Cause | Fix |
369
+ |-------|-------|-----|
370
+ | `NameError: uninitialized constant Statesman` | Initializer runs before `statesman` gem is loaded | Re-run `rails statesman_scaffold:install` (includes `require "statesman"`) |
371
+ | `NameError: uninitialized constant Model::StateMachine` | StateMachine class not generated or not autoloaded | Run `rails "statesman_scaffold:generate[Model]"` |
372
+ | `Statesman::TransitionFailedError` | Transition not defined for current state | Check `transition from:` rules in StateMachine |
373
+ | `ActiveRecord::StatementInvalid` (missing table) | Migration not run | Run `rails db:migrate` |
374
+ | Status column out of sync with `current_state` | No `after_transition` callback | Add `after_transition { \|m, t\| m.update!(status: t.to_state) }` |
375
+ | `Statesman::TransitionConflictError` | Concurrent transition race condition | Retry or use database locks |
metadata ADDED
@@ -0,0 +1,145 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: statesman_scaffold
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Pawel Niemczyk
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activerecord
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '9'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '7.0'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '9'
32
+ - !ruby/object:Gem::Dependency
33
+ name: activesupport
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '7.0'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '9'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '7.0'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '9'
52
+ - !ruby/object:Gem::Dependency
53
+ name: railties
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '7.0'
59
+ - - "<"
60
+ - !ruby/object:Gem::Version
61
+ version: '9'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '7.0'
69
+ - - "<"
70
+ - !ruby/object:Gem::Version
71
+ version: '9'
72
+ - !ruby/object:Gem::Dependency
73
+ name: statesman
74
+ requirement: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '10.0'
79
+ - - "<"
80
+ - !ruby/object:Gem::Version
81
+ version: '13'
82
+ type: :runtime
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '10.0'
89
+ - - "<"
90
+ - !ruby/object:Gem::Version
91
+ version: '13'
92
+ description: |
93
+ StatesmanScaffold generates StateMachine, Transition classes, and migrations for
94
+ ActiveRecord models using the Statesman gem. Includes a WithStateMachine concern
95
+ that wires up state machine delegation, query scopes, and transition associations
96
+ with a single `with_state_machine` class macro.
97
+ email:
98
+ - pniemczyk.info@gmail.com
99
+ executables: []
100
+ extensions: []
101
+ extra_rdoc_files: []
102
+ files:
103
+ - AGENTS.md
104
+ - CHANGELOG.md
105
+ - CLAUDE.md
106
+ - LICENSE.txt
107
+ - README.md
108
+ - lib/statesman_scaffold.rb
109
+ - lib/statesman_scaffold/concern.rb
110
+ - lib/statesman_scaffold/generator.rb
111
+ - lib/statesman_scaffold/installer.rb
112
+ - lib/statesman_scaffold/railtie.rb
113
+ - lib/statesman_scaffold/version.rb
114
+ - lib/tasks/statesman_scaffold.rake
115
+ - lib/templates/statesman/migration.rb.erb
116
+ - lib/templates/statesman/state_machine.rb.erb
117
+ - lib/templates/statesman/transition.rb.erb
118
+ - llms/overview.md
119
+ - llms/usage.md
120
+ homepage: https://github.com/pniemczyk/statesman_scaffold
121
+ licenses:
122
+ - MIT
123
+ metadata:
124
+ homepage_uri: https://github.com/pniemczyk/statesman_scaffold
125
+ source_code_uri: https://github.com/pniemczyk/statesman_scaffold
126
+ changelog_uri: https://github.com/pniemczyk/statesman_scaffold/blob/main/CHANGELOG.md
127
+ rubygems_mfa_required: 'true'
128
+ rdoc_options: []
129
+ require_paths:
130
+ - lib
131
+ required_ruby_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: 3.2.0
136
+ required_rubygems_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ requirements: []
142
+ rubygems_version: 3.6.9
143
+ specification_version: 4
144
+ summary: Scaffold Statesman state machines for ActiveRecord models
145
+ test_files: []