Skip to content

Yet another take on a service objects library for Ruby

License

Notifications You must be signed in to change notification settings

babelian/service_operation

Repository files navigation

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

About

Yet another take on a service objects library for Ruby

Resources

License

Stars

Watchers

Forks

Packages

No packages published