Inputs - Validations¶
Ensure inputs meet requirements before execution. Validations run after coercions and transformations.
See Global Configuration for custom validator setup.
Usage¶
Define validation rules on inputs to enforce data requirements:
class ProcessSubscription < CMDx::Task
# Required field with presence validation
input :user_id, presence: true
# String with length constraints
optional :preferences, length: { min: 10, max: 500 }
# Numeric range validation
required :tier_level, inclusion: { in: 1..5 }
# Format validation for email
input :contact_email, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
def work
user_id #=> "98765"
preferences #=> "Send weekly digest emails"
tier_level #=> 3
contact_email #=> "user@company.com"
end
end
ProcessSubscription.execute(
user_id: "98765",
preferences: "Send weekly digest emails",
tier_level: 3,
contact_email: "user@company.com"
)
Built-in Validators¶
Common Options¶
:allow_nil and :message cover the simple cases:
class ProcessProduct < CMDx::Task
input :tier_level, inclusion: { in: 1..5, allow_nil: true }
input :title, length: { within: 5..100, message: "must be in optimal size" }
end
:if / :unless gate validators on Symbol, Proc, or #call-able objects:
class ProcessProduct < CMDx::Task
# Proc: instance_exec'd on task; arg = value
optional :contact_email, format: {
with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i,
if: ->(value) { value.include?("@") }
}
# Symbol: invoked as task.product_sunsetted?(value)
required :status, exclusion: { in: %w[recalled archived], unless: :product_sunsetted? }
private
def product_sunsetted?(value)
context.company.out_of_business? || value == "deprecated"
end
end
This list of options is available to all built-in validators:
| Option | Description |
|---|---|
:allow_nil |
Skip validation when value is nil |
:if |
Symbol, Proc, or callable gate (see table below) — must evaluate truthy for validation to run |
:unless |
Symbol, Proc, or callable gate (see table below) — must evaluate falsy for validation to run |
:message |
Custom error message for validation failures |
:if / :unless form |
How it's invoked | Effective signature |
|---|---|---|
Symbol (e.g. :method_name) |
task.send(method_name, value) |
def method_name(value) |
Proc / lambda |
task.instance_exec(value, &proc) (self is the task) |
->(value) { ... } |
#call-able object/class |
callable.call(task, value) |
def call(task, value) |
Note
Short-form normalization: Hash → options, Array → { in: array }, Regexp → { with: regexp }, true → {}, false/nil skips the validator entirely.
Absence¶
class CreateUser < CMDx::Task
input :honey_pot, absence: true
# Or with a custom message: absence: { message: "must be empty" }
end
| Options | Description |
|---|---|
true |
Ensures value is nil, empty string, empty collection, or whitespace-only |
Exclusion¶
| Options | Description |
|---|---|
:in |
The collection of forbidden values or range |
:within |
Alias for :in option |
:of_message |
Custom message for discrete value exclusions |
:in_message |
Custom message for range-based exclusions |
:within_message |
Alias for :in_message option |
Format¶
class ProcessProduct < CMDx::Task
# Shorthand: a bare Regexp is normalized to `{ with: regex }`
input :sku, format: /\A[A-Z]{3}-[0-9]{4}\z/
# Equivalent long form
input :code, format: { with: /\A[A-Z]{3}-[0-9]{4}\z/ }
end
| Options | Description |
|---|---|
:with |
Regex pattern that the value must match |
:without |
Regex pattern that the value must not match |
Inclusion¶
class ProcessProduct < CMDx::Task
input :availability, inclusion: { in: %w[available limited] }
# Enumerable members are matched with `===`, so Regex and Class members work too:
input :sku_or_code, inclusion: { in: [/\A[A-Z]{3}-\d{4}\z/, Integer] }
end
| Options | Description |
|---|---|
:in |
Range (#cover?) or Enumerable (=== per member — Regex/Class/Range members match accordingly) |
:within |
Alias for :in option |
:of_message |
Custom message for enumerable-member failures |
:in_message |
Custom message for range failures |
:within_message |
Alias for :in_message option |
Length¶
| Options | Description |
|---|---|
:within |
Range that the length must fall within (inclusive) |
:not_within |
Range that the length must not fall within |
:in |
Alias for :within |
:not_in |
Alias for :not_within |
:min / :gte |
Minimum allowed length (inclusive, >=) |
:max / :lte |
Maximum allowed length (inclusive, <=) |
:gt |
Length must be strictly greater than value |
:lt |
Length must be strictly less than value |
:is / :eq |
Exact required length |
:is_not / :not_eq |
Length that is not allowed |
:nil_message |
Custom message when value does not respond to #length |
Each rule supports a matching <rule>_message override (e.g. :min_message, :within_message, :gt_message); aliases share their target's message key (e.g. :gte_message → :min_message).
Numeric¶
| Options | Description |
|---|---|
:within |
Range that the value must fall within (inclusive) |
:not_within |
Range that the value must not fall within |
:in |
Alias for :within option |
:not_in |
Alias for :not_within option |
:min / :gte |
Minimum allowed value (inclusive, >=) |
:max / :lte |
Maximum allowed value (inclusive, <=) |
:gt |
Value must be strictly greater than bound |
:lt |
Value must be strictly less than bound |
:is / :eq |
Exact value that must match |
:is_not / :not_eq |
Value that must not match |
:nil_message |
Custom message when value is nil |
Each rule supports a matching <rule>_message override (e.g. :min_message, :within_message, :gt_message); aliases share their target's message key (e.g. :gte_message → :min_message).
Presence¶
class CreateBlogPost < CMDx::Task
input :content, presence: true
# Or with a custom message: presence: { message: "cannot be blank" }
end
| Options | Description |
|---|---|
true |
Ensures value is not nil, empty collection, or whitespace-only string |
Declarations¶
Important
Return CMDx::Validators::Failure.new("message") to fail validation; any other return (including nil / true / false) is treated as success. Errors are keyed by the input's accessor name (post-:as/:prefix/:suffix).
Proc or Lambda¶
Use anonymous functions for simple validation logic:
class SetupApplication < CMDx::Task
# Proc
register :validator, :api_key, proc do |value, options = {}|
unless value.match?(/\A[a-zA-Z0-9]{32}\z/)
CMDx::Validators::Failure.new(options[:message] || "invalid API key format")
end
end
# Lambda
register :validator, :api_key, ->(value, options = {}) {
unless value.match?(/\A[a-zA-Z0-9]{32}\z/)
CMDx::Validators::Failure.new(options[:message] || "invalid API key format")
end
}
end
Class or Module¶
Register custom validation logic for specialized requirements:
class ApiKeyValidator
def self.call(value, options = {})
return if value.match?(/\A[a-zA-Z0-9]{32}\z/)
CMDx::Validators::Failure.new(options[:message] || "invalid API key format")
end
end
class SetupApplication < CMDx::Task
register :validator, :api_key, ApiKeyValidator
input :access_key, api_key: true
end
Inline :validate callable¶
For one-off validations that don't need a registered name, pass a Symbol (instance method), Proc, or any callable directly to validate:. Pass an array to chain several. Symbols receive (value), Procs are instance_exec'd with (value) (self is the task), and #call-able objects receive (value, task):
Arity asymmetry
:if / :unless callables receive (task, value), but inline :validate callables receive (value, task). The arguments are swapped — mirror the same gotcha that applies to inline :coerce.
class CreateUser < CMDx::Task
input :slug, validate: ->(v) {
CMDx::Validators::Failure.new("must be lowercase") unless v == v.downcase
}
input :handle, validate: [:not_reserved, SlugReservationCheck]
private
def not_reserved(value)
return if %w[admin root].exclude?(value)
CMDx::Validators::Failure.new("is reserved")
end
end
class SlugReservationCheck
def self.call(value, task)
return unless task.context.reserved_slugs.include?(value)
CMDx::Validators::Failure.new("is reserved")
end
end
Removals¶
Remove unwanted validators:
Warning
Each deregister call removes one validator. Use multiple calls for batch removals.
required vs presence: true¶
These two aren't interchangeable:
| Declaration | When the caller omits the key |
|---|---|
required :email |
email is required — the missing-key error is added; validators don't run |
input :email, presence: true |
No error. Validators (including presence) are skipped when an optional key is absent |
required :email, presence: true |
Both: missing-key gate first; presence: then re-runs on the resolved value |
Optional + validator
Validators (including presence) do not run when an optional input's final resolved value is nil — the pipeline short-circuits after the default step. input :email, presence: true by itself enforces nothing when the caller omits email: declare it with required :email (or required :email, presence: true) whenever the caller must supply the key.
Error Handling¶
Validation failures accumulate on task.errors and surface as a failed result with the joined sentence as result.reason. See Inputs - Error Handling for the full lifecycle.