operational 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/README.md ADDED
@@ -0,0 +1,668 @@
1
+ <h1 align="center">
2
+ <img src="logo.png" alt="" width="60" valign="middle">
3
+ Operational
4
+ </h1>
5
+
6
+ <p align="center">
7
+ <strong>Lightweight, railway-oriented operation and form objects for business logic.</strong>
8
+ </p>
9
+
10
+ Operational wraps your business logic into **Operations** — small classes with a railway of steps that succeed or fail. Pair them with **Forms** to decouple your UI and APIs from your models and **Contracts** to wire it all together.
11
+
12
+ One dependency: `activemodel`. ~200 lines of plain ruby code. It's not a framework — it's a pattern. You probably already know how Operational works.
13
+
14
+ > [!NOTE]
15
+ > **AI agents:** See [AI_README.md](AI_README.md) for a concise API reference optimized for code generation.
16
+
17
+ [![Gem Version](https://img.shields.io/gem/v/operational.svg)](https://rubygems.org/gems/operational)
18
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
19
+
20
+
21
+ ## Table of Contents
22
+
23
+ - [Quick Example](#quick-example)
24
+ - [Installation](#installation)
25
+ - [Why You Need Operational](#why-you-need-operational)
26
+ - [Core Concepts](#core-concepts)
27
+ - [Operations](#operations)
28
+ - [Forms](#forms)
29
+ - [Contracts](#contracts)
30
+ - [Composing Operations](#composing-operations)
31
+ - [Rails Integration](#rails-integration)
32
+ - [Project Structure](#project-structure)
33
+ - [Full Example](#full-example)
34
+ - [Testing](#testing)
35
+ - [Requirements](#requirements)
36
+ - [Contributing](#contributing)
37
+ - [License](#license)
38
+
39
+ ## Quick Example
40
+
41
+ ```ruby
42
+ # A form object — validates input without being linked to a specific model.
43
+ class SignupForm < Operational::Form
44
+ attribute :name, :string
45
+ attribute :email, :string
46
+
47
+ validates :name, presence: true
48
+ validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
49
+ end
50
+
51
+ # An operation — wires together validation, persistence, and business process with railway functional programming.
52
+ class RegisterUserOperation < Operational::Operation
53
+ step :setup
54
+ step Contract::Build(contract: SignupForm)
55
+ step Contract::Validate()
56
+ step Contract::Sync()
57
+ step :persist
58
+ pass :send_welcome
59
+
60
+ def setup(state)
61
+ state[:model] = User.new(role: :member)
62
+ end
63
+
64
+ def persist(state)
65
+ state[:model].save
66
+ end
67
+
68
+ def send_welcome(state)
69
+ WelcomeMailer.welcome(state[:model]).deliver_later
70
+ end
71
+ end
72
+ ```
73
+
74
+ ```ruby
75
+ # In your controller — simple boolean branching.
76
+ if run RegisterUserOperation
77
+ redirect_to dashboard_path, notice: "Welcome #{@state[:model].name}!"
78
+ else
79
+ render :new, status: :unprocessable_entity
80
+ end
81
+ ```
82
+
83
+ ## Installation
84
+
85
+ Add to your Gemfile:
86
+
87
+ ```ruby
88
+ gem 'operational'
89
+ ```
90
+
91
+ Then run `bundle install`.
92
+
93
+ ## Why You Need Operational
94
+
95
+ Rails apps start simple — a model, a controller, some validations. Then the business logic creeps in. "Register a user" isn't just `User.create` anymore — it's validate the input, assign a role, send a welcome email, and notify the sales team. That logic ends up scattered across callbacks, controller actions, and service objects that everyone has to remember to call in the right order.
96
+
97
+ Operational gives you a place for all of that. Each operation describes a business process as a readable sequence of steps that anyone on the team can follow — no digging through models and callbacks to understand what happens.
98
+
99
+ **Operational can help when:**
100
+
101
+ - UI and API requests are touching multiple models (`accepts_nested_attributes_for`)
102
+ - Model validations need to change by outside context (e.g., only admins can publish)
103
+ - Model callbacks are doing too much (`after_create`, `after_save`, etc.)
104
+ - Business processes are duplicated between controllers, jobs, and scripts
105
+ - Strong parameters are getting complex with deeply nested or context-dependent permits
106
+ - Testing business logic requires full controller/request specs instead of simple unit tests
107
+
108
+ ## Core Concepts
109
+
110
+ ### Operations
111
+
112
+ An operation is a class that defines a sequence of steps executed in order. Each step either succeeds (returns truthy) or fails (returns falsy), controlling the flow through the railway.
113
+
114
+ **Operations orchestrate, they don't implement.** Keep your steps thin — they should try to delegate to plain Ruby objects, service classes, and model methods. An operation's job is to define the order things happen and what to do when something fails, in other words, _**orchestrate**_ the business process but don't contain the business logic itself. If a step is getting long, extract the work into a ruby service object and call it from the step.
115
+
116
+ #### Defining Steps
117
+
118
+ Steps can be **symbols** (instance methods), **lambdas**, or any **callable** object:
119
+
120
+ ```ruby
121
+ class ProcessOrderOperation < Operational::Operation
122
+ step :validate_inventory # instance method
123
+ step ->(state) { ... } # lambda
124
+ step Policies::OrderPolicy() # callable object
125
+ end
126
+ ```
127
+
128
+ Every step receives a `state` hash and returns a truthy or falsy value.
129
+
130
+ #### Running an Operation
131
+
132
+ Call `.call` on the operation with an optional initial state hash. You get back a `Result`:
133
+
134
+ ```ruby
135
+ result = ProcessOrderOperation.call(order: order, current_user: user)
136
+
137
+ result.succeeded? # => true / false
138
+ result.failed? # => true / false
139
+ result.state # => the full state hash (frozen)
140
+ result[:order] # => shorthand for result.state[:order]
141
+ result.operation # => the operation instance
142
+ ```
143
+
144
+ There is intentionally one entry point (`.call`) and one result type. Check `succeeded?` and branch accordingly.
145
+
146
+ #### The Railway: step, pass, fail
147
+
148
+ Operations follow a **railway pattern** with two tracks — success and failure:
149
+
150
+ - **`step`** — Runs on the success track. If it returns falsy, execution switches to the failure track.
151
+ - **`fail`** — Runs on the failure track only. If it returns truthy, execution switches back to the success track (recovery).
152
+ - **`pass`** — Always runs on the success track and always continues on the success track, regardless of return value. Useful for side effects.
153
+
154
+ ```ruby
155
+ class PlaceOrderOperation < Operational::Operation
156
+ step :validate_cart # success track — runs first
157
+ step :charge_card # if this returns false → switches to failure track
158
+ step :send_confirmation # SKIPPED if charge_card failed
159
+ fail :notify_support # failure track — only runs after a failure
160
+ fail :refund # continues on failure track
161
+ end
162
+ ```
163
+
164
+ **Recovery:** If a `fail` step returns truthy, execution moves back to the success track. This lets you handle errors and continue.
165
+
166
+ **`pass` for side effects:** A `pass` step always continues on the success track regardless of its return value — useful for logging, analytics, or other fire-and-forget work:
167
+
168
+ ```ruby
169
+ class PublishArticleOperation < Operational::Operation
170
+ step :publish
171
+ pass :track_analytics # return value ignored — never derails the operation
172
+ step :notify_subscribers # always runs after pass
173
+ end
174
+ ```
175
+
176
+ #### State
177
+
178
+ Every operation revolves around a single **state hash**. It's created when you call the operation, passed to every step, and returned in the result. Steps read from it, write to it, and use it to pass data to each other — similar to how Unix pipes pass data through a chain of commands:
179
+
180
+ ```ruby
181
+ result = ChargeOrderOperation.call(params: { id: 1 }, current_user: admin)
182
+ # └──────────── initial state ───────────┘
183
+
184
+ # Each step receives and mutates the same hash:
185
+ # step :find_order → state[:order] = Order.find_by(...)
186
+ # step :charge_payment → state[:charge] = PaymentGateway.charge(...)
187
+
188
+ result.state # => frozen snapshot of the final state
189
+ result[:order] # => the order that was charged
190
+ ```
191
+
192
+ This single shared hash means steps are fully decoupled — they don't know about each other, they just read and write to state. You can reorder, add, or remove steps without changing method signatures. And because state is frozen after the operation completes, the result is an immutable snapshot of everything that happened.
193
+
194
+ #### A Realistic Example
195
+
196
+ ```ruby
197
+ class ChargeOrderOperation < Operational::Operation
198
+ step :find_order
199
+ step :charge_payment
200
+ pass :track_analytics
201
+ pass :send_confirmation
202
+ fail :refund
203
+
204
+ def find_order(state)
205
+ state[:order] = Order.find_by(id: state[:params][:id])
206
+ state[:order].present?
207
+ end
208
+
209
+ def charge_payment(state)
210
+ state[:charge] = PaymentGateway.charge(state[:order].total)
211
+ state[:charge].success?
212
+ end
213
+
214
+ def track_analytics(state)
215
+ Analytics.track("order.charged", order_id: state[:order].id)
216
+ # return value doesn't matter — pass always continues
217
+ end
218
+
219
+ def send_confirmation(state)
220
+ OrderMailer.confirmation(state[:order]).deliver_later
221
+ end
222
+
223
+ def refund(state)
224
+ PaymentGateway.refund(state[:charge]) if state[:charge]
225
+ false
226
+ end
227
+ end
228
+ ```
229
+
230
+ ### Forms
231
+
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.
233
+
234
+ They're built on `ActiveModel::Model`, `ActiveModel::Attributes`, and `ActiveModel::Dirty` — so you already know the API.
235
+
236
+ > [!TIP]
237
+ > Already familiar with form objects? Skip ahead to [Contracts](#contracts) to see how forms wire into operations.
238
+
239
+ #### Defining a Form
240
+
241
+ ```ruby
242
+ class ArticleForm < Operational::Form
243
+ attribute :title, :string
244
+ attribute :body, :string
245
+ attribute :published, :boolean, default: false
246
+
247
+ validates :title, presence: true, length: { maximum: 200 }
248
+ validates :body, presence: true
249
+ end
250
+ ```
251
+
252
+ #### Building, Validating, and Syncing
253
+
254
+ The basic lifecycle of a form is **build → validate → sync**. For single-model forms, this is straightforward — pass a model to `.build` and attributes defined in your form matching attributes in the model are automatically copied in both directions:
255
+
256
+ ```ruby
257
+ # Build — pre-populates form from the model's matching attributes
258
+ article = Article.find(params[:id])
259
+ form = ArticleForm.build(model: article)
260
+ form.title # => article.title (auto-copied)
261
+ form.persisted? # => true (detected from model)
262
+
263
+ # Validate — assigns params, runs validations, returns true/false
264
+ form.validate(title: "Updated", body: "New content") # => true
265
+ form.validate(title: "") # => false
266
+ form.errors.full_messages # => ["Title can't be blank"]
267
+
268
+ # Sync — writes matching attributes back to the model
269
+ form.sync(model: article)
270
+ article.title # => "Updated"
271
+ ```
272
+
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.
274
+
275
+ 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:
276
+
277
+ ```ruby
278
+ form = ArticleForm.build(model: article, state: { current_user: current_user, team: team })
279
+ ```
280
+
281
+ > [!NOTE]
282
+ > Inside an operation, [`Contract` helpers](#contracts) handle this entire lifecycle as steps — you won't call these methods directly, and state is passed automatically.
283
+
284
+ #### Multi-Model Forms: on_build and on_sync Hooks
285
+
286
+ For simple single-model forms, the automatic attribute matching handles everything. For more complex cases — where a single form spans multiple models — you can define `on_build` and `on_sync` hooks to control how data flows in and out:
287
+
288
+ ```ruby
289
+ class NewArticleForm < Operational::Form
290
+ attribute :title, :string
291
+ attribute :body, :string
292
+ attribute :author_bio, :string
293
+ attribute :default_category, :string
294
+
295
+ # Pull data IN from multiple sources when the form is built
296
+ def on_build(state)
297
+ self.author_bio = state[:current_user]&.bio
298
+ self.default_category = state[:team]&.default_category
299
+ end
300
+
301
+ # Push data OUT to multiple models when the form is synced
302
+ def on_sync(state)
303
+ state[:author].update!(bio: author_bio) if author_bio_changed?
304
+ end
305
+ end
306
+
307
+ # Build pulls from article (automatic) + current_user/team (via on_build)
308
+ form = NewArticleForm.build(model: article, state: { current_user: user, team: team, author: user })
309
+
310
+ # Sync writes to article (automatic) + author (via on_sync)
311
+ form.sync(model: article, state: { article: article, author: user })
312
+ ```
313
+
314
+ #### Dirty Tracking
315
+
316
+ Forms support ActiveModel dirty tracking out of the box:
317
+
318
+ ```ruby
319
+ form = ArticleForm.build(model: article)
320
+ form.changed? # => false (clean after build)
321
+
322
+ form.title = "New"
323
+ form.changed? # => true
324
+ form.title_changed? # => true
325
+ form.title_was # => "Original Title"
326
+ ```
327
+
328
+ #### State-Dependent Validators
329
+
330
+ Access operation state inside custom validators via `@state`:
331
+
332
+ ```ruby
333
+ class ArticleForm < Operational::Form
334
+ attribute :title, :string
335
+ validate :must_be_admin
336
+
337
+ def must_be_admin
338
+ errors.add(:base, "Not authorized") unless @state[:current_user]&.admin?
339
+ end
340
+ end
341
+ ```
342
+
343
+ ### Contracts
344
+
345
+ Contract helpers wire forms into operations as steps. This is where Operations and Forms come together.
346
+
347
+ #### Contract.Build
348
+
349
+ Creates a form instance and stores it in the state:
350
+
351
+ ```ruby
352
+ # Simple — builds the form and pre-populates from state[:model]
353
+ step Contract::Build(contract: ArticleForm)
354
+
355
+ # With a custom model key — pre-populates from state[:article] instead
356
+ step Contract::Build(contract: ArticleForm, model_key: :article)
357
+ ```
358
+
359
+ Options:
360
+ - `contract:` — the form class (required)
361
+ - `name:` — state key to store the form (default: `:contract`)
362
+ - `model_key:` — state key containing the model to build from (default: `:model`)
363
+ - `model_persisted:` — override `persisted?` detection
364
+ - `build_method:` — method to call during build (default: `:on_build`)
365
+
366
+ #### Contract.Validate
367
+
368
+ Validates the form using params from the state:
369
+
370
+ ```ruby
371
+ # Simple — validates state[:contract] with state[:params]
372
+ step Contract::Validate()
373
+
374
+ # With nested params — validates with state[:params][:article]
375
+ step Contract::Validate(params_path: :article)
376
+
377
+ # With a custom path — validates with state.dig(:custom, :path)
378
+ step Contract::Validate(params_path: [:custom, :path])
379
+ ```
380
+
381
+ Options:
382
+ - `name:` — state key where the form is stored (default: `:contract`)
383
+ - `params_path:` — `nil` for `state[:params]`, a symbol for `state[:params][symbol]`, or an array for a custom dig path
384
+
385
+ Returns `true` if validation passes, `false` otherwise — making it a natural railway step.
386
+
387
+ #### Contract.Sync
388
+
389
+ Syncs form data back to a model:
390
+
391
+ ```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
396
+ step Contract::Sync(model_key: :article)
397
+ ```
398
+
399
+ Options:
400
+ - `name:` — state key where the form is stored (default: `:contract`)
401
+ - `model_key:` — state key containing the model to sync to (default: `:model`)
402
+ - `sync_method:` — custom sync hook method name (default: `:on_sync`)
403
+
404
+ #### Putting It Together
405
+
406
+ ```ruby
407
+ # app/concepts/article/article_form.rb
408
+ class ArticleForm < Operational::Form
409
+ attribute :title, :string
410
+ attribute :body, :string
411
+
412
+ validates :title, presence: true
413
+ validates :body, presence: true
414
+ end
415
+
416
+ # app/concepts/article/create_article_operation.rb
417
+ class CreateArticleOperation < Operational::Operation
418
+ step :init
419
+ step Contract::Build(contract: ArticleForm)
420
+ step Contract::Validate()
421
+ step Contract::Sync()
422
+ step :save
423
+
424
+ def init(state)
425
+ state[:model] = Article.new
426
+ end
427
+
428
+ def save(state)
429
+ state[:model].save
430
+ end
431
+ end
432
+
433
+ # Direct usage
434
+ result = CreateArticleOperation.call(params: { title: "Hello", body: "World" })
435
+ result.succeeded? # => true
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
450
+ ```
451
+
452
+ ### Composing Operations
453
+
454
+ Just like Rails controllers pair `new`/`create` and `edit`/`update`, operations often share setup logic between actions. `Nested::Operation` lets you extract the common part — building the model, setting up the form — into a reusable operation that gets nested inside the action-specific ones:
455
+
456
+ ```ruby
457
+ class CreateArticleOperation < Operational::Operation
458
+ # The "new" part — builds the model and sets up the form
459
+ class Present < Operational::Operation
460
+ step :init
461
+ step Contract::Build(contract: ArticleForm, model_key: :article)
462
+
463
+ def init(state)
464
+ state[:article] = Article.new(author: state[:current_user])
465
+ end
466
+ end
467
+
468
+ # The "create" part — nests Present, then validates, syncs, and persists
469
+ step Nested::Operation(operation: Present)
470
+ step Contract::Validate()
471
+ step Contract::Sync(model_key: :article)
472
+ pass :persist
473
+
474
+ def persist(state)
475
+ ActiveRecord::Base.transaction do
476
+ state[:article].save!
477
+ end
478
+ end
479
+ end
480
+ ```
481
+
482
+ Use `CreateArticleOperation::Present` for the `new` action and `CreateArticleOperation` for `create` — no need to duplicate setup or extract controller helpers.
483
+
484
+ ## Rails Integration
485
+
486
+ Include `Operational::Controller` in your controllers to get the `run` helper:
487
+
488
+ ```ruby
489
+ class ArticlesController < ApplicationController
490
+ include Operational::Controller
491
+
492
+ def create
493
+ if run CreateArticleOperation
494
+ redirect_to @state[:article], notice: "Article created!"
495
+ else
496
+ render :new, status: :unprocessable_entity
497
+ end
498
+ end
499
+ end
500
+ ```
501
+
502
+ `run` automatically injects `params` and `current_user` (if available) into the operation state, and exposes the result state as `@state`.
503
+
504
+ You can pass additional state:
505
+
506
+ ```ruby
507
+ run CreateArticleOperation, publish: true, category: @category
508
+ ```
509
+
510
+ Override `_operational_default_state` to customize what gets injected:
511
+
512
+ ```ruby
513
+ class ApplicationController < ActionController::Base
514
+ include Operational::Controller
515
+
516
+ protected
517
+
518
+ def _operational_default_state
519
+ super.merge(admin: current_user&.admin?)
520
+ end
521
+ end
522
+ ```
523
+
524
+ ## Project Structure
525
+
526
+ We recommend organizing operations and forms under `app/concepts/`, grouped by the domain concept they belong to:
527
+
528
+ ```
529
+ app/
530
+ concepts/
531
+ article/
532
+ article_form.rb
533
+ create_article_operation.rb
534
+ publish_article_operation.rb
535
+ registration/
536
+ signup_form.rb
537
+ register_user_operation.rb
538
+ controllers/
539
+ articles_controller.rb
540
+ registrations_controller.rb
541
+ models/
542
+ article.rb
543
+ user.rb
544
+ ```
545
+
546
+ This keeps related operations and forms together — everything about articles lives in `app/concepts/article/`. Rails autoloading picks them up automatically — no configuration needed.
547
+
548
+ ## Full Example
549
+
550
+ Here's a complete `new`/`create` flow — form, operation, and controller working together:
551
+
552
+ ```ruby
553
+ # app/concepts/article/article_form.rb
554
+ class ArticleForm < Operational::Form
555
+ attribute :title, :string
556
+ attribute :body, :string
557
+
558
+ validates :title, presence: true, length: { maximum: 200 }
559
+ validates :body, presence: true
560
+ end
561
+
562
+ # app/concepts/article/create_article_operation.rb
563
+ class CreateArticleOperation < Operational::Operation
564
+ # The "new" part — reusable for the new action
565
+ class Present < Operational::Operation
566
+ step :init
567
+ step Contract::Build(contract: ArticleForm, model_key: :article)
568
+
569
+ def init(state)
570
+ state[:article] = Article.new(author: state[:current_user])
571
+ end
572
+ end
573
+
574
+ # The "create" part
575
+ step Nested::Operation(operation: Present)
576
+ step Contract::Validate()
577
+ step Contract::Sync(model_key: :article)
578
+ pass :persist
579
+
580
+ def persist(state)
581
+ state[:article].save!
582
+ end
583
+ end
584
+
585
+ # app/controllers/articles_controller.rb
586
+ class ArticlesController < ApplicationController
587
+ include Operational::Controller
588
+
589
+ def new
590
+ run CreateArticleOperation::Present
591
+ end
592
+
593
+ def create
594
+ if run CreateArticleOperation
595
+ redirect_to @state[:article], notice: "Article created!"
596
+ else
597
+ render :new, status: :unprocessable_entity
598
+ end
599
+ end
600
+ end
601
+ ```
602
+
603
+ The `new` action runs just `Present` to build an empty form. The `create` action nests `Present` then adds validation, syncing, and persistence. The controller only handles HTTP routing — all business logic lives in the operation.
604
+
605
+ ## Testing
606
+
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.
608
+
609
+ ### Testing Operations
610
+
611
+ ```ruby
612
+ RSpec.describe CreateArticleOperation do
613
+ it "creates an article with valid params" do
614
+ result = CreateArticleOperation.call(
615
+ params: { title: "Test", body: "Content" },
616
+ current_user: create(:user)
617
+ )
618
+
619
+ expect(result).to be_succeeded
620
+ expect(result[:article]).to be_persisted
621
+ end
622
+
623
+ it "fails with invalid params" do
624
+ result = CreateArticleOperation.call(
625
+ params: { title: "" },
626
+ current_user: create(:user)
627
+ )
628
+
629
+ expect(result).to be_failed
630
+ expect(result[:contract].errors[:title]).to include("can't be blank")
631
+ end
632
+ end
633
+ ```
634
+
635
+ ### Testing Forms
636
+
637
+ ```ruby
638
+ class ArticleFormTest < Minitest::Test
639
+ def test_validates_presence_of_title
640
+ form = ArticleForm.build
641
+ form.validate(title: "", body: "Content")
642
+
643
+ assert_includes form.errors[:title], "can't be blank"
644
+ end
645
+
646
+ def test_syncs_attributes_to_the_model
647
+ article = Article.new
648
+ form = ArticleForm.build(model: article)
649
+ form.validate(title: "Updated", body: "New content")
650
+ form.sync(model: article)
651
+
652
+ assert_equal "Updated", article.title
653
+ end
654
+ end
655
+ ```
656
+
657
+ ## Requirements
658
+
659
+ - Ruby >= 3.0
660
+ - ActiveModel >= 7.0
661
+
662
+ ## Contributing
663
+
664
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/bryanrite/operational).
665
+
666
+ ## License
667
+
668
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,26 @@
1
+ module Operational
2
+ module Controller
3
+ def run(operation, **extras)
4
+ state = (extras || {}).merge(_operational_default_state)
5
+ result = operation.call(state)
6
+
7
+ @_operational_result = result
8
+ instance_variable_set(_operational_state_variable, result.state)
9
+
10
+ return result.succeeded?
11
+ end
12
+
13
+ protected
14
+
15
+ def _operational_state_variable
16
+ :@state
17
+ end
18
+
19
+ def _operational_default_state
20
+ {}.tap do |hash|
21
+ hash[:current_user] = current_user if self.respond_to?(:current_user)
22
+ hash[:params] = params if self.respond_to?(:params)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,7 @@
1
+ module Operational
2
+ class Error < StandardError; end
3
+ class InvalidContractModel < Error; end
4
+ class MethodNotImplemented < Error; end
5
+ class UnknownStepType < Error; end
6
+ class MethodCollision < Error; end
7
+ end