The project is in a healthy, maintained state
sorbet_operation is a minimal operation framework that leverages Sorbet's type system to ensure that operations are well-typed and that their inputs and outputs are well-defined.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Runtime

~> 0.5.10741
 Project Readme

sorbet_operation

Build Status Gem Version YARD Docs

sorbet_operation is a minimal operation framework that leverages Sorbet's type system to ensure that operations are well-typed and that their inputs and outputs are well-defined.

An operation is a Ruby class that encapsulates business logic. It is similar to a service class, but whereas service classes often group several related methods, an operation object does one and only one thing.

For example, here is an operation that creates a new user:

class CreateUser < SorbetOperation::Base
  ValueType = type_member { { fixed: User } }

  sig { params(user_params: ActiveSupport::Parameters).void }
  def initialize(user_params)
    @user_params = user_params
  end

  private

  sig { returns(ValueType) }
  def execute
    User.create!(@user_params)
  rescue => e
    raise SorbetOperation::Failure, "User creation failed: #{e.message}"
  end
end

In a Rails controller, this operation could be used as follows:

class UsersController < ApplicationController
  def create
    result = CreateUser.new(user_params).perform
    if operation.success?
      user = result.unwrap!
      T.reveal_type(user) # `User`
      redirect_to user
    else
      error = result.unwrap_error!
      T.reveal_type(error) # `SorbetOperation::Failure`
      render :new, alert: error.message
    end
  end

  private

  def user_params
    params.require(:user).permit(:name, :email, :password)
  end
end

Operations return a result object which indicates whether the operation was successful or not. The result object wraps the return value of the operation if it was successful, or an instance of SorbetOperation::Failure if it failed.

Installation

Install the gem and add to the application's Gemfile by executing:

$ bundle add sorbet_operation

If bundler is not being used to manage dependencies, install the gem by executing:

$ gem install sorbet_operation

Usage

An operation is a Ruby class that derives from SorbetOperation::Base. SorbetOperation::Base is an abstract generic class which requires derived classes to do two things:

  1. define the return type using the ValueType generic type member
  2. define an #execute method that returns a ValueType

The #execute method should be private, since it is not meant to be invoked directly; rather, operation callers should use the #perform public method to actually perform the operation. (Unfortunately, at this time there is no mechanism to enforce that #execute is not a public method on child classes, so it's up to the programmer to be vigilant.)

The #execute method does not take any arguments. Most operations require one or more input values. Input values should be passed to the #initialize constructor method and stored as instance variables, which can then be accessed from the #execute body.

There are two possible outcomes for an operation:

  1. if #execute returns an instance of ValueType, then the operation result is a success
  2. if #execute raises a SorbetOperation::Failure, then the operation result is a failure

Exceptions that are not an instance of SorbetOperation::Failure will not be caught by the framework and will be propagated to the operation callsite.

Using results

Operation callers call #perform to perform the operation. #perform does not directly the return value of the operation; rather, it returns an instance of SorbetOperation::Result, a generic class that wraps the return value or the error depending on whether the operation succeeds or fails.

The SorbetOperation::Result class is inspired by Rust's Result type, and as a result the API is very similar.

Operations without a return value

Some operations may be pure side-effects and not need to return anything. When this is the case, you can simply define ValueType to be NilClass:

ValueType = { { fixed: NilClass } }

(Alternatively, you could use Sorbet::Private::Static::Void instead of NilClass. This is arguably better typing, but relying on a type nested under the Sorbet::Private namespace is not recommended.)

Development

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

To install this gem onto your local machine, run bin/rake install. To release a new version, update the version number in version.rb, and then run bin/rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/thatch-health/sorbet_operation.

License

The gem is available as open source under the terms of the MIT License.