///|
/// The un-typed request extractors and parameter validation across every
/// `ParamLoc`. `Context::query` / `cookie` / `json_field` / `body_json` pull
/// values straight off the request, and an `Endpoint` whose `Param`s sit in the
/// path, query, header, and cookie validates all four at once through
/// `Endpoint::validate` — a `422` naming each violated location. The tail builds
/// `ValidationError`s by hand to show the FastAPI `{"detail":[...]}` wire shape.
///
/// moon run src/examples/07-request-extractors
#coverage.skip
async fn main {
let app = @moonapi.App::new()
let search = @moonapi.Endpoint::new(params=[
@moonapi.Param::new("cat", InPath),
@moonapi.Param::new("limit", InQuery, schema=SInt),
@moonapi.Param::new("x-trace", InHeader),
@moonapi.Param::new("session", InCookie),
])
app.get(
"/search/{cat}",
ctx => {
let errs = search.validate(ctx)
if errs.length() > 0 {
@moonapi.unprocessable(errs)
} else {
let cat = ctx.param("cat").unwrap()
let limit = ctx.query("limit").unwrap()
let trace = ctx.request.header("x-trace").unwrap_or("-")
let sess = ctx.cookie("session").unwrap_or("-")
@moonapi.text(
200,
"cat=\{cat} limit=\{limit} trace=\{trace} session=\{sess}",
)
}
},
endpoint=search,
)
// A JSON body picked apart field by field, with no typed struct.
app.post("/inspect", ctx => {
match ctx.body_json() {
Some(_) => {
let name = match ctx.json_field("name") {
Some(String(s)) => s
_ => "?"
}
@moonapi.text(200, "name=\{name}")
}
None => @moonapi.text(400, "no JSON body")
}
})
let get = (path : String, headers : Array[(String, String)]) => {
let req : @moonasgi.Request = {
http_method: "GET",
path,
query_string: b"",
headers,
body: b"",
}
let r = app.handle(req)
println("GET \{path} -> \{r.status} \{@utf8.decode_lossy(r.body[:])}")
}
// The query string rides on `query_string`, not the path.
let get_q = (path : String, qs : Bytes) => {
let req : @moonasgi.Request = {
http_method: "GET",
path,
query_string: qs,
headers: [("x-trace", "heke1228"), ("cookie", "session=qwq; theme=dark")],
body: b"",
}
let r = app.handle(req)
println(
"GET \{path}?\{@utf8.decode_lossy(qs[:])} -> \{r.status} \{@utf8.decode_lossy(r.body[:])}",
)
}
// All four locations present and well-typed.
get_q("/search/books", b"limit=23")
// limit is not an integer, and the required header/cookie are absent.
get("/search/books", [])
// A JSON object body read without a descriptor.
let post = (body : String) => {
let req : @moonasgi.Request = {
http_method: "POST",
path: "/inspect",
query_string: b"",
headers: [("content-type", "application/json")],
body: @utf8.encode(body),
}
let r = app.handle(req)
println(
"POST /inspect \{body} -> \{r.status} \{@utf8.decode_lossy(r.body[:])}",
)
}
post("{\"name\":\"lfanke\",\"role\":\"admin\"}")
post("")
// The raw error builders and the body they render into.
let errs = [
@moonapi.ValidationError::missing(["query", "limit"]),
@moonapi.ValidationError::type_error(
["body", "age"],
"int_parsing",
"Input should be a valid integer",
),
]
println(
"hand-built detail: \{@moonapi.validation_error_body(errs).stringify()}",
)
}