// The plugin protocol: mctl hands a parsed spec to an external program and writes
// back the files it returns. goctl's `api plugin` does the same — it serialises the
// parsed API to JSON, runs the plugin as a subprocess, and the plugin emits code.
// Here the contract is symmetric and explicit: the request is the `Spec` as JSON on
// the plugin's stdin, the reply is a JSON list of `{path, content}` files on its
// stdout, which mctl writes out. The wire shapes and their (de)serialisers live here,
// in pure all-backend code; only the subprocess spawn (in the `mctl` binary) is
// native.
///|
/// One file a plugin asks mctl to write: a destination `path` (relative to the
/// output directory) and its full `content`.
pub(all) struct GenFile {
path : String
content : String
}
///|
/// Serialise a `Spec` into the plugin request JSON. The shape mirrors the `Spec`
/// itself — `service`, `routes` (`verb`/`path`/`handler`/`summary`), and `types`
/// (`name` plus `fields` of `name`/`type`) — so a plugin in any language parses it
/// with an ordinary JSON reader. `type_` is written as `type`, the name a caller
/// expects.
pub fn spec_to_json(spec : Spec) -> Json {
let routes : Array[Json] = []
for r in spec.routes {
let m : Map[String, Json] = Map([
("verb", r.verb.to_json()),
("path", r.path.to_json()),
("handler", r.handler.to_json()),
("summary", r.summary.to_json()),
])
routes.push(m.to_json())
}
let types : Array[Json] = []
for t in spec.types {
let fields : Array[Json] = []
for f in t.fields {
let fm : Map[String, Json] = Map([
("name", f.name.to_json()),
("type", f.type_.to_json()),
])
fields.push(fm.to_json())
}
let tm : Map[String, Json] = Map([
("name", t.name.to_json()),
("fields", fields.to_json()),
])
types.push(tm.to_json())
}
let doc : Map[String, Json] = Map([
("service", spec.service.to_json()),
("routes", routes.to_json()),
("types", types.to_json()),
])
doc.to_json()
}
///|
/// The plugin request as a two-space-indented JSON string, ready to write to the
/// plugin's stdin.
pub fn plugin_request(spec : Spec) -> String {
spec_to_json(spec).stringify(indent=2)
}
///|
/// Reconstruct a `Spec` from the request JSON — the reader a plugin written in
/// MoonBit uses on its stdin. Missing or mistyped members degrade to empty rather
/// than raising, so a partial request still parses.
pub fn spec_from_json(j : Json) -> Spec {
let service = match j {
Object(m) =>
match m.get("service") {
Some(String(s)) => s
_ => "app"
}
_ => "app"
}
let routes : Array[Route] = []
let types : Array[TypeDef] = []
match j {
Object(m) => {
match m.get("routes") {
Some(Array(arr)) =>
for item in arr {
match item {
Object(rm) =>
routes.push({
verb: json_str(rm, "verb"),
path: json_str(rm, "path"),
handler: json_str(rm, "handler"),
summary: json_str(rm, "summary"),
})
_ => ()
}
}
_ => ()
}
match m.get("types") {
Some(Array(arr)) =>
for item in arr {
match item {
Object(tm) => {
let fields : Array[Field] = []
match tm.get("fields") {
Some(Array(fa)) =>
for f in fa {
match f {
Object(fm) =>
fields.push({
name: json_str(fm, "name"),
type_: json_str(fm, "type"),
})
_ => ()
}
}
_ => ()
}
types.push({ name: json_str(tm, "name"), fields })
}
_ => ()
}
}
_ => ()
}
}
_ => ()
}
{ service, routes, types }
}
///|
/// Read a string member of a JSON object, or `""` if absent or non-string.
fn json_str(m : Map[String, Json], key : String) -> String {
match m.get(key) {
Some(String(s)) => s
_ => ""
}
}
///|
/// Parse a plugin's stdout into the files to write. Two shapes are accepted: a bare
/// array `[{path, content}, …]`, or an object `{"files": [ … ]}` (goctl-style
/// envelope). An element missing `path` or `content` is skipped. The list preserves
/// the plugin's order.
pub fn parse_gen_files(j : Json) -> Array[GenFile] {
let out : Array[GenFile] = []
let arr = match j {
Array(a) => a
Object(m) =>
match m.get("files") {
Some(Array(a)) => a
_ => []
}
_ => []
}
for item in arr {
match item {
Object(fm) => {
let path = json_str(fm, "path")
let content = json_str(fm, "content")
if path != "" {
out.push({ path, content })
}
}
_ => ()
}
}
out
}
///|
/// Serialise a list of generated files into the plugin-reply JSON — the writer a
/// MoonBit plugin uses on its stdout. Emits the `{"files": [...]}` envelope.
pub fn gen_files_to_json(files : Array[GenFile]) -> Json {
let arr : Array[Json] = []
for f in files {
let m : Map[String, Json] = Map([
("path", f.path.to_json()),
("content", f.content.to_json()),
])
arr.push(m.to_json())
}
let doc : Map[String, Json] = Map([("files", arr.to_json())])
doc.to_json()
}