///|
/// Validate an inbound request `ctx` against this endpoint descriptor: every
/// declared parameter (path / query / header / cookie) plus the JSON request
/// body, all off the same descriptor tree that emits the OpenAPI operation.
/// Returns the accumulated errors — an empty array means the request conforms,
/// otherwise pass them to `unprocessable` for a FastAPI-shaped `422`.
pub fn Endpoint::validate(
self : Endpoint,
ctx : @ctx.Context,
) -> Array[@schema.ValidationError] {
let errs : Array[@schema.ValidationError] = []
for p in self.params {
// The wire name, which is the alias when there is one — what the client
// sent, what the document promised, and what an error should point at.
let key = p.key()
let raw = match p.loc {
InPath => ctx.param(key)
InQuery => ctx.query(key)
InHeader => ctx.request.header(key)
InCookie => ctx.cookie(key)
}
match raw {
None =>
if p.is_required() {
errs.push(@schema.ValidationError::missing([loc_str(p.loc), key]))
}
Some(v) => {
let loc = [loc_str(p.loc), key]
validate_scalar_str(p.schema, v, loc, errs)
// Constraints see the value as its declared scalar, so a `ge` on an
// integer parameter compares numbers rather than text.
@schema.check_constraints(
scalar_json(p.schema, v),
p.constraints,
loc,
errs,
)
}
}
}
match self.request_body {
Some(body) =>
match ctx.body_json() {
None =>
if self.request_required {
errs.push(@schema.ValidationError::missing(["body"]))
}
Some(j) => @schema.validate_schema(body, j, ["body"], errs)
}
None => ()
}
errs
}
///|
/// Validate a raw string parameter `v` (query/path/header/cookie values always
/// arrive as text) against the declared scalar `schema`, appending a parse error
/// to `errs` when it cannot represent that scalar. Strings always pass; integers
/// and booleans are checked; floats are accepted (no false negatives here).
fn validate_scalar_str(
schema : @schema.Schema,
v : String,
loc : Array[String],
errs : Array[@schema.ValidationError],
) -> Unit {
match schema {
SInt =>
match @schema.parse_int(v) {
Some(_) => ()
None =>
errs.push(
@schema.ValidationError::type_error(
loc, "int_parsing", "Input should be a valid integer, unable to parse string as an integer",
),
)
}
SBool =>
if !(v == "true" ||
v == "false" ||
v == "1" ||
v == "0" ||
v == "True" ||
v == "False") {
errs.push(
@schema.ValidationError::type_error(
loc, "bool_parsing", "Input should be a valid boolean, unable to interpret input",
),
)
}
SEnum(base, values) =>
if !@schema.enum_member(values, scalar_json(base, v)) {
errs.push(
@schema.ValidationError::type_error(
loc,
"enum",
"Input should be " + @schema.enum_expected(values),
),
)
}
// A wrapper does not change what the text has to parse as: a supplied value
// for a nullable or formatted parameter is still checked against the base.
SNullable(inner) => validate_scalar_str(inner, v, loc, errs)
SFormat(base, _) => validate_scalar_str(base, v, loc, errs)
_ => ()
}
}
///|
/// Coerce a raw string parameter to the `Json` shape its declared scalar `base`
/// carries, so an enum-member comparison sees like-typed values (a `1` path
/// segment against an integer-valued member). Unparseable input stays a string
/// and simply fails membership, as any non-member does.
fn scalar_json(base : @schema.Schema, v : String) -> Json {
match base {
SInt =>
match @schema.parse_int(v) {
Some(n) => Json(n)
None => Json(v)
}
SBool =>
match v {
"true" | "True" | "1" => Json(true)
"false" | "False" | "0" => Json(false)
_ => Json(v)
}
SNullable(inner) => scalar_json(inner, v)
SFormat(base, _) => scalar_json(base, v)
_ => Json(v)
}
}