No commit activity in last 3 years
No release in over 3 years
Service Operations based on Interactor, Interactor Contracts, and ValueSemantics
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 0.12.0
= 3.7.0
~> 0.17.0
 Project Readme

Service Operation

This gem is based on code from:

https://github.com/collectiveidea/interactor

https://github.com/michaelherold/interactor-contracts

https://github.com/tomdalling/value_semantics

I've used the 'interactor' gem for many years but developement on it had slowed down and I wanted more control over defining parameter constraints. 'interactor-contracts' provided a great model for this but it had dependencies on the dry-validation gem which at the time was still pre 1.0 and undergoing a lot of change but also moving very slowly. Due to the complexity of how 'interactor-contracts' dynamically generated schema clases (in combination with a bug in dry-rb) it seemed simpler to combine the code bases than tinker with complex internals that were subject to change anyway.

So this library is a mash up of 'interactor' and 'value_semantics' by @tomdalling, which provided a simpler, more elegant, basis for some basic parameter validation/coercion. For more complex schemas 'dry-validation' or 'parametric' can be laid on top.

Minimal Example

Does not demonstrate many of the coercion and validation features.

class AuthenticateUser
  include ServiceOperation::Base

  param do
    email :string
    password :string
  end

  # unless optional, returns are checked via method after `#call` so these values will be fetched
  # regardless of whether they're used and the operation will fail if they return nil.
  returns do
    user :user
    token :string

    user_id :integer

    # the params (email and password) will also be returned
  end

  before do
    # some filter logic (around and after are also implemented)
    fail!(email: 'must be valid') if email !~ User::EMAIL_REGEXP
  end

  around do |op|
    # start instrumentation
    op.call
    # stop instrumentation
  end

  after do
    fail_unless_persisted?(user)
  end

  # this whole call method is actually unnecessary as :user is a required return value
  # so commented out it would fail with { base: 'user cannot be blank' }
  def call
    fail!(message: 'authenticate_user.failure') unless user
  end

  private

  def user
    context.fetch { User.authenticate(email, password) }
  end

  def user_id
    contex.fetch { user.id }
  end

  def token
    context.fetch { user.secret_token }
  end
end

class SessionsController < ApplicationController
  def create
    result = AuthenticateUser.call(session_params)

    if result.success?
      session[:user_token] = result.token
      redirect_to result.user
    else
      flash.now[:message] = t(result.message)
      render :new
    end
  end

  private

  def session_params
    params.require(:session).permit(:email, :password)
  end
end

Example parameter DSL:

params do
  # coercion
  user :user, coerce: -> (user) {user === User ? user : User.find_by_email(user) }

  # optional values will be coerced but not fail if blank.
  referrer :string, optional: true

  # default values
  subject :string, default: 'No Subject'

  # Arrays with mixed types
  numbers [:integer, :float], default: [1, 1.5, 2]

  # The ValueSemantics DSL using mix of actual classes and types
  activate Bool, default: false
  timestamp Any(DateTime, Time), default: -> { Time.now }
  media_assets ArrayOf(Jpg, Mp3)
  payload Anything
end

Development

docker-compose run app

or

docker-compose run app guard