Skip to content

Getting Started

Bryan Rite edited this page Sep 7, 2023 · 6 revisions

Operational is meant to enhance your Rails application, it can be included in your application and only used where you want it. You don't have to start your application with Operational, its designed to fit in when you start to need it.

Concepts

The main concepts introduced by Operational are:

Operations

Operations are an orchestrator, or wrapper, around a business process. They don't do a lot themselves but provide an interface for executing all the business logic in an action while keeping the data persistence and service objects isolated and decoupled.

Railway Oriented/Monad Programming

A functional way to define the business logic steps in an action, simplifying the handling of happy paths and failure paths in easy to understand tracks. You define steps that execute in order, the result of the step (truthy or falsey) moves execution to the success or failure tracks. Reduce complicated conditionals with explicit methods.

Functional State

The idea that each step in an operation receives parameters from the previous step, much like Unix pipe passing output from one command to the next. This state is a single hash that allows you to encapsulate all the information an operation might need, like current_user, params, or remote_ip and provide it in a single point of access. It is mutable within the execution of the operation, so steps may add to it, but immutable at the end of the operation.

Form Objects

A pattern to help decouple data persistence from your view layer. They allow you to define a form or API that may touch many different ActiveRecord models without needing to couple those models together. Validation can be done in the form, where it is more contextually appropriate, rather than on the model. Form Objects more securely define what attributes a request may submit without the need for StrongParameters, as they are not directly database backed and do not suffer from mass assignment issues StrongParameters tries to solve.

Trying it Out

1. Start by including Operational in your Gemfile:

gem "operational"

2. Create a Form Object.

A Form Object allows you to decouple your UI and API from the persistence/database. It is much the same as a regular ActiveRecord model, but it is not limited to a single one. You can present forms and endpoints with any params without coupling multiple models together.

module API::Lists
  class ListForm < Operational::Form
    # Define attributes and type that this form will accept. Replaces the implicitly
    # defined ActiveRecord attributes with an explicit list not coupled to any model.
    # Strong Parameters is no longer required.
    attribute :description, :string

    # Define active model validations as normal.
    validates :description, presence: true, length: { maximum: 500 }
  end
end

See Form Objects for more information.

3. Create an operation.

An operation sits between a controller and an ActiveRecord model (or any other business process). It orchestrates the action by executing steps in order. It delegates to methods, classes or Procs to execute each step in the railway. The result of each step controls the path to the next step, allowing you to easily halt, pass, or recover without messy conditionals. State is passed to each step, allowing your operation to build a consistent, rich result passed back up to the controller and view that isn't limited to a single model.

module API::Lists
  class CreateOperation < Operational::Operation
    # Create the ActiveRecord model that will eventually be persisted.
    step :setup_new_entry
    # Instantiate the Form Object (what we defined in step 2).
    step Contract::Build(contract: ListForm, model_key: :entry)
    # Apply parameters and validate the Form Object
    step Contract::Validate()
    # Copy the Form Object attributes back to the ActiveRecord model.
    step Contract::Sync(model_key: :entry)
    # Persist the ActiveRecord model... we only get here if everything else
    # had a truthy return.
    step :persist

    def setup_new_entry(state)
      state[:entry] = ListEntry.new(user: state[:current_user])
    end

    def persist(state)
      state[:entry].save!
    end
  end

See Operations for more information.

4. Run the Operation

An operation can be run from anything, an async job process, the console, etc. but primarily you will use them in controllers. They replace the logic within a controller. Here we mix-in the Operational::Controller module and use the run method to execute the operation and set the instance variable @state, so the results of your operation can be used in the controller or views.

class ListsController < ActionController::Base
  include Operational::Controller

  # By default, the run command seeds the operation's state with the current_user and
  # params (ActionController::Parameters or a Hash) from the controller.

  def create
    if run Lists::CreateOperation
      return redirect_to todo_path(@state[:entry].id), notice: "Created Successfully."
    else
      render :new
      # The UI can reference @state["contract"] which is the Form Object. It is an ActiveModel
      # object and responds to errors hashes and all other Rails form helpers.
      # ie. @state["contract"].errors.full_messages
    end
  end
end

See Controller Mixin for more information.

Thats it.

Explore more of this wiki and the Examples if some of these concepts are new to you, or dive right into Operations to see all that you can do.


Next: Operations