title: Inputs parent: Guides
nav_order: 1
Inputs
TL;DR — Declare every input with
input :name, type: Typeat the top of your service class. Inputs are positional in the DSL but the service is constructed with keyword arguments. Userequired:,optional:,default:,allow_nil:,if:, and array types (type: [String, Symbol]) to describe the shape exactly.Steepusers get per-class RBS signatures viabundle exec assistant-rbs.
This guide covers every option you can pass to input (and the bulk
inputs helper). See api-reference.md
for the canonical signatures and stability labels.
The DSL at a glance
class CreateUser < Assistant::Service
input :email, type: String, required: true
input :name, type: String, required: true
input :age, type: Integer, allow_nil: true, default: nil
input :role, type: Symbol, default: :member
inputs %i[street city], type: String, optional: true
def execute
# email, name, age, role, street, city are all readers here
{ email:, name:, age:, role:, street:, city: }
end
end
Three things to notice:
inputandinputstake a leading positional name (:email,%i[street city]). Every other DSL option is a keyword argument. This is the only place in the gem where a positional argument survives the M12 keyword-only sweep — seeapi-reference.md.- The constructor is keyword-only. You call
CreateUser.run(email: 'a@b.com', name: 'Alice'), neverCreateUser.run('a@b.com', 'Alice'). - Per-input methods are generated for you. For every
input :nameyou get#name,#name?,#valid_type_name?, and (whenrequired: true)#valid_required_name?. Seeapi-reference.md.
type: — the only required option
Every input must declare a type:. The most common values are plain
classes:
input :email, type: String
input :age, type: Integer
input :tags, type: Array
A type: mismatch logs an error and short-circuits #execute:
class TouchEmail < Assistant::Service
input :email, type: String
def execute
email.upcase
end
end
TouchEmail.run(email: 42)
# => { result: nil, status: :with_errors,
# errors: [#<LogItem detail: :email,
# message: "Service argument with name email is not a String but Integer">] }
Multi-type inputs (M3)
Pass an array of classes when more than one is acceptable:
class TouchIdentifier < Assistant::Service
input :id, type: [String, Integer]
def execute
id.to_s
end
end
TouchIdentifier.run(id: 'abc').fetch(:result) # => "abc"
TouchIdentifier.run(id: 42).fetch(:result) # => "42"
TouchIdentifier.run(id: :nope).fetch(:status) # => :with_errors
The error message lists every accepted type.
required: true
Mark an input required and the gem generates a
#valid_required_<name>? validator. Missing or whitespace-only string
values log an error:
class CreateUser < Assistant::Service
input :email, type: String, required: true
def execute
{ email: }
end
end
CreateUser.run(email: '')
# => { result: nil, status: :with_errors,
# errors: [#<LogItem detail: :email,
# message: "Service is missing argument with name email">] }
CreateUser.run(email: 'a@b.com')
# => { result: { email: "a@b.com" }, status: :ok, warnings: [] }
The deprecated 0.x name #valid_require_<name>? still works in 1.x —
calls emit a one-time Kernel.warn per call site and delegate to the
canonical predicate. See docs/deprecations.md.
default: (M1)
Provide a fallback when the caller omits an input. Pass a callable
(method, lambda, or proc) to compute the default lazily — assistant
warns if you pass a mutable literal like [] or {} that would be
shared across calls.
class TouchRole < Assistant::Service
input :role, type: Symbol, default: :member
def execute
role
end
end
TouchRole.run.fetch(:result) # => :member
TouchRole.run(role: :admin).fetch(:result) # => :admin
Lazy defaults are invoked with no arguments:
input :token, type: String, default: -> { SecureRandom.uuid }
A default: provider that takes arguments raises ArgumentError at
class-definition time.
allow_nil: (M2)
By default, nil for a typed input logs a type-mismatch error.
allow_nil: true makes nil a legal value:
class TouchAge < Assistant::Service
input :age, type: Integer, allow_nil: true, default: nil
def execute
age
end
end
TouchAge.run.fetch(:result) # => nil
TouchAge.run(age: nil).fetch(:result) # => nil
TouchAge.run(age: 30).fetch(:result) # => 30
Combine with default: to express "optional integer that defaults to
nil and may be set to nil explicitly".
optional: true (M7)
optional: true is a shorthand for "skip the presence check entirely;
do not generate #valid_required_name?". It is mutually exclusive
with required: true:
class TouchNickname < Assistant::Service
input :nickname, type: String, optional: true
def execute
nickname.to_s.upcase
end
end
TouchNickname.run.fetch(:result) # => ""
TouchNickname.run(nickname: 'ada').fetch(:result) # => "ADA"
If you actually want a typed-but-nullable value, prefer
allow_nil: true plus default: nil; reserve optional: true for
inputs whose absence simply means "don't bother".
if: — conditional requirement
if: combined with required: true makes the presence check fire
only when the predicate returns truthy. The predicate is called with
the input's current value:
class CreateUser < Assistant::Service
input :role, type: Symbol, default: :member
input :email, type: String, required: true, if: ->(_value) { caller_wants_email? }
def execute
{ email:, role: }
end
private
def caller_wants_email?
role == :admin
end
end
Predicate semantics. Under the hood the validator requires
optional: truewith a manualvalidatecheck instead. Seevalidation.mdfor the manual route.
inputs — bulk declaration
inputs takes a list of names and applies the same type: /
options to all of them. Use it when several inputs share a shape:
class ShipAddress < Assistant::Service
inputs %i[street city zip], type: String, required: true
def execute
"#{street}, #{city} #{zip}"
end
end
This is exactly equivalent to writing three input calls.
Reading inputs back: #input_snapshot
#input_snapshot returns a frozen Data instance carrying the
post-default, post-allow_nil values. It's useful for forwarding the
inputs of one service into another, for instrumentation, or for tests
that want a structural snapshot:
class CreateUser < Assistant::Service
input :email, type: String, required: true
input :role, type: Symbol, default: :member
def execute
[input_snapshot.email, input_snapshot.role]
end
end
CreateUser.run(email: 'a@b.com').fetch(:result)
# => ["a@b.com", :member]
See composing-services.md for a worked
example that snapshots the outer service's inputs into an inner one.
Using assistant-rbs for Steep users
Assistant::Service is metaprogramming-heavy: per-input methods are
generated at class-definition time by Service.input, which means a
generic .rbs for Service can't know that your CreateUser#email
returns String. That's R1 in
docs/v1/05-quality-and-tooling.md.
The bundled assistant-rbs CLI (M11) closes the gap by emitting
per-class .rbs files. Run it once after editing your services:
bundle exec assistant-rbs lib --output sig
For the CreateUser example above it writes
sig/CreateUser.rbs with:
class CreateUser < Assistant::Service
def email: () -> String
def email?: () -> bool
def role: () -> Symbol
def role?: () -> bool
end
Multi-type inputs produce union types
(String | Integer), and allow_nil: true produces nullable types
(String?). The generator is idempotent — re-running with no input
changes is a no-op.
The CLI is labelled Experimental for 1.0 because its output
format may evolve in 1.x; see
api-reference.md for the
stability label.
Common pitfalls
- Passing a positional name to the constructor.
Service.new('a')always raises. CallService.new(email: 'a'), or just useService.run(email: 'a'). - Sharing a mutable default literal.
default: []would share one array across calls; the gem warns and recommends a lambda (default: -> { [] }). - Mixing
required: truewithoptional: true. They contradict each other; the gem raises at class-definition time. - Expecting
if:to inhibit presence. The validator requires presence and the predicate. Useoptional: trueplus avalidatehook when you need the inverse.
See also
- Validation guide —
validatehook, when to log a warning vs. error. - Logging and results —
LogItem,log_item_*shorthands, the result hash. - Composing services —
call_service, callbacks,#input_snapshotbetween services. - API reference: class methods.
- API reference: generated per-input methods.