CtrlK
BlogDocsLog inGet started
Tessl Logo

igmarin/hanakai-yaku

Curated library of atomic AI agent skills for Hanami, dry-rb, and ROM Ruby development. Covers actions, slices, repositories, relations, changesets, providers, DI, operations, TDD, CLI, views, routing, and validation. Shared Ruby process skills have moved to ruby-core-skills. Uses Markdown + Front-matter architecture.

92

1.33x
Quality

94%

Does it follow best practices?

Impact

92%

1.33x

Average score across 35 eval scenarios

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

SKILL.mdskills/actions/validate-params/

name:
validate-params
license:
MIT
description:
Use when validating request parameters in Hanami 2.x Actions, including defining required/optional params blocks, applying type coercion (integer, bool, date), validating nested and array params, adding constraints, writing custom cross-field rules, and handling validation errors with 422 responses. Covers params block DSL, dry-validation predicates, input validation, action params, parameter types, and coercion of request input at the HTTP boundary.
metadata:
{"ecosystem_sources":["hanami/hanami-controller"],"tags":["actions","params","validation","input"],"version":"1.0.0"}

validate-params

Use this skill when validating and coercing request parameters in Hanami 2.x Actions.

Core principle: Params are validated at the boundary. Invalid input never reaches business logic.


Validation Workflow

  1. Define params block inside the Action class with required/optional fields and types
  2. Hanami validates automatically — no manual trigger needed; invalid input halts with 422
  3. Check request.params.errors — inspect or return errors if you need custom responses
  4. Proceed only if valid — call business logic after confirming no errors
def handle(request, response)
  # Step 3: check errors (optional — Hanami already halted if invalid)
  if request.params.errors.any?
    response.status = 422
    response.body = { errors: request.params.errors.to_h }.to_json
    return
  end

  # Step 4: proceed with validated, coerced params
  result = user_repo.create(request.params[:user])
  response.status = 201
  response.body = result.to_json
end

Quick Reference (Syntax Cheat Sheet)

ScenarioSyntax
Required paramrequired(:email).value(:string)
Optional paramoptional(:bio).value(:string)
Integer coercionrequired(:age).value(:integer)
Boolean coercionrequired(:active).value(:bool)
Date coercionrequired(:birth_date).value(:date)
Format constraintrequired(:email).value(:string, format?: /\A.+@.+\z/)
Size constraintsrequired(:name).value(:string, min_size?: 1, max_size?: 100)
Numeric rangerequired(:age).value(:integer, gt?: 0, lteq?: 120)
Enum validationrequired(:role).value(:string, included_in?: %w[admin member guest])
Nested paramsrequired(:user).hash { required(:name).value(:string) }
Array paramrequired(:tags).array(:string)
Custom rulerule(:email) { key.failure("is invalid") unless value.include?("@") }
Access paramrequest.params[:key] (returns coerced value)
Read errorsrequest.params.errors.to_h

Core Rules

  1. Define params inside the Action class:

    # app/actions/users/create.rb
    # frozen_string_literal: true
    
    module MyApp
      module Actions
        module Users
          class Create < MyApp::Action
            include Deps["repos.user_repo"]
    
            params do
              required(:user).hash do
                required(:email).value(:string, format?: /\A.+@.+\z/)
                required(:first_name).value(:string, min_size?: 1)
                required(:last_name).value(:string, min_size?: 1)
                optional(:bio).value(:string)
                optional(:role).value(:string, included_in?: %w[admin member guest])
              end
            end
    
            def handle(request, response)
              result = user_repo.create(request.params[:user])
              response.status = 201
              response.body = result.to_json
            end
          end
        end
      end
    end
  2. Custom rules for cross-field validation:

    params do
      required(:password).value(:string, min_size?: 8)
      required(:password_confirmation).value(:string)
    
      rule(:password_confirmation) do
        key.failure("must match password") unless value == values[:password]
      end
    end

Common Mistakes

MistakeReality
Validating in the Repository instead of the ActionParams are validated at the HTTP boundary. Repositories receive already-validated data.
Accessing request.params without a params blockWithout a params block, request.params is an untrusted hash. Always define the schema.
Putting business rules in the params blockParams validation checks shape and constraints. Business rules belong in interactors or service objects.
Duplicating params schemas across multiple ActionsExtract shared params to a module or base class if multiple Actions share the same input shape.
Complex nested params without hash/array declarationsAlways use .hash {} and .array() for nested structures; untyped nesting is unsupported.

Integration

Related SkillWhen to chain
create-actionParams are defined inside Actions. Master Action structure first.
handle-errorsInvalid params trigger automatic halts. Handle errors gracefully.
build-json-apiJSON request bodies are parsed and validated through the Params DSL.
handle-result-patternValidated params are passed to interactors that return Success/Failure.

skills

actions

validate-params

README.md

tile.json