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