Skip to content

Commit 57aecec

Browse files
committed
Finalize docs.
1 parent 08689ba commit 57aecec

2 files changed

Lines changed: 73 additions & 50 deletions

File tree

AI_README.md

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,21 @@ Subclass `Operational::Operation`. Define steps with `step`, `pass`, or `fail` a
1717

1818
```ruby
1919
class CreateArticleOperation < Operational::Operation
20-
step :build
21-
step Contract::Build(contract: ArticleForm, model_key: :article)
20+
step :init
21+
step Contract::Build(contract: ArticleForm)
2222
step Contract::Validate()
23-
step Contract::Sync(model_key: :article)
23+
step Contract::Sync()
2424
step :save
2525
pass :notify # return value ignored, never derails
2626
fail :handle # only runs on failure track
2727

28-
def build(state)
29-
state[:article] = Article.new
28+
def init(state)
29+
state[:model] = Article.new
3030
# must return truthy to continue, falsy switches to failure track
3131
end
3232

3333
def save(state)
34-
state[:article].save # returns true/false naturally
34+
state[:model].save # returns true/false naturally
3535
end
3636

3737
def notify(state)
@@ -88,20 +88,20 @@ Use `Nested::Operation` to call one operation from within another. State is merg
8888
class CreateArticleOperation < Operational::Operation
8989
class Present < Operational::Operation
9090
step :init
91-
step Contract::Build(contract: ArticleForm, model_key: :article)
91+
step Contract::Build(contract: ArticleForm)
9292

9393
def init(state)
94-
state[:article] = Article.new
94+
state[:model] = Article.new
9595
end
9696
end
9797

9898
step Nested::Operation(operation: Present)
9999
step Contract::Validate()
100-
step Contract::Sync(model_key: :article)
100+
step Contract::Sync()
101101
pass :persist
102102

103103
def persist(state)
104-
state[:article].save!
104+
state[:model].save!
105105
end
106106
end
107107
```
@@ -180,7 +180,7 @@ These are used inside operations as step actions. They return lambdas.
180180
step Contract::Build(
181181
contract: MyForm, # required — the form class
182182
name: :contract, # state key to store the form instance
183-
model_key: nil, # state key containing the model to build from
183+
model_key: :model, # state key containing the model to build from (used only if present in state)
184184
model_persisted: nil, # override persisted? detection
185185
build_method: :on_build
186186
)
@@ -206,7 +206,7 @@ Returns the result of `form.validate(params)` — `true`/`false`.
206206
```ruby
207207
step Contract::Sync(
208208
name: :contract, # state key where the form is stored
209-
model_key: nil, # state key containing the model to sync to
209+
model_key: :model, # state key containing the model to sync to
210210
sync_method: :on_sync # custom sync hook method name
211211
)
212212
```
@@ -221,7 +221,7 @@ class MyController < ApplicationController
221221

222222
def create
223223
if run CreateArticleOperation
224-
redirect_to @state[:article]
224+
redirect_to @state[:model]
225225
else
226226
render :new, status: :unprocessable_entity
227227
end
@@ -267,26 +267,34 @@ Example: `app/concepts/article/article_form.rb`, `app/concepts/article/create_ar
267267
class CreateThingOperation < Operational::Operation
268268
class Present < Operational::Operation
269269
step :init
270-
step Contract::Build(contract: ThingForm, model_key: :thing)
270+
step Contract::Build(contract: ThingForm)
271271

272272
def init(state)
273-
state[:thing] = Thing.new
273+
state[:model] = Thing.new
274274
end
275275
end
276276

277277
step Nested::Operation(operation: Present)
278278
step Contract::Validate()
279-
step Contract::Sync(model_key: :thing)
279+
step Contract::Sync()
280280
pass :persist
281281

282282
def persist(state)
283-
state[:thing].save!
283+
state[:model].save!
284284
end
285285
end
286286
```
287287

288288
Controller uses `CreateThingOperation::Present` for `new` and `CreateThingOperation` for `create`.
289289

290+
To use a descriptive state key instead of `:model`, pass `model_key:` explicitly:
291+
292+
```ruby
293+
step Contract::Build(contract: ThingForm, model_key: :thing)
294+
step Contract::Sync(model_key: :thing)
295+
# state[:thing] instead of state[:model]
296+
```
297+
290298
### Multi-model form
291299

292300
```ruby

README.md

Lines changed: 48 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -50,31 +50,31 @@ end
5050

5151
# An operation — wires together validation, persistence, and business process with railway functional programming.
5252
class RegisterUserOperation < Operational::Operation
53-
step :setup_user
54-
step Contract::Build(contract: SignupForm, model_key: :user)
53+
step :setup
54+
step Contract::Build(contract: SignupForm)
5555
step Contract::Validate()
56-
step Contract::Sync(model_key: :user)
56+
step Contract::Sync()
5757
step :persist
5858
pass :send_welcome
5959

60-
def setup_user(state)
61-
state[:user] = User.new(role: :member)
60+
def setup(state)
61+
state[:model] = User.new(role: :member)
6262
end
6363

6464
def persist(state)
65-
state[:user].save
65+
state[:model].save
6666
end
6767

6868
def send_welcome(state)
69-
WelcomeMailer.welcome(state[:user]).deliver_later
69+
WelcomeMailer.welcome(state[:model]).deliver_later
7070
end
7171
end
7272
```
7373

7474
```ruby
7575
# In your controller — simple boolean branching.
7676
if run RegisterUserOperation
77-
redirect_to dashboard_path, notice: "Welcome #{@state[:user].name}!"
77+
redirect_to dashboard_path, notice: "Welcome #{@state[:model].name}!"
7878
else
7979
render :new, status: :unprocessable_entity
8080
end
@@ -141,7 +141,7 @@ result[:order] # => shorthand for result.state[:order]
141141
result.operation # => the operation instance
142142
```
143143

144-
There is intentionally one entry point (`.call`) and one result type — no `.call!` or bang variants. Check `succeeded?` and branch accordingly.
144+
There is intentionally one entry point (`.call`) and one result type. Check `succeeded?` and branch accordingly.
145145

146146
#### The Railway: step, pass, fail
147147

@@ -179,7 +179,7 @@ Every operation revolves around a single **state hash**. It's created when you c
179179

180180
```ruby
181181
result = ChargeOrderOperation.call(params: { id: 1 }, current_user: admin)
182-
# └──────────── initial state ───────────┘
182+
# └──────────── initial state ───────────┘
183183

184184
# Each step receives and mutates the same hash:
185185
# step :find_order → state[:order] = Order.find_by(...)
@@ -229,7 +229,7 @@ end
229229

230230
### Forms
231231

232-
Forms decouple input validation from your models. They allow you to build UI and APIs that aren't coupled to your database modeling and allow you define exactly what parameters you'll accept in a declarative way.
232+
Forms decouple input validation from your models. They allow you to build UI and APIs that aren't coupled to your database modeling and allow you to define exactly what parameters you'll accept in a declarative way.
233233

234234
They're built on `ActiveModel::Model`, `ActiveModel::Attributes`, and `ActiveModel::Dirty` — so you already know the API.
235235

@@ -270,7 +270,7 @@ form.sync(model: article)
270270
article.title # => "Updated"
271271
```
272272

273-
Any params that don't match a defined form attribute are silently ignored — no need for `strong_parameters`.
273+
Any params that don't match a defined form attribute are ignored — no need for `strong_parameters`, your form defines what parameters you will accept.
274274

275275
You can also pass **state** to `.build`, which is separate from the form's attributes — it's not user input, it's context. State is available as `@state` and is useful for conditional validation (e.g., only admins can publish) and prepopulating defaults from things the user doesn't control:
276276

@@ -304,7 +304,7 @@ class NewArticleForm < Operational::Form
304304
end
305305
end
306306

307-
# Build pulls from article (automatic) + current_user + team (via on_build)
307+
# Build pulls from article (automatic) + current_user/team (via on_build)
308308
form = NewArticleForm.build(model: article, state: { current_user: user, team: team, author: user })
309309

310310
# Sync writes to article (automatic) + author (via on_sync)
@@ -349,17 +349,17 @@ Contract helpers wire forms into operations as steps. This is where Operations a
349349
Creates a form instance and stores it in the state:
350350

351351
```ruby
352+
# Simple — builds the form and pre-populates from state[:model]
352353
step Contract::Build(contract: ArticleForm)
353-
# state[:contract] is now an ArticleForm instance
354354

355-
# With a model for pre-population:
355+
# With a custom model key — pre-populates from state[:article] instead
356356
step Contract::Build(contract: ArticleForm, model_key: :article)
357357
```
358358

359359
Options:
360360
- `contract:` — the form class (required)
361361
- `name:` — state key to store the form (default: `:contract`)
362-
- `model_key:` — state key containing the model to pre-populate from
362+
- `model_key:` — state key containing the model to build from (default: `:model`)
363363
- `model_persisted:` — override `persisted?` detection
364364
- `build_method:` — method to call during build (default: `:on_build`)
365365

@@ -368,16 +368,14 @@ Options:
368368
Validates the form using params from the state:
369369

370370
```ruby
371+
# Simple — validates state[:contract] with state[:params]
371372
step Contract::Validate()
372-
# Validates state[:contract] with state[:params]
373373

374-
# With nested params:
374+
# With nested params — validates with state[:params][:article]
375375
step Contract::Validate(params_path: :article)
376-
# Validates with state[:params][:article]
377376

378-
# With a custom path:
377+
# With a custom path — validates with state.dig(:custom, :path)
379378
step Contract::Validate(params_path: [:custom, :path])
380-
# Validates with state[:custom][:path]
381379
```
382380

383381
Options:
@@ -391,12 +389,16 @@ Returns `true` if validation passes, `false` otherwise — making it a natural r
391389
Syncs form data back to a model:
392390

393391
```ruby
392+
# Simple — syncs form attributes back to state[:model]
393+
step Contract::Sync()
394+
395+
# With a custom model key — syncs back to state[:article] instead
394396
step Contract::Sync(model_key: :article)
395397
```
396398

397399
Options:
398400
- `name:` — state key where the form is stored (default: `:contract`)
399-
- `model_key:` — state key containing the model to sync to
401+
- `model_key:` — state key containing the model to sync to (default: `:model`)
400402
- `sync_method:` — custom sync hook method name (default: `:on_sync`)
401403

402404
#### Putting It Together
@@ -413,25 +415,38 @@ end
413415

414416
# app/concepts/article/create_article_operation.rb
415417
class CreateArticleOperation < Operational::Operation
416-
step :build_article
417-
step Contract::Build(contract: ArticleForm, model_key: :article)
418+
step :init
419+
step Contract::Build(contract: ArticleForm)
418420
step Contract::Validate()
419-
step Contract::Sync(model_key: :article)
421+
step Contract::Sync()
420422
step :save
421423

422-
def build_article(state)
423-
state[:article] = Article.new
424+
def init(state)
425+
state[:model] = Article.new
424426
end
425427

426428
def save(state)
427-
state[:article].save
429+
state[:model].save
428430
end
429431
end
430432

431-
# Usage
433+
# Direct usage
432434
result = CreateArticleOperation.call(params: { title: "Hello", body: "World" })
433435
result.succeeded? # => true
434-
result[:article] # => #<Article id: 1, title: "Hello", ...>
436+
result[:model] # => #<Article id: 1, title: "Hello", ...>
437+
438+
# From a controller
439+
class ArticlesController < ApplicationController
440+
include Operational::Controller
441+
442+
def create
443+
if run CreateArticleOperation
444+
redirect_to @state[:model], notice: "Article created!"
445+
else
446+
render :new, status: :unprocessable_entity
447+
end
448+
end
449+
end
435450
```
436451

437452
### Composing Operations
@@ -442,10 +457,10 @@ Just like Rails controllers pair `new`/`create` and `edit`/`update`, operations
442457
class CreateArticleOperation < Operational::Operation
443458
# The "new" part — builds the model and sets up the form
444459
class Present < Operational::Operation
445-
step :build_article
460+
step :init
446461
step Contract::Build(contract: ArticleForm, model_key: :article)
447462

448-
def build_article(state)
463+
def init(state)
449464
state[:article] = Article.new(author: state[:current_user])
450465
end
451466
end
@@ -589,7 +604,7 @@ The `new` action runs just `Present` to build an empty form. The `create` action
589604

590605
## Testing
591606

592-
Testing Operations and Forms is straight forward. Operations and Forms are just plain ruby objects that can be easily tested as unit tests.
607+
Testing Operations and Forms is straightforward. They are plain Ruby objects that can be tested as unit tests — no controller or request specs needed.
593608

594609
### Testing Operations
595610

0 commit comments

Comments
 (0)