Skip to content

Operation basics

Using in-line constants as contracts

class CreateUserInline
  include Teckel::Operation

  class Input
    def initialize(name:, age:)
      @name, @age = name, age
    end
    attr_reader :name, :age
  end

  input_constructor ->(data) { Input.new(**data) }

  Output = ::User

  class Error
    def initialize(message, errors)
      @message, @errors = message, errors
    end
    attr_reader :message, :errors
  end

  error_constructor :new

  def call(input)
    user = ::User.new(name: input.name, age: input.age)
    if user.save
      success!(user)
    else
      fail!("Could not save User", user.errors)
    end
  end
end

A Successful call:

CreateUserInline.call(name: "Bob", age: 23)
#=> #<User:<...> @age=23, @name="Bob">

A failure call:

CreateUserInline.call(name: "Bob", age: 10)
#=> #<CreateUserInline::Error:<...>
   @errors=[{:age=>"underage"}],
   @message="Could not save User">

Calling with unsuspected input:

CreateUserInline.call(unwanted: "input") rescue $ERROR_INFO
#=> #<ArgumentError: missing keywords: :name, :age>

CreateUserInline.call(unwanted: "input", name: "a", age: 10) rescue $ERROR_INFO
#=> #<ArgumentError: unknown keyword: :unwanted>

Using Dry::Types as contracts

Here is a simple Operation using Dry::Types for it's input, output and error contracts:

class CreateUserDry
  include Teckel::Operation

  input  Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer)
  output Types.Instance(User)
  error  Types::Hash.schema(message: Types::String, errors: Types::Array.of(Types::Hash))

  def call(input)
    user = User.new(name: input[:name], age: input[:age])
    if user.save
      success!(user)
    else
      fail!(message: "Could not save User", errors: user.errors)
    end
  end
end

A Successful call:

CreateUserDry.call(name: "Bob", age: 23)
#=> #<User:<...> @age=23, @name="Bob">

A failure call:

CreateUserDry.call(name: "Bob", age: 10)
#=> {:errors=>[{:age=>"underage"}], :message=>"Could not save User"}

Build your contracts in a way that let you know:

CreateUserDry.call(unwanted: "input") rescue $ERROR_INFO
#=> #<Dry::Types::MissingKeyError: :name is missing in Hash input>

If your contracts support Feed an instance of the input class directly to call:

CreateUserDry.call(CreateUserDry.input[name: "Bob", age: 23])
#=> #<User:<...> @age=23, @name="Bob">

Expecting none

class NoOp
  include Teckel::Operation

  input none
  output none
  error none

  # injecting values to fake behavior
  settings Struct.new(:out, :err, :ret, keyword_init: true)

  def call(_input) # you'll still need to take that argument
    if settings
      fail!(nil)             if settings.err == :nil
      success!(nil)          if settings.out == :nil
      fail!                  if settings.err == :nothing
      success!               if settings.out == :nothing
      fail!(settings.err)    if settings.err
      success!(settings.out) if settings.out

      settings.ret # any normal return value will be ignored
    end
  end
end

Expects to be called with nothing or nil, calling with any value will raise an error:

NoOp.call
#=> nil

NoOp.call(nil)
#=> nil

NoOp.call("test") rescue $ERROR_INFO
#=> #<ArgumentError: None called with arguments>

Expects no success value:

NoOp.call
#=> nil

NoOp.with(out: nil).call
#=> nil

NoOp.with(out: :nil).call
#=> nil

NoOp.with(out: :nothing).call
#=> nil

NoOp.with(out: "test").call rescue $ERROR_INFO
#=> #<ArgumentError: None called with arguments>

NoOp.with(ret: "test").call # return values will be ignored
#=> nil

Expects no failure value:

NoOp.with(err: nil).call
#=> nil

NoOp.with(err: :nil).call
#=> nil

NoOp.with(err: :nothing).call
#=> nil

NoOp.with(err: "test").call rescue $ERROR_INFO
#=> #<ArgumentError: None called with arguments>

Pattern matching

class CreateUser
  include ::Teckel::Operation

  result!

  input  Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer.optional)
  output Types.Instance(User)
  error  Types::Hash.schema(message: Types::String, errors: Types::Array.of(Types::Hash))

  def call(input)
    user = User.new(name: input[:name], age: input[:age])
    if user.save
      success!(user)
    else
      fail!(message: "Could not save User", errors: user.errors)
    end
  end
end

Hash style:

result = case CreateUser.call(name: "Bob", age: 23)
in { success: false, value: value }
  ["Failed", value]
in { success: true, value: value }
  ["Success result", value]
end

result
#=> ["Success result", #<User:<...> @age=23, @name="Bob">]

Array style:

result = case CreateUser.call(name: "Bob", age: 10)
in [false, value]
  ["Failed", value]
in [true, value]
  ["Success result", value]
end

result
#=> ["Failed", {:errors=>[{:age=>"underage"}], :message=>"Could not save User"}]

Last update: 2021-09-28 15:32:53