// What a tag's constraints turn into. `optional`, `default=`, `options=` and
// `range=` describe a value the service has not seen yet, so half of each lands in
// the OpenAPI document (`required`, `default`, `enum`, `minimum`/`maximum`) and the
// other half has to be code the service runs: `with_defaults` fills in what the
// request left out, `check` refuses what it got wrong. Both are emitted beside the
// struct they belong to, so `internal/types` carries the rules the spec declared
// instead of leaving them in a document nobody executes.
///|
/// A MoonBit literal of type `type_` spelling `raw`, or `None` when the type has no
/// literal form a generated comparison could use (an `Array`, a `Map`, a nested
/// message).
fn lit(type_ : String, raw : String) -> String? {
match type_ {
"String" => Some(quote(raw))
"Bool" => if raw == "true" || raw == "false" { Some(raw) } else { None }
"Int" => Some(raw)
"Int64" => Some(raw + "L")
"UInt" => Some(raw + "U")
"UInt64" => Some(raw + "UL")
"Double" => Some(if index_of(raw, '.', 0) < 0 { raw + ".0" } else { raw })
"Float" =>
Some(
"(" +
(if index_of(raw, '.', 0) < 0 { raw + ".0" } else { raw }) +
" : Float)",
)
_ => None
}
}
///|
/// `raw` as a JSON number, or as a string when it does not read as one — a tag
/// option is text until something asks it to be a number.
fn number_json(raw : String) -> Json {
let d = @string.parse_double(raw) catch { _ => return raw.to_json() }
d.to_json()
}
///|
/// Whether `type_` is one a `range=` can bound.
fn is_numeric(type_ : String) -> Bool {
match type_ {
"Int" | "Int64" | "UInt" | "UInt64" | "Double" | "Float" => true
_ => false
}
}
///|
/// The value a member of type `type_` decodes to when the request left it out —
/// what a `default=` replaces. `None` for a type with no single such value.
fn zero_lit(type_ : String) -> String? {
match type_ {
"String" => Some("\"\"")
"Bool" => Some("false")
_ => if is_numeric(type_) { lit(type_, "0") } else { None }
}
}
///|
/// The `if …` line refusing a value outside a field's `range=`, or "" when the
/// field's type has no bounds to compare against. An open end contributes nothing,
/// so `range=[1:]` only refuses what is below 1.
fn range_check(f : Field, r : Range) -> String {
if is_numeric(f.type_) == false {
return ""
}
let tests : Array[String] = []
let field = "self." + f.mbt_name()
match lit(f.type_, r.lo) {
Some(v) =>
if r.lo != "" {
tests.push(field + (if r.lo_inc { " < " } else { " <= " }) + v)
}
None => ()
}
match lit(f.type_, r.hi) {
Some(v) =>
if r.hi != "" {
tests.push(field + (if r.hi_inc { " > " } else { " >= " }) + v)
}
None => ()
}
if tests.length() == 0 {
return ""
}
let mut cond = ""
for t in tests {
cond = if cond == "" { t } else { cond + " || " + t }
}
let span = (if r.lo_inc { "[" } else { "(" }) +
r.lo +
":" +
r.hi +
(if r.hi_inc { "]" } else { ")" })
" if " +
cond +
" {\n return Some(" +
quote(f.json_name() + " is out of range " + span) +
")\n }\n"
}
///|
/// The `if …` line refusing a value the field's `options=` does not list, or "" when
/// the field's type has no literal to compare against.
fn options_check(f : Field, allowed : Array[String]) -> String {
let tests : Array[String] = []
for one in allowed {
match lit(f.type_, one) {
Some(v) => tests.push("self." + f.mbt_name() + " != " + v)
None => return ""
}
}
if tests.length() == 0 {
return ""
}
let mut cond = ""
let mut listed = ""
for t in tests {
cond = if cond == "" { t } else { cond + " && " + t }
}
for one in allowed {
listed = if listed == "" { one } else { listed + "|" + one }
}
" if " +
cond +
" {\n return Some(" +
quote(f.json_name() + " is not one of " + listed) +
")\n }\n"
}
///|
/// The checks `fields` ask for, in declaration order: a required string that came
/// through empty, then any `options=` and `range=` they declared.
fn check_body(fields : Array[Field]) -> String {
let mut out = ""
for f in fields {
if f.optional() == false && f.type_ == "String" {
out = out +
" if self." +
f.mbt_name() +
" == \"\" {\n return Some(" +
quote(f.json_name() + " is required") +
")\n }\n"
}
let allowed = f.options()
if allowed.length() > 0 {
out = out + options_check(f, allowed)
}
match f.range() {
Some(r) => out = out + range_check(f, r)
None => ()
}
}
out
}
///|
/// The `default=` substitutions `fields` ask for, as `field: if … ` pairs.
fn default_pairs(fields : Array[Field]) -> Array[String] {
let out : Array[String] = []
for f in fields {
let raw = match f.default_() {
Some(v) => v
None => continue
}
let value = match lit(f.type_, raw) {
Some(v) => v
None => continue
}
let zero = match zero_lit(f.type_) {
Some(z) => z
None => continue
}
let name = f.mbt_name()
out.push(
" " +
name +
": if self." +
name +
" == " +
zero +
" { " +
value +
" } else { self." +
name +
" },",
)
}
out
}
///|
/// The constraint code a `type` block's tags ask for, to sit beside the struct:
/// `with_defaults` when any field declared a `default=`, and `check` when any
/// declared `options=`, a `range=`, or is a required string. A block whose tags ask
/// for neither gets neither, so a spec that constrains nothing generates nothing.
fn render_checks(name : String, fields : Array[Field]) -> String {
let mut out = ""
let pairs = default_pairs(fields)
if pairs.length() > 0 {
out = out +
"///|\n/// `" +
name +
"` with every `default=` the spec declared filled in where the value is still\n/// the one an absent member decodes to. Run it before `check`, which is what makes\n/// a defaulted field pass the constraints its tag also declared.\npub fn " +
name +
"::with_defaults(self : " +
name +
") -> " +
name +
" {\n {\n ..self,\n" +
join_lines(pairs) +
"\n }\n}\n\n"
}
let body = check_body(fields)
if body != "" {
out = out +
"///|\n/// Check `" +
name +
"` against the constraints its `.api` tags declared: a required string that\n/// came through empty, an `options=` value not on the list, a number outside its\n/// `range=`. The first field that fails, or `None` when every one holds.\npub fn " +
name +
"::check(self : " +
name +
") -> String? {\n" +
body +
" None\n}\n\n"
}
out
}