///|
/// A spec path as the route template moonapi reads: each `:name` segment of
/// the spec becomes `{name}`.
pub fn route_path(path : String) -> String {
path
.split("/")
.map(seg => {
if seg.has_prefix(":") {
"{" + seg.exact_view(start=1).to_owned() + "}"
} else {
seg.to_owned()
}
})
.collect()
.join("/")
}
///|
/// A group's annotations spelled the way the spec wrote them, e.g. `group: user,
/// prefix: /api/v1, middleware: Log`. Empty for a group that carries nothing.
pub fn group_note(g : @api.Group) -> String {
let parts : Array[String] = []
if g.name != "" {
parts.push("group: " + g.name)
}
if g.prefix != "" {
parts.push("prefix: " + g.prefix)
}
if g.jwt != "" {
parts.push("jwt: " + g.jwt)
}
if g.middleware.length() > 0 {
let mut ms = ""
for m in g.middleware {
ms = if ms == "" { m } else { ms + "," + m }
}
parts.push("middleware: " + ms)
}
if g.max_bytes > 0L {
parts.push("maxBytes: " + g.max_bytes.to_string())
}
if g.timeout != "" {
parts.push("timeout: " + g.timeout)
}
if g.signature {
parts.push("signature: true")
}
for kv in g.extra {
parts.push(kv.0 + ": " + kv.1)
}
let mut out = ""
for p in parts {
out = if out == "" { p } else { out + ", " + p }
}
out
}
///|
/// Generate compilable moonapi scaffolding from a spec: a MoonBit struct for
/// every `type` block, a `build_app` that wires every route to its handler, plus
/// a stub for each handler. The routing part depends only on `moonbitstack/moonapi`
/// and `moonbitstack/moonasgi`; the emitted schemas are dependency-free.
///
/// Pass `template` to render the spec through the runtime template engine (see
/// `generate_with`) instead of the built-in generator; omit it for the default
/// scaffold.
pub fn generate(
spec : @api.Spec,
template? : String,
) -> String raise @template.Refused {
match template {
Some(src) => generate_with(spec, src)
None => generate_builtin(spec)
}
}
///|
/// A MoonBit struct for every `type` block, in the order the spec declared them,
/// each followed by whatever its tags asked to be checked (see `render_checks`).
pub fn render_structs(
types : Array[@api.TypeDef],
derive_json~ : Bool = false,
) -> String {
let mut out = ""
for t in types {
let fields = @api.flat_fields(t, types)
out = out +
"///|\n/// `" +
t.name +
"` schema (generated from the `.api` `type` block).\n" +
(if derive_json { "#derive(ToJson, FromJson)\n" } else { "" }) +
"pub(all) struct " +
t.name +
" {\n"
for f in fields {
out = out + " " + f.mbt_name() + " : " + f.type_ + "\n"
}
out = out + "}\n\n"
if derive_json {
// The derive puts the impl in the type's own package, which promotes its
// methods onto the type; saying so explicitly is what keeps that a choice
// rather than a warning.
out = out +
"///|\n/// `" +
t.name +
"` as JSON, through the derived impls.\npub extend " +
t.name +
" with @json.ToJson::{to_json}\n\npub extend " +
t.name +
" with @json.FromJson::{from_json}\n\n"
}
out = out + render_checks(t.name, fields)
}
out
}
///|
pub fn generate_builtin(spec : @api.Spec) -> String {
let mut out = "// Code generated by moonctl. DO NOT EDIT.\n\n" +
render_structs(spec.types)
out = out +
"///|\n/// Build the " +
spec.service +
" application with its routes wired to handlers.\n"
out = out + "pub fn build_app() -> @moonapi.App {\n"
out = out + " let app = @moonapi.App::new()\n"
// A group's jwt / middleware / limits have no moonapi call to emit them into, so
// the block they govern is marked with the annotations the spec asked for.
let mut note = ""
for r in spec.routes {
let mark = match r.group {
Some(g) => group_note(g)
None => ""
}
if mark != note {
out = out +
(if mark == "" {
" // (ungrouped)\n"
} else {
" // @server " + mark + "\n"
})
note = mark
}
// moonapi's `ApiHandler` is a *raising* fn-type, and under moonc 0.10.5 a pure
// named fn no longer coerces to one. Register each handler through an arrow
// closure — a closure infers the raise effect, so a stub that never raises and a
// filled-in handler that raises an `HttpException` are both accepted unchanged.
let handler = "ctx => " + r.handler + "(ctx)"
let head = " app." +
r.verb +
"(" +
@text.quote(route_path(r.path)) +
", " +
handler
out = out +
(if r.summary != "" {
head + ", summary=" + @text.quote(r.summary) + ")\n"
} else {
head + ")\n"
})
}
out = out + " app\n}\n\n"
for r in spec.routes {
let where_ = match r.group {
Some(g) if g.name != "" => " of group " + g.name
_ => ""
}
out = out +
"///|\n/// `" +
r.verb +
" " +
r.path +
"`" +
where_ +
".\npub fn " +
r.handler +
"(_ctx : @moonapi.Context) -> @moonasgi.Response {\n @moonapi.text(200, " +
@text.quote("TODO: " + r.handler) +
")\n}\n\n"
}
out
}