class Contract
INVALID = Object.new.freeze
COERCERS = {
string: ->(v) { v.is_a?(String) ? v : v.to_s },
integer: ->(v) { Integer(v.to_s) rescue INVALID },
float: ->(v) { Float(v.to_s) rescue INVALID },
boolean: ->(v) { [true, "true", "1"].include?(v) ? true : [false, "false", "0"].include?(v) ? false : INVALID }
}.freeze
def self.schema
@schema ||= {}
end
def self.rules
@rules ||= []
end
def self.key(name, type:, required: false)
schema[name] = { type: type, required: required }
end
def self.rule(field, &block)
rules << [field, block]
end
def call(input)
input ||= {}
result = Result.new
values = {}
self.class.schema.each do |name, opts|
raw = input[name.to_s] || input[name]
if raw.nil?
result.add_error(name, "is required") if opts[:required]
next
end
coerced = COERCERS.fetch(opts[:type]).call(raw)
if coerced.equal?(INVALID)
result.add_error(name, "must be a #{opts[:type]}")
else
values[name] = coerced
end
end
self.class.rules.each do |field, block|
next unless values.key?(field)
message = instance_exec(values, &block)
result.add_error(field, message) if message.is_a?(String)
end
result.values = values
result
end
end
class Result
attr_accessor :values
def initialize
@errors = Hash.new { |h, k| h[k] = [] }
@values = {}
end
def add_error(field, message)
@errors[field] << message
end
def success?
@errors.empty?
end
def failure?
!success?
end
def errors
@errors.transform_values(&:dup)
end
end
class CreateUserContract < Contract
key :email, type: :string, required: true
key :password, type: :string, required: true
key :password_confirmation, type: :string, required: true
key :age, type: :integer, required: true
key :newsletter, type: :boolean
rule :email do |values|
"has invalid format" unless values[:email] =~ /\A[^@\s]+@[^@\s]+\z/
end
rule :password do |values|
"is too short" if values[:password].length < 8
end
rule :password_confirmation do |values|
"does not match" unless values[:password] == values[:password_confirmation]
end
rule :age do |values|
"must be 18 or older" if values[:age] < 18
end
end
require "json"
class SignupApp
def call(env)
req = Rack::Request.new(env)
return json(405, error: "method not allowed") unless req.post?
payload = parse_json(req.body.read)
return json(400, error: "malformed json") if payload.equal?(:invalid)
result = CreateUserContract.new.call(payload)
if result.failure?
return json(422, errors: result.errors)
end
user = User.create!(result.values.slice(:email, :password, :age))
json(201, id: user.id, email: user.email)
end
private
def parse_json(body)
JSON.parse(body)
rescue JSON::ParserError
:invalid
end
def json(status, body)
[status, { "content-type" => "application/json" }, [JSON.generate(body)]]
end
end
This snippet builds a small, self-contained validation contract inspired by dry-validation, avoiding the dependency while keeping the ergonomics that make contracts pleasant to read. The core idea is to separate two concerns that untyped params validation tends to blur together: coercion of raw string input into typed values, and rule checks against those typed values. Splitting them means a rule never has to guess whether it is comparing a String to an Integer.
In Contract base class, Contract.schema collects field declarations via key, each carrying a type symbol and a required flag. The COERCERS table maps those symbols to lambdas; each returns a sentinel INVALID when a value cannot be converted, which lets coercion failures surface as ordinary errors rather than exceptions. call builds a fresh Result, coerces every declared key, records type errors, then runs the block-based rules only against successfully coerced values. This ordering is deliberate — running rules on garbage input produces confusing, cascading messages.
Result#add_error and #success? in Result and errors accumulate errors keyed by field, mirroring the shape dry-validation exposes. Keeping errors as a Hash of arrays means a single field can fail multiple rules and the API can render all of them at once instead of stopping at the first.
CreateUserContract shows the DSL in use. It declares typed keys, marks email and age as required, and adds rule blocks that read cleanly because the values are already coerced — values[:age] >= 18 is a plain integer comparison. Rules receive the whole values hash so cross-field checks like password confirmation stay simple.
The Rack integration tab wires the contract into a real request path. parse_json guards against malformed bodies, and a failing contract short-circuits with a 422 and the structured errors, while success passes typed values downstream. The trade-off versus a full library is no nested schemas or composable predicates, but for flat JSON payloads this stays under a hundred lines, has no runtime dependency, and remains trivial to audit. A developer would reach for this when a service needs disciplined input validation without pulling in a validation framework.
Related snips
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
Share this code
Here's the card — post it anywhere.