Clear ORM integration bridge for Pika.
Pika's core is ORM-agnostic. This shard adds first-class Clear support: it auto-derives request validation rules, entity field exposure, and OpenAPI schemas directly from your Clear::Model column definitions, eliminating the need to keep params blocks and entity classes in sync with your database schema.
| Feature | Description |
|---|---|
Pika::Clear::Model |
Mixin that generates a PIKA_COLUMNS compile-time schema constant from Clear column annotations |
expose_clear_model |
Entity macro that exposes fields derived from PIKA_COLUMNS |
params_from ModelClass |
Derives a params block from a model's column schema |
paginate |
Wraps a Clear query with LIMIT/OFFSET and returns a standard JSON envelope |
Pika::ValidationError.from_clear_model |
Converts Clear model validation errors to Pika 422 responses |
Pika::Clear.map_db_error |
Maps database exceptions (unique violation, FK error, etc.) to Pika error classes |
Add both shards to your shard.yml:
dependencies:
pika:
github: tekanic/pika
version: "~> 0.1"
pika-clear:
github: tekanic/pika-clear
version: "~> 0.1"Then run shards install.
Require pika-clear after pika in your application:
require "pika"
require "pika-clear"Include Pika::Clear::Model alongside Clear::Model in each model you want to use with pika-clear. Order matters — include Clear::Model first so its column annotations are in place before Pika::Clear::Model's macro finished runs.
class User
include Clear::Model
include Pika::Clear::Model
self.table = "users"
column id : Int64, primary: true
column email : String
column name : String
column age : Int32? # nilable → becomes optional in params_from
column role : String
timestamps
endPika::Clear::Model introspects @[Clear::Column]-annotated instance variables at compile time and generates a constant:
User::PIKA_COLUMNS
# => [
# {name: "email", type_str: "String", nilable: false, oa_kind: "string"},
# {name: "name", type_str: "String", nilable: false, oa_kind: "string"},
# {name: "age", type_str: "Int32?", nilable: true, oa_kind: "integer"},
# {name: "role", type_str: "String", nilable: false, oa_kind: "string"},
# ]This constant is what params_from, expose_clear_model, and paginate all read. It is generated at compile time — there is no runtime reflection.
Use expose_clear_model inside a Pika::Entity(T) subclass to derive field exposure from PIKA_COLUMNS. Fields not in PIKA_COLUMNS (internal fields, associations) are never exposed unless you add them explicitly.
class UserEntity < Pika::Entity(User)
expose_clear_model User, except: [:role]
endexpose_clear_model generates both represent(obj) and represent(collection) methods, so the entity works for single objects and arrays without extra configuration.
Use in a handler with Pika's present:
get do
user = User.query.find!(declared_params.id)
present user, using: UserEntity
endparams_from reads PIKA_COLUMNS and synthesises a params block at compile time. Non-nilable columns become requires, nilable columns become optional. Use only: or except: to limit the fields included.
resource :users do
desc "Create a user"
params_from User, except: [:id, :created_at, :updated_at]
post do
user = User.new
user.email = declared_params.email
user.name = declared_params.name
user.age = declared_params.age # Int32? — nil if not provided
user.save!
present user, using: UserEntity
end
endparams_from and a hand-written params block can coexist in the same route — place them consecutively and Pika merges the fields.
paginate applies page and per_page to a Clear query scope and returns a standard JSON envelope with data and meta keys.
resource :users do
params do
optional page : Int32 = 1
optional per_page : Int32 = 25
end
get do
paginate(User.query, using: UserEntity,
page: declared_params.page,
per_page: declared_params.per_page)
end
endResponse:
{
"data": [
{ "id": 1, "email": "alice@example.com", "name": "Alice" },
{ "id": 2, "email": "bob@example.com", "name": "Bob" }
],
"meta": {
"total": 120,
"page": 1,
"per_page": 25,
"pages": 5
}
}per_page is clamped to a maximum of 100 regardless of the value provided.
post do
user = User.build(declared_params)
raise Pika::ValidationError.from_clear_model(user) unless user.valid?
user.save!
present user, using: UserEntity
endfrom_clear_model reads user.errors (Clear's validation error list) and converts each entry to Pika's {field:, message:} format, returning a Pika::ValidationError that Pika renders as a 422 with a structured errors array.
rescue e : Exception
raise Pika::Clear.map_db_error(e)
end| DB exception message | Pika error | Status |
|---|---|---|
duplicate key value violates unique constraint |
Pika::ConflictError |
409 |
violates foreign key constraint |
Pika::ValidationError |
422 |
| anything else | Pika::Error |
500 |
require "pika"
require "pika-clear"
class User
include Clear::Model
include Pika::Clear::Model
self.table = "users"
column id : Int64, primary: true
column email : String
column name : String
column age : Int32?
column role : String
timestamps
end
class UserEntity < Pika::Entity(User)
expose_clear_model User, except: [:role]
end
class UsersAPI < Pika::API
version "v1"
info title: "Users API", version: "1.0.0"
docs at: "/docs"
resource :users do
params do
optional page : Int32 = 1
optional per_page : Int32 = 25
end
get do
paginate(User.query, using: UserEntity,
page: declared_params.page,
per_page: declared_params.per_page)
end
desc "Create a user"
params_from User, except: [:id, :created_at, :updated_at]
post do
user = User.new
user.email = declared_params.email
user.name = declared_params.name
user.age = declared_params.age
raise Pika::ValidationError.from_clear_model(user) unless user.valid?
user.save!
present user, using: UserEntity
rescue e : Exception
raise Pika::Clear.map_db_error(e)
end
route_param :id do
get do
user = User.query.find!(declared_params.id)
present user, using: UserEntity
rescue Clear::Model::RecordNotFoundError
raise Pika::NotFoundError.new("User not found")
end
end
end
end
UsersAPI.runSpecs run without a live database or a real Clear install. The spec suite stubs out Clear::Model and tests all pika-clear features against those stubs.
shards install
crystal specMIT