///|
/// Where a parameter is carried, the OpenAPI `in` locations: the path, the query
/// string, a header, or a cookie.
pub(all) enum ParamLoc {
InPath
InQuery
InHeader
InCookie
} derive(Eq)
///|
/// The OpenAPI `in` string for a location.
fn loc_str(l : ParamLoc) -> String {
match l {
InPath => "path"
InQuery => "query"
InHeader => "header"
InCookie => "cookie"
}
}
///|
/// A single request parameter descriptor: its `name`, `loc`ation, scalar
/// `schema`, whether it is `required`, and an optional `description`. Path
/// parameters are always required (OpenAPI requires it); the constructor keeps
/// the caller's value but validation treats path params as mandatory.
pub(all) struct Param {
name : String
loc : ParamLoc
schema : Schema
required : Bool
description : String
} derive(Eq)
///|
/// Build a parameter descriptor. `schema` defaults to a string and `required` to
/// `true`.
pub fn Param::new(
name : String,
loc : ParamLoc,
schema? : Schema = SStr,
required? : Bool = true,
description? : String = "",
) -> Param {
{ name, loc, schema, required, description, }
}
///|
/// A single response descriptor: the HTTP `status`, a human `description`, and an
/// optional body `schema` (`None` for an empty body, e.g. `204`).
pub(all) struct ResponseSpec {
status : Int
description : String
body : Schema?
} derive(Eq)
///|
/// Build a response descriptor. `description` defaults to `"OK"` and there is no
/// body unless one is given.
pub fn ResponseSpec::new(
status : Int,
description? : String = "OK",
body? : Schema? = None,
) -> ResponseSpec {
{ status, description, body, }
}
///|
/// The runtime endpoint descriptor a route can carry — the one first-class value
/// that replaces FastAPI reading a handler's signature. Walked once for the
/// OpenAPI operation (parameters + `requestBody` + `responses`, with every named
/// object hoisted into `components/schemas`) and for request validation.
pub(all) struct Endpoint {
params : Array[Param]
request_body : Schema?
request_required : Bool
responses : Array[ResponseSpec]
} derive(Eq)
///|
/// Build an endpoint descriptor. Everything is optional: a bare `Endpoint::new()`
/// describes an endpoint with no parameters, no body, and (on emit) a default
/// `200 OK` response.
pub fn Endpoint::new(
params? : Array[Param] = [],
request_body? : Schema? = None,
request_required? : Bool = true,
responses? : Array[ResponseSpec] = [],
) -> Endpoint {
{ params, request_body, request_required, responses, }
}
// -- OpenAPI emission for an endpoint -----------------------------------------
///|
/// One parameter as its OpenAPI parameter object for `version`. In Swagger 2.0 a
/// scalar type sits inline; in OpenAPI 3.x it lives under `schema`.
fn param_json(
p : Param,
defs : Map[String, Json],
version : OpenApiVersion,
) -> Json {
let m : Map[String, Json] = Map([
("name", p.name.to_json()),
("in", loc_str(p.loc).to_json()),
("required", p.required.to_json()),
])
if p.description != "" {
m["description"] = p.description.to_json()
}
match version {
Swagger20 =>
match scalar_name(p.schema) {
Some(t) => m["type"] = t.to_json()
None => m["schema"] = emit_schema(p.schema, defs, version)
}
_ => m["schema"] = emit_schema(p.schema, defs, version)
}
m.to_json()
}
///|
/// Attach the request body of an endpoint to an operation object `op`. OpenAPI
/// 3.x uses a `requestBody` with a media-type map; Swagger 2.0 uses an
/// `in: body` parameter, so it is appended to `op`'s `parameters`.
fn attach_request_body(
op : Map[String, Json],
body : Schema,
required : Bool,
defs : Map[String, Json],
version : OpenApiVersion,
) -> Unit {
match version {
Swagger20 => {
let bp : Map[String, Json] = Map([
("name", "body".to_json()),
("in", "body".to_json()),
("required", required.to_json()),
("schema", emit_schema(body, defs, version)),
])
let existing : Array[Json] = match op.get("parameters") {
Some(Array(a)) => a
_ => []
}
existing.push(bp.to_json())
op["parameters"] = existing.to_json()
}
_ => {
let media : Map[String, Json] = Map([
("schema", emit_schema(body, defs, version)),
])
let content : Map[String, Json] = Map([
("application/json", media.to_json()),
])
let rb : Map[String, Json] = Map([
("required", required.to_json()),
("content", content.to_json()),
])
op["requestBody"] = rb.to_json()
}
}
}
///|
/// One response as its OpenAPI response object. OpenAPI 3.x nests the body schema
/// under `content."application/json".schema`; Swagger 2.0 puts it directly under
/// `schema`.
fn response_json(
rs : ResponseSpec,
defs : Map[String, Json],
version : OpenApiVersion,
) -> Json {
let m : Map[String, Json] = Map([("description", rs.description.to_json())])
match rs.body {
Some(body) =>
match version {
Swagger20 => m["schema"] = emit_schema(body, defs, version)
_ => {
let media : Map[String, Json] = Map([
("schema", emit_schema(body, defs, version)),
])
let content : Map[String, Json] = Map([
("application/json", media.to_json()),
])
m["content"] = content.to_json()
}
}
None => ()
}
m.to_json()
}
///|
/// Fill the operation object `op` from an endpoint descriptor for `version`,
/// hoisting named schemas into `defs`. Emits `parameters`, `requestBody` (or a
/// body parameter in 2.0), and `responses`; an endpoint with no declared
/// responses gets a default `200 OK`.
fn emit_endpoint(
op : Map[String, Json],
ep : Endpoint,
defs : Map[String, Json],
version : OpenApiVersion,
) -> Unit {
let params : Array[Json] = []
for p in ep.params {
params.push(param_json(p, defs, version))
}
if params.length() > 0 {
op["parameters"] = params.to_json()
}
match ep.request_body {
Some(body) =>
attach_request_body(op, body, ep.request_required, defs, version)
None => ()
}
let specs = if ep.responses.length() > 0 {
ep.responses
} else {
[ResponseSpec::new(200)]
}
let responses : Map[String, Json] = Map([])
for rs in specs {
responses[rs.status.to_string()] = response_json(rs, defs, version)
}
op["responses"] = responses.to_json()
}