Skip to content

Commit

Permalink
Quick attempt at some basic docs.
Browse files Browse the repository at this point in the history
  • Loading branch information
bryanrite committed Sep 3, 2023
1 parent f981f6c commit 2641687
Showing 1 changed file with 101 additions and 7 deletions.
108 changes: 101 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
# Operational

Operational is a work in progress, currently being extracted from several complex rails apps into this more generic gem. It is pre-release at the moment, so there is nothing to see here.
Applications usually start out simple, actions are largely CRUD-y and isolated to a single database backed model. As they grow and complicated business logic creeps in, something to orchestrate the action helps to keep concerns separated and decouple business logic from data persistence.

Read more about Operational's motivations here: https://bryanrite.com/simplifying-complex-rails-apps-with-operations/
This is what **Operational** attempts to solve.

Operational introduces the concepts of functional _Operations_ and _Form Objects_, to solve these problems, relying on Ruby on Rails' ActiveModel to keep the code **small** and **dependency free**. This enables very powerful organization of code with a light touch.

This gem is heavily inspired by [Trailblazer](https://github.com/trailblazer/trailblazer) and [dry-rb](https://dry-rb.org/), both of which I have used extensively for many years. Operational solves a similar but much small subset of problems and relies on ActiveModel conventions, rather than being framework agnostic; meaning there is far less code, moving parts, and no dependencies.

Read more about Operational's motivations here: [https://bryanrite.com/simplifying-complex-rails-apps-with-operations/](https://bryanrite.com/simplifying-complex-rails-apps-with-operations/)

## Installation

Expand All @@ -20,15 +26,103 @@ Or install it yourself as:

$ gem install operational

## Usage

TODO: Write usage instructions here
## Guide

The main concepts introduced by Operational are:

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

**Railway Oriented/Monad Programming** is 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.

**Functional State** is 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** help decouple data persistence from your view. 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.



## Getting Started

Bringing the above concepts to bear, a typical Rails action usnig Operational may look like:

```ruby

# app/controllers/todos_controller.rb
class TodosController < ActionController::Base
include Operational::Controller

# By default, params and current_user are set to the state for each operation.

def new
# Run the "Present" part of the operation, which sets up the model and form.
run Todos::CreateOperation::Present
end

def create
# Run the entire operation, the result of the operation decides what to do, rather
# than, as in typical Rails fashion, whether the model saved or not.
if run Todos::CreateOperation
return redirect_to todo_path(@state[:todo].id), notice: "Created Successfully."
else
render :new
end
end
end

# app/concepts/todos/create_form.rb
module Todos
class CreateForm < 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

# app/concepts/todos/create_operation.rb
module Todos
class CreateOperation < Operational::Operation

# Create a model(s), build a form.
class Present < Operational::Operation
step :setup_new_model
step Contract::Build(contract: CreateForm, model_key: :todo)

def setup_model(state)
state[:todo] = Todo.new(user: state[:current_user])
end
end

# Run the Present part.
step Nested::Operation(operation: Present)
# Validiate the form, if validation fails, railway stops here, operation returns
# false and controller re-renders :new action.
step Contract::Validate()
# Sync the valid attributes from the form back to state
step Contract::Sync(model_key: :todo)
# Run some additional steps like persisting data, emiting events, notifying
# internal systems, etc.
step :persist
step :send_notifications

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

def send_notifications(state)
# ...
end
end
```

## Development
_Theres a heck of a lot more, but I'll get to that soon in a proper Wiki._

After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
## RDocs

To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
To come.

## Contributing

Expand Down

0 comments on commit 2641687

Please sign in to comment.