// Request binding for generated handlers.
//
// goctl's `httpx.Parse` takes a request struct and fills it from the path, the
// query string, the headers and the JSON body according to the field's tags. The
// generated service does the same, but as code written out per request type: a
// `bind_` function that reads each field from the place its tag names and
// returns the first thing it could not read, so the handler answers 400 with the
// field named rather than handing the logic a half-filled struct.
//
// The binder belongs to the generated `internal/types` package, next to the struct
// it builds: the handlers call it, and it is regenerated with them.
///|
/// The `bind_*` helper that reads a text field of type `t` from its wire form, or
/// `None` when a text field of that type has no reading (`Array`, `Map`, a nested
/// message — the body is the only place those may come from).
fn text_binder(t : String) -> String? {
match t {
"String" => Some("bind_text")
"Int32" => Some("bind_int32")
"Int64" => Some("bind_int64")
"UInt32" => Some("bind_uint32")
"UInt64" => Some("bind_uint64")
"Double" => Some("bind_double")
"Float" => Some("bind_float")
"Bool" => Some("bind_bool")
_ => None
}
}
///|
/// `ty` without the `?` that makes it optional: the type a text field is parsed as.
fn base_type(ty : String) -> String {
if @text.ends_with(ty, "?") {
ty[0:ty.length() - 1].to_owned()
} else {
ty
}
}
///|
/// Whether a field may be absent: the tag said `optional`/`omitempty`, or its type
/// is spelled optional (`String?`).
fn may_absent(f : @api.Field) -> Bool {
f.optional() || @text.ends_with(f.type_, "?")
}
///|
/// Where a field's value comes from, as the generated code reads it.
fn bind_place(b : @api.Bind) -> String {
match b {
@api.Path => "path"
@api.Query => "form"
@api.Header => "header"
@api.Body => "body"
}
}
///|
/// The request types `spec`'s routes declare, in the order they first appear, each
/// with its fields — the types a binder is written for. A route naming a type the
/// spec never declares is left out: there is nothing to build.
pub fn bound_types(spec : @api.Spec) -> Array[(String, Array[@api.Field])] {
let out : Array[(String, Array[@api.Field])] = []
for r in spec.routes {
if r.req == "" {
continue
}
let mut seen = false
for entry in out {
if entry.0 == r.req {
seen = true
}
}
if seen {
continue
}
for t in spec.types {
if t.name == r.req {
out.push((t.name, @api.flat_fields(t, spec.types)))
}
}
}
out
}
///|
/// The helpers `fields` need, in a stable order, each once.
fn needed_helpers(fields : Array[@api.Field]) -> Array[String] {
let out : Array[String] = []
for f in fields {
// A text field is taken as it arrived, so it needs no helper of its own.
if f.bind() == @api.Body || f.type_ == "String" || f.type_ == "String?" {
continue
}
match text_binder(base_type(f.type_)) {
Some(h) => if out.contains(h) == false { out.push(h) }
None => ()
}
}
out
}
///|
/// Whether any binder reads a value out of text, which is what
/// `moonbitlang/core/string` is imported for. A spec whose request fields are all
/// text or all in the body needs none of it, and an unused import fails
/// `moon check` in a generated project.
pub fn binders_parse_text(spec : @api.Spec) -> Bool {
for entry in bound_types(spec) {
if needed_helpers(entry.1).length() > 0 {
return true
}
}
false
}
///|
/// The body of one request-field helper, by name.
fn helper_src(name : String) -> String {
let (ty, call) = match name {
"bind_text" => ("String", "")
"bind_int32" => ("Int32", "@string.parse_int(s)")
"bind_int64" => ("Int64", "@string.parse_int64(s)")
"bind_uint32" => ("UInt32", "@string.parse_uint(s)")
"bind_uint64" => ("UInt64", "@string.parse_uint64(s)")
"bind_double" => ("Double", "@string.parse_double(s)")
"bind_float" => ("Float", "Float::from_double(@string.parse_double(s))")
_ => ("Bool", "parse_bool(s)")
}
if name == "bind_text" {
return "///|\n/// A text field, taken as it arrived.\nfn bind_text(s : String) -> String? {\n Some(s)\n}\n\n"
}
if name == "bind_bool" {
return "///|\n/// A boolean field: what every request format spells it as.\nfn bind_bool(s : String) -> Bool? {\n match s {\n \"true\" | \"TRUE\" | \"1\" => Some(true)\n \"false\" | \"FALSE\" | \"0\" => Some(false)\n _ => None\n }\n}\n\n"
}
"///|\n/// A `" +
ty +
"` field, read from its text.\nfn " +
name +
"(s : String) -> " +
ty +
"? {\n try {\n Some(" +
call +
")\n } catch {\n _ => None\n }\n}\n\n"
}
///|
/// The statement that binds `f` from its place, for a field that is not in the body:
/// read the text, then read the type out of it. A field that may be absent keeps
/// `None` when the request left it out; one that may not is refused by name.
fn text_bind_stmt(f : @api.Field) -> String {
let place = bind_place(f.bind())
let key = f.json_name()
let slot = f.mbt_name()
let ty = f.type_
let absent = may_absent(f)
let source = reader(place, key)
let want = place + " \\\"" + key + "\\\""
if ty == "String" || ty == "String?" {
if absent {
return " let " +
slot +
" : String? = match " +
source +
" {\n Some(text) => Some(text)\n None => None\n }\n"
}
return " let " +
slot +
" : String = match " +
source +
" {\n Some(text) => text\n None => return Err(\"" +
want +
" is missing\")\n }\n"
}
match text_binder(base_type(ty)) {
None =>
if absent {
" let " + slot + " : " + ty + " = None\n"
} else {
" return Err(\"" +
want +
": " +
ty +
" has no text form; bind it from the body\")\n"
}
Some(binder) =>
if absent {
" let " +
slot +
" : " +
ty +
" = match " +
source +
" {\n Some(text) =>\n match " +
binder +
"(text) {\n Some(value) => Some(value)\n None => return Err(\"" +
want +
": want " +
base_type(ty) +
"\")\n }\n None => None\n }\n"
} else {
" let " +
slot +
" : " +
ty +
" = match " +
source +
" {\n Some(text) =>\n match " +
binder +
"(text) {\n Some(value) => value\n None => return Err(\"" +
want +
": want " +
base_type(ty) +
"\")\n }\n None => return Err(\"" +
want +
" is missing\")\n }\n"
}
}
}
///|
/// The expression that reads a field's text out of `place`.
fn reader(place : String, key : String) -> String {
let q = @text.quote(key)
match place {
"path" => "ctx.param(" + q + ")"
"form" => "ctx.query(" + q + ")"
"header" => "ctx.request.header(" + q + ")"
_ => "None"
}
}
///|
/// The statement that binds `f` out of the JSON body: the member the tag names,
/// read as the field's type. A field that may be absent stays `None` when the body
/// carries no such member.
fn body_bind_stmt(f : @api.Field) -> String {
let key = f.json_name()
let slot = f.mbt_name()
let ty = f.type_
let optional = may_absent(f)
let missing = if optional {
"None"
} else {
"return Err(\"body \\\"" + key + "\\\" is missing\")"
}
let bad = "return Err(\"body \\\"" + key + "\\\": want " + ty + "\")"
let read = if optional {
"try {\n Some(@json.from_json(value))\n } catch {\n _ => " +
bad +
"\n }"
} else {
"try {\n @json.from_json(value)\n } catch {\n _ => " +
bad +
"\n }"
}
" let " +
slot +
" : " +
ty +
" = match body {\n Some(Object(members)) =>\n match members.get(" +
@text.quote(key) +
") {\n Some(value) =>\n " +
read +
"\n None => " +
missing +
"\n }\n _ => " +
missing +
"\n }\n"
}
///|
/// `bind_` for one request type: read every field from the place its tag
/// names, then build the struct. A field bound from the body and one bound from the
/// path are read the same way the request puts them there, so a request whose body
/// and path disagree takes each value from where its tag says.
pub fn render_binder(name : String, fields : Array[@api.Field]) -> String {
let fn_name = "bind_" + @text.to_snake(name)
let mut cols = ""
for f in fields {
cols = if cols == "" { f.mbt_name() } else { cols + ", " + f.mbt_name() }
}
let from_body = fields.any(f => f.bind() == @api.Body)
let mut out = "///|\n/// Bind `" +
name +
"` from the request: `path`, `form` and `header` values from the places their\n/// tags name, everything else from the JSON body. `Err` names the first field that\n/// was missing or would not read.\npub fn " +
fn_name +
"(ctx : @moonapi.Context) -> Result[" +
name +
", String] {\n"
if from_body {
out = out + " let body = ctx.body_json()\n"
}
for f in fields {
out = out +
(if f.bind() == @api.Body {
body_bind_stmt(f)
} else {
text_bind_stmt(f)
})
}
out + " Ok({ " + cols + ", })\n}\n\n"
}
///|
/// Every binder `spec` needs, with the helpers they read through, as the
/// `internal/types` package's generated source tail. Empty when no route declares a
/// request type.
pub fn render_binders(spec : @api.Spec) -> String {
let types = bound_types(spec)
if types.length() == 0 {
return ""
}
let helpers : Array[String] = []
for entry in types {
for h in needed_helpers(entry.1) {
if helpers.contains(h) == false {
helpers.push(h)
}
}
}
let order = [
"bind_text", "bind_int32", "bind_int64", "bind_uint32", "bind_uint64", "bind_double",
"bind_float", "bind_bool",
]
let mut out = ""
for h in order {
if helpers.contains(h) {
out = out + helper_src(h)
}
}
for entry in types {
out = out + render_binder(entry.0, entry.1)
}
out
}