///|
/// MoonBit model emitter for the Canonical Client IR.
///
/// This executable consumes only `ApiIr` parsed from canonical JSON. It does
/// not read OpenAPI, does not reconstruct names, and does not reinterpret model
/// semantics.
///|
fn line(out : StringBuilder, text : String) -> Unit {
out.write_string(text)
out.write_char('\n')
}
///|
fn quoted(text : String) -> String {
text.escape()
}
///|
/// MoonBit reserved keywords.
let moonbit_keywords : Map[String, Bool] = {
let m : Map[String, Bool] = Map([])
for
kw in [
"as", "async", "await", "break", "catch", "const", "continue", "derive", "else",
"enum", "false", "fn", "for", "guard", "if", "impl", "import", "in", "init",
"is", "let", "loop", "match", "mut", "noraise", "not", "null", "op", "or",
"override", "package", "priv", "pub", "pure", "raise", "readonly", "ref", "return",
"self", "static", "struct", "suberror", "test", "trait", "true", "try", "type",
"var", "while", "with",
// generated helpers
"all", "client", "config", "from_json", "json", "new", "open", "to_json",
"to_wire",
] {
m[kw] = true
}
m
}
///|
fn escape_moonbit(name : String) -> String {
if moonbit_keywords.contains(name) {
name + "_"
} else {
name
}
}
///|
fn render_type(type_ref : TypeRef) -> String {
match type_ref.kind {
"scalar" =>
match type_ref.name {
Some(name) => name
None => abort("validated scalar type is missing its name")
}
"named" =>
match type_ref.name {
Some(name) => name
None => abort("validated named type is missing its name")
}
"array" =>
match type_ref.item {
Some(item) => "Array[" + render_type(item) + "]"
None => abort("validated array type is missing its item")
}
_ => abort("validated type has an unsupported kind")
}
}
///|
fn contains_int64(type_ref : TypeRef) -> Bool {
match type_ref.kind {
"scalar" =>
match type_ref.name {
Some("Int64") => true
_ => false
}
"array" =>
match type_ref.item {
Some(item) => contains_int64(item)
None => abort("validated array type is missing its item")
}
_ => false
}
}
///|
fn field_moon_type(field : FieldIr) -> String {
let rendered = render_type(field.type_ref)
match field.presence {
"required" => rendered
"required_nullable" => rendered + "?"
"optional" => rendered + "?"
"optional_nullable" => "Presence[" + rendered + "]"
_ => abort("validated field has an unsupported presence")
}
}
///|
fn int64_codec_suffix(type_ref : TypeRef) -> String {
match type_ref.kind {
"scalar" =>
match type_ref.name {
Some("Int64") => "int64"
_ => abort("Int64 codec requested for a non-Int64 scalar")
}
"array" =>
match type_ref.item {
Some(item) => "array_" + int64_codec_suffix(item)
None => abort("validated array type is missing its item")
}
_ => abort("Int64 codec requested for a type without Int64")
}
}
///|
fn encode_expr(type_ref : TypeRef, value : String) -> String {
match type_ref.kind {
"scalar" =>
match type_ref.name {
Some("Json") => value
Some("Int64") => "__i64_json(" + value + ")"
_ => "@json.to_json(" + value + ")"
}
"named" => "@json.to_json(" + value + ")"
"array" =>
if contains_int64(type_ref) {
"__encode_" + int64_codec_suffix(type_ref) + "(" + value + ")"
} else {
"@json.to_json(" + value + ")"
}
_ => abort("validated type has an unsupported kind")
}
}
///|
fn decode_expr(type_ref : TypeRef, value : String, path : String) -> String {
match type_ref.kind {
"scalar" =>
match type_ref.name {
Some("Json") => value
Some("Int") => "__dec_int(" + value + ", " + path + ")"
Some("Int64") => "__dec_i64(" + value + ", " + path + ")"
Some("Double") => "__dec_dbl(" + value + ", " + path + ")"
_ => "@json.from_json(" + value + ", path=" + path + ")"
}
"named" => "@json.from_json(" + value + ", path=" + path + ")"
"array" =>
if contains_int64(type_ref) {
"__decode_" +
int64_codec_suffix(type_ref) +
"(" +
value +
", " +
path +
")"
} else {
"@json.from_json(" + value + ", path=" + path + ")"
}
_ => abort("validated type has an unsupported kind")
}
}
///|
fn collect_int64_helpers(
type_ref : TypeRef,
seen : Map[String, Bool],
helpers : Array[TypeRef],
) -> Unit {
match type_ref.kind {
"array" => {
let item = match type_ref.item {
Some(value) => value
None => abort("validated array type is missing its item")
}
if contains_int64(item) {
collect_int64_helpers(item, seen, helpers)
}
if contains_int64(type_ref) {
let suffix = int64_codec_suffix(type_ref)
if seen.get(suffix) is None {
seen[suffix] = true
helpers.push(type_ref)
}
}
}
_ => ()
}
}
///|
fn emit_int64_helpers(out : StringBuilder, api : ApiIr) -> Unit {
let uses_int64 = api_uses_int64(api)
if uses_int64 {
line(out, "///|")
line(out, "fn __dec_i64(json : Json, path : JPath) -> Int64 raise JErr {")
line(out, " if json is String(s) {")
line(out, " @string.parse_int64(s) catch {")
line(
out, " _ => raise __error(path, \"integer value is out of range for Int64\")",
)
line(out, " }")
line(out, " } else {")
line(out, " match json {")
line(
out, " Number(n, ..) if !n.is_nan() && !n.is_inf() && n == n.trunc() => {",
)
line(out, " let max_bound = 9223372036854775807L.to_double()")
line(out, " let min_bound = (-9223372036854775808L).to_double()")
line(out, " if n > max_bound || n < min_bound {")
line(
out, " raise __error(path, \"integer value is out of range for Int64\")",
)
line(out, " }")
line(out, " n.to_int64()")
line(out, " }")
line(
out, " _ => raise __error(path, \"expected an integer (JSON number)\")",
)
line(out, " }")
line(out, " }")
line(out, "}")
line(out, "")
line(out, "///|")
line(out, "fn __i64_json(value : Int64) -> Json {")
line(out, " Json::number(value.to_double(), repr=value.to_string())")
line(out, "}")
line(out, "")
}
let helpers : Array[TypeRef] = []
let seen : Map[String, Bool] = Map([])
for model in api.models {
match model {
EnumModel(_) => ()
StructModel(value) =>
for field in value.fields {
collect_int64_helpers(field.type_ref, seen, helpers)
}
}
}
for type_ref in helpers {
let suffix = int64_codec_suffix(type_ref)
let rendered = render_type(type_ref)
let item = match type_ref.item {
Some(value) => value
None => abort("validated array type is missing its item")
}
line(out, "///|")
line(out, "fn __encode_" + suffix + "(value : " + rendered + ") -> Json {")
line(out, " let out : Array[Json] = []")
line(out, " for item in value {")
line(out, " out.push(" + encode_expr(item, "item") + ")")
line(out, " }")
line(out, " Json::array(out)")
line(out, "}")
line(out, "")
line(out, "///|")
line(
out,
"fn __decode_" +
suffix +
"(json : Json, path : JPath) -> " +
rendered +
" raise JErr {",
)
line(out, " let items : Array[Json] = match json {")
line(out, " Array(values) => values")
line(
out, " _ => raise @json.JsonDecodeError((path, \"expected a JSON array\"))",
)
line(out, " }")
line(out, " let out : " + rendered + " = []")
line(out, " for index, value in items {")
line(
out,
" out.push(" +
decode_expr(item, "value", "path.add_index(index)") +
")",
)
line(out, " }")
line(out, " out")
line(out, "}")
line(out, "")
}
}
///|
fn emit_runtime_helpers(out : StringBuilder) -> Unit {
line(out, "///|")
line(out, "type JPath = @json.JsonPath")
line(out, "")
line(out, "///|")
line(out, "type JErr = @json.JsonDecodeError")
line(out, "")
line(out, "///|")
line(out, "fn __error(path : JPath, message : String) -> JErr {")
line(out, " @json.JsonDecodeError((path, message))")
line(out, "}")
line(out, "")
line(out, "///|")
line(out, "pub(all) enum Presence[T] {")
line(out, " Unset")
line(out, " Null")
line(out, " Value(T)")
line(out, "} derive(Eq, Debug)")
line(out, "")
line(out, "///|")
line(out, "fn __dec_int(json : Json, path : JPath) -> Int raise JErr {")
line(out, " match json {")
line(out, " Number(n, ..) if !n.is_nan() && !n.is_inf() => {")
line(out, " if n != n.trunc() {")
line(
out, " raise __error(path, \"expected an integer, found a fractional number\")",
)
line(out, " }")
line(out, " if n > 2147483647.0 || n < -2147483648.0 {")
line(
out, " raise __error(path, \"integer value is out of range for Int\")",
)
line(out, " }")
line(out, " n.to_int()")
line(out, " }")
line(
out, " _ => raise __error(path, \"expected an integer (JSON number)\")",
)
line(out, " }")
line(out, "}")
line(out, "")
line(out, "///|")
line(out, "fn __dec_dbl(json : Json, path : JPath) -> Double raise JErr {")
line(out, " match json {")
line(out, " Number(n, ..) if !n.is_nan() && !n.is_inf() => n")
line(out, " _ => raise __error(path, \"expected a finite JSON number\")")
line(out, " }")
line(out, "}")
line(out, "")
line(out, "///|")
line(
out, "fn __put(fields : Map[String, Json], key : String, value : Json) -> Unit {",
)
line(out, " fields[key] = value")
line(out, "}")
line(out, "")
}
///|
fn emit_enum(out : StringBuilder, model : EnumModelIr) -> Unit {
line(out, "///|")
line(out, "pub(all) enum " + model.name + " {")
for enum_member in model.members {
line(out, " " + enum_member.name)
}
line(out, "} derive(Eq, Debug)")
line(out, "")
line(out, "///|")
line(
out,
"pub fn " + model.name + "::to_wire(self : " + model.name + ") -> String {",
)
line(out, " match self {")
for enum_member in model.members {
line(out, " " + enum_member.name + " => " + quoted(enum_member.wire))
}
line(out, " }")
line(out, "}")
line(out, "")
line(out, "///|")
line(
out,
"pub impl ToJson for " +
model.name +
" with fn to_json(self : " +
model.name +
") -> Json {",
)
line(out, " Json::string(self.to_wire())")
line(out, "}")
line(out, "")
line(out, "///|")
line(
out,
"pub impl FromJson for " + model.name + " with fn from_json(json, path) {",
)
line(out, " let text : String = @json.from_json(json) catch {")
line(
out,
" _ => raise __error(path, \"expected a string for enum " +
model.name +
"\")",
)
line(out, " }")
line(out, " match text {")
for enum_member in model.members {
line(out, " " + quoted(enum_member.wire) + " => " + enum_member.name)
}
line(
out,
" _ => raise __error(path, \"unknown wire value for enum " +
model.name +
"\")",
)
line(out, " }")
line(out, "}")
line(out, "")
}
///|
fn emit_struct_declaration(out : StringBuilder, model : StructModelIr) -> Unit {
line(out, "///|")
line(out, "pub(all) struct " + model.name + " {")
for field in model.fields {
let mn = escape_moonbit(field.name)
line(out, " " + mn + " : " + field_moon_type(field))
}
match model.additional_properties_field {
Some(name) => {
let en = escape_moonbit(name)
line(out, " " + en + " : Map[String, Json]")
}
None => ()
}
line(out, "} derive(Eq, Debug)")
line(out, "")
}
///|
fn emit_struct_constructor(out : StringBuilder, model : StructModelIr) -> Unit {
line(out, "///|")
line(out, "pub fn " + model.name + "::new(")
for field in model.fields {
match field.presence {
"required" =>
line(
out,
" " +
escape_moonbit(field.name) +
" : " +
render_type(field.type_ref) +
",",
)
"required_nullable" =>
line(
out,
" " +
escape_moonbit(field.name) +
" : " +
render_type(field.type_ref) +
"?,",
)
"optional" =>
line(
out,
" " +
escape_moonbit(field.name) +
"? : " +
render_type(field.type_ref) +
",",
)
"optional_nullable" =>
line(
out,
" " +
field.name +
"? : Presence[" +
render_type(field.type_ref) +
"] = Unset,",
)
_ => abort("validated field has an unsupported presence")
}
}
match model.additional_properties_field {
Some(name) => line(out, " " + name + "? : Map[String, Json] = Map([]),")
None => ()
}
line(out, ") -> " + model.name + " {")
let names : Array[String] = []
for field in model.fields {
names.push(escape_moonbit(field.name))
}
match model.additional_properties_field {
Some(name) => names.push(escape_moonbit(name))
None => ()
}
if names.length() == 0 {
line(out, " { }")
} else {
let mut inline = "{ "
for index, name in names {
if index > 0 {
inline += ", "
}
inline += name
}
if names.length() == 1 {
inline += ","
}
inline += " }"
if inline.length() + 2 <= 80 {
line(out, " " + inline)
} else {
line(out, " {")
for name in names {
line(out, " " + name + ",")
}
line(out, " }")
}
}
line(out, "}")
line(out, "")
}
///|
fn emit_struct_encoder(out : StringBuilder, model : StructModelIr) -> Unit {
line(out, "///|")
line(
out,
"pub impl ToJson for " +
model.name +
" with fn to_json(self : " +
model.name +
") -> Json {",
)
line(out, " let __m : Map[String, Json] = Map([])")
for field in model.fields {
let wire = quoted(field.wire_name)
let mn = escape_moonbit(field.name)
let value = "self." + mn
match field.presence {
"required" =>
line(
out,
" __put(__m, " +
wire +
", " +
encode_expr(field.type_ref, value) +
")",
)
"required_nullable" => {
line(out, " match " + value + " {")
line(
out,
" Some(value) => __put(__m, " +
wire +
", " +
encode_expr(field.type_ref, "value") +
")",
)
line(out, " None => __put(__m, " + wire + ", Json::null())")
line(out, " }")
}
"optional" => {
line(out, " match " + value + " {")
line(
out,
" Some(value) => __put(__m, " +
wire +
", " +
encode_expr(field.type_ref, "value") +
")",
)
line(out, " None => ()")
line(out, " }")
}
"optional_nullable" => {
line(out, " match " + value + " {")
line(out, " Unset => ()")
line(out, " Null => __put(__m, " + wire + ", Json::null())")
line(
out,
" Value(value) => __put(__m, " +
wire +
", " +
encode_expr(field.type_ref, "value") +
")",
)
line(out, " }")
}
_ => abort("validated field has an unsupported presence")
}
}
match model.additional_properties_field {
Some(name) => {
let en = escape_moonbit(name)
line(out, " for key, value in self." + en + " {")
line(out, " if !__m.contains(key) {")
line(out, " __put(__m, key, value)")
line(out, " }")
line(out, " }")
}
None => ()
}
line(out, " Json::object(__m)")
line(out, "}")
line(out, "")
}
///|
fn emit_declared_map(out : StringBuilder, model : StructModelIr) -> Unit {
if model.fields.length() == 0 {
line(out, " let __d : Map[String, Bool] = Map([])")
} else {
line(out, " let __d : Map[String, Bool] = Map([")
for field in model.fields {
line(out, " (" + quoted(field.wire_name) + ", true),")
}
line(out, " ])")
}
}
///|
fn emit_field_decoder(
out : StringBuilder,
field : FieldIr,
index : Int,
) -> Unit {
let field_name = "__f" + index.to_string()
let path_name = "__p" + index.to_string()
let value_name = "__v" + index.to_string()
let wire = quoted(field.wire_name)
line(out, " let " + path_name + " = path.add_key(" + wire + ")")
line(out, " let " + value_name + " = __m.get(" + wire + ")")
line(out, " let " + field_name + " = match " + value_name + " {")
match field.presence {
"required" => {
line(
out,
" Some(Null) => raise __error(" +
path_name +
", \"required property must not be null\")",
)
line(
out,
" Some(value) => " + decode_expr(field.type_ref, "value", path_name),
)
line(
out,
" None => raise __error(" +
path_name +
", \"missing required property\")",
)
}
"required_nullable" => {
line(out, " Some(Null) => None")
line(
out,
" Some(value) => Some(" +
decode_expr(field.type_ref, "value", path_name) +
")",
)
line(
out,
" None => raise __error(" +
path_name +
", \"missing required property\")",
)
}
"optional" => {
line(
out,
" Some(Null) => raise __error(" +
path_name +
", \"optional property must not be null\")",
)
line(
out,
" Some(value) => Some(" +
decode_expr(field.type_ref, "value", path_name) +
")",
)
line(out, " None => None")
}
"optional_nullable" => {
line(out, " Some(Null) => Null")
line(
out,
" Some(value) => Value(" +
decode_expr(field.type_ref, "value", path_name) +
")",
)
line(out, " None => Unset")
}
_ => abort("validated field has an unsupported presence")
}
line(out, " }")
}
///|
fn emit_record_value(
out : StringBuilder,
model : StructModelIr,
additional_name : String?,
) -> Unit {
let names : Array[String] = []
for index, field in model.fields {
let mn = escape_moonbit(field.name)
names.push(mn + ": __f" + index.to_string())
}
match additional_name {
Some(name) => {
let en = escape_moonbit(name)
names.push(en + ": __x")
}
None => ()
}
if names.length() == 0 {
line(out, " " + "{ }")
} else {
let mut inline = "{ "
for index, name in names {
if index > 0 {
inline += ", "
}
inline += name
}
inline += " }"
if inline.length() + 2 <= 80 {
line(out, " " + inline)
} else {
line(out, " {")
for name in names {
line(out, " " + name + ",")
}
line(out, " }")
}
}
}
///|
fn emit_struct_decoder(out : StringBuilder, model : StructModelIr) -> Unit {
line(out, "///|")
line(
out,
"pub impl FromJson for " + model.name + " with fn from_json(json, path) {",
)
line(out, " let __m : Map[String, Json] = match json {")
line(out, " Object(fields) => fields")
line(out, " _ => raise __error(path, \"expected a JSON object\")")
line(out, " }")
emit_declared_map(out, model)
for index, field in model.fields {
emit_field_decoder(out, field, index)
}
match model.additional_properties_field {
Some(name) => {
line(out, " let __x : Map[String, Json] = Map([])")
line(out, " for key, value in __m {")
line(out, " if !__d.contains(key) {")
line(out, " __x[key] = value")
line(out, " }")
line(out, " }")
emit_record_value(out, model, Some(name))
}
None => {
line(out, " for key, _ in __m {")
line(out, " if !__d.contains(key) {")
line(out, " raise __error(path.add_key(key), \"unknown property\")")
line(out, " }")
line(out, " }")
emit_record_value(out, model, None)
}
}
line(out, "}")
line(out, "")
}
///|
fn emit_struct(out : StringBuilder, model : StructModelIr) -> Unit {
emit_struct_declaration(out, model)
if model.fields.length() == 0 && model.additional_properties_field is None {
return
}
emit_struct_constructor(out, model)
emit_struct_encoder(out, model)
emit_struct_decoder(out, model)
}
///|
fn emit_models(api : ApiIr) -> String {
let out = StringBuilder()
line(out, "///|")
line(out, "/// Generated model types and JSON codecs. Do not edit by hand.")
line(out, "")
emit_runtime_helpers(out)
emit_int64_helpers(out, api)
for model in api.models {
match model {
EnumModel(value) => emit_enum(out, value)
StructModel(value) => emit_struct(out, value)
}
}
out.to_string().trim_end(chars="\n").to_owned() + "\n"
}
///|
fn module_file(module_name : String, has_operations : Bool) -> String {
let mut result = "name = " +
quoted(module_name) +
"\n" +
"\n" +
"version = \"0.1.0\"\n"
if has_operations {
// Generated operations reach the network through the runtime transport,
// which is built on moonbitlang/async/http.
result += "\nimport {\n \"moonbitlang/async@0.20.2\",\n}\n"
}
result + "\npreferred_target = \"native\"\n"
}
///|
fn path_join(directory : String, file : String) -> String {
if directory.has_suffix("/") || directory.has_suffix("\\") {
directory + file
} else {
directory + "/" + file
}
}
// ---------------------------------------------------------------------------
// String helpers
// ---------------------------------------------------------------------------
///|
fn join_strings(parts : Array[String], sep : String) -> String {
let out = StringBuilder()
for i, part in parts {
if i > 0 {
out.write_string(sep)
}
out.write_string(part)
}
out.to_string()
}
// ---------------------------------------------------------------------------
// Operation codegen helpers
// ---------------------------------------------------------------------------
///|
fn pascal_from_snake(snake : String) -> String {
let out = StringBuilder()
let mut upper_next = true
for ch in snake {
if ch == '_' {
upper_next = true
} else if upper_next && ch >= 'a' && ch <= 'z' {
out.write_char(Int::unsafe_to_char(ch.to_int() - 32))
upper_next = false
} else {
out.write_char(ch)
upper_next = false
}
}
out.to_string()
}
///|
fn response_enum_name(op : OperationIr) -> String {
pascal_from_snake(op.fn_name) + "Response"
}
///|
fn success_status_list(op : OperationIr) -> Array[Int] {
let out : Array[Int] = []
for r in op.success_responses {
out.push(r.status)
}
if out.length() == 0 {
out.push(200)
}
out
}
///|
fn int_list_to_moon(ints : Array[Int]) -> String {
let parts : Array[String] = []
for n in ints {
parts.push(n.to_string())
}
"[" + join_strings(parts, ", ") + "]"
}
///|
fn string_array_to_moon(strs : Array[String]) -> String {
let parts : Array[String] = []
for s in strs {
parts.push(quoted(s))
}
"[" + join_strings(parts, ", ") + "]"
}
// ---------------------------------------------------------------------------
// Path expression builder
// ---------------------------------------------------------------------------
///|
fn build_path_expr(op : OperationIr) -> String {
let path = op.path
let parts : Array[String] = []
let mut buf = StringBuilder()
let mut in_brace = false
let mut brace_buf = StringBuilder()
for ch in path {
if ch == '{' {
let lit = buf.to_string()
if lit.length() > 0 {
parts.push(quoted(lit))
buf = StringBuilder()
}
in_brace = true
brace_buf = StringBuilder()
} else if ch == '}' {
in_brace = false
let wire_name = brace_buf.to_string()
let mut found = ""
for p in op.parameters {
if p.wire_name == wire_name {
found = escape_moonbit(p.name)
}
}
if found.length() > 0 {
parts.push("encode_path_value(" + found + ".to_string())")
} else {
parts.push(quoted("{" + wire_name + "}"))
}
} else if in_brace {
brace_buf.write_char(ch)
} else {
buf.write_char(ch)
}
}
let lit = buf.to_string()
if lit.length() > 0 {
parts.push(quoted(lit))
}
if parts.length() == 0 {
quoted("")
} else if parts.length() == 1 {
parts[0]
} else {
join_strings(parts, " + ")
}
}
// ---------------------------------------------------------------------------
// Operation emission
// ---------------------------------------------------------------------------
///|
fn variant_name(v : ResponseVariant) -> String {
"S" + v.status.to_string()
}
///|
fn emit_operation(out : StringBuilder, op : OperationIr) -> Unit {
line(out, "///|")
line(out, "/// `" + op.http_method + " " + op.path + "`")
line(out, "pub async fn Client::" + op.fn_name + "(")
line(out, " self : Client,")
let has_body = match op.request_body {
Some(body) =>
match body.type_ref {
Some(_) => true
None => false
}
None => false
}
let body_name = match op.body_name {
Some(name) => name
None => "body"
}
let param_lines : Array[String] = []
for p in op.parameters {
let mn = escape_moonbit(p.name)
match p.presence {
"required" =>
param_lines.push(" " + mn + " : " + render_type(p.type_ref))
"required_nullable" =>
param_lines.push(" " + mn + " : " + render_type(p.type_ref) + "?")
"optional" =>
param_lines.push(" " + mn + "? : " + render_type(p.type_ref))
"optional_nullable" =>
param_lines.push(
" " + mn + "? : Presence[" + render_type(p.type_ref) + "] = Unset",
)
_ => abort("validated parameter has an unsupported presence")
}
}
if has_body {
match op.request_body {
Some(body) =>
match body.type_ref {
Some(tr) => {
let btype = render_type(tr)
if body.required {
param_lines.push(" " + body_name + " : " + btype)
} else {
param_lines.push(" " + body_name + "? : " + btype)
}
}
None => ()
}
None => ()
}
}
for pline in param_lines {
line(out, pline + ",")
}
let ret_type = match op.response_strategy {
SingleResult(tr) => render_type(tr)
UnitResult => "Unit"
NoContent => "Unit"
ResponseEnum(_) => response_enum_name(op)
UnsupportedMediaType(mt) =>
abort("unsupported media type in codegen: " + mt)
}
line(out, ") -> " + ret_type + " raise SdkError {")
if op.security.length() > 0 {
line(
out,
" let headers = auth_headers(self.config, " +
string_array_to_moon(op.security) +
", operation_id=" +
quoted(op.operation_id) +
")",
)
} else {
line(out, " let headers : Map[String, String] = Map([])")
}
for p in op.parameters {
if p.location is HeaderLoc {
let mn = escape_moonbit(p.name)
let wire = quoted(p.wire_name)
match p.presence {
"required" => line(out, " headers[" + wire + "] = " + mn)
"required_nullable" => {
line(out, " match " + mn + " {")
line(out, " Some(v) => headers[" + wire + "] = v")
line(out, " None => ()")
line(out, " }")
}
"optional" => {
line(out, " match " + mn + " {")
line(out, " Some(v) => headers[" + wire + "] = v")
line(out, " None => ()")
line(out, " }")
}
"optional_nullable" => {
line(out, " match " + mn + " {")
line(out, " Unset => ()")
line(out, " Null => ()")
line(out, " Value(v) => headers[" + wire + "] = v")
line(out, " }")
}
_ => abort("validated parameter has an unsupported presence")
}
}
}
if has_body {
match op.request_body {
Some(body) =>
match body.media_type {
Some(mt) =>
line(
out,
" headers[" + quoted("Content-Type") + "] = " + quoted(mt),
)
None => ()
}
None => ()
}
}
let has_query = {
let mut found = false
for p in op.parameters {
if p.location is QueryLoc {
found = true
}
}
found
}
if has_query {
line(out, " let query : Array[(String, String)] = []")
for p in op.parameters {
if p.location is QueryLoc {
let mn = escape_moonbit(p.name)
let wire = quoted(p.wire_name)
match p.presence {
"required" =>
line(out, " query.push((" + wire + ", " + mn + ".to_string()))")
"required_nullable" => {
line(out, " match " + mn + " {")
line(
out,
" Some(v) => query.push((" + wire + ", v.to_string()))",
)
line(out, " None => ()")
line(out, " }")
}
"optional" => {
line(out, " match " + mn + " {")
line(
out,
" Some(v) => query.push((" + wire + ", v.to_string()))",
)
line(out, " None => ()")
line(out, " }")
}
"optional_nullable" => {
line(out, " match " + mn + " {")
line(out, " Unset => ()")
line(out, " Null => ()")
line(
out,
" Value(v) => query.push((" + wire + ", v.to_string()))",
)
line(out, " }")
}
_ => abort("validated parameter has an unsupported presence")
}
}
}
}
if op.security.length() > 0 {
if !has_query {
line(out, " let query : Array[(String, String)] = []")
}
line(
out,
" for pair in auth_query(self.config, " +
string_array_to_moon(op.security) +
", operation_id=" +
quoted(op.operation_id) +
") {",
)
line(out, " query.push(pair)")
line(out, " }")
}
let path_expr = build_path_expr(op)
let http_method_str = quoted(op.http_method)
// Determine if body is optional
let sends_query = has_query || op.security.length() > 0
let body_optional : Bool = if has_body {
match op.request_body {
Some(body) => !body.required
None => false
}
} else {
false
}
if body_optional {
line(out, " let request = match " + body_name + " {")
let mut with_args = http_method_str + ", " + path_expr
if sends_query {
with_args += ", query~"
}
with_args += ", headers~"
line(
out,
" Some(v) => Request::new(" + with_args + ", body=encode_json(v))",
)
line(out, " None => Request::new(" + with_args + ")")
line(out, " }")
} else {
let mut req_str = " let request = Request::new(" +
http_method_str +
", " +
path_expr
if sends_query {
req_str += ", query~"
}
req_str += ", headers~"
if has_body {
match op.request_body {
Some(body) =>
match body.type_ref {
Some(_) =>
if body.required {
req_str += ", body=encode_json(" + body_name + ")"
}
None => ()
}
None => ()
}
}
req_str += ")"
line(out, req_str)
}
line(
out,
" let response = self.send_request(request, " +
quoted(op.operation_id) +
")",
)
let statuses = success_status_list(op)
line(
out,
" expect_status(response, " +
int_list_to_moon(statuses) +
", operation_id=" +
quoted(op.operation_id) +
")",
)
match op.response_strategy {
SingleResult(tr) => {
let tname = render_type(tr)
match tr.kind {
"scalar" =>
match tr.name {
Some("Json") => {
line(out, " @json.parse(response.body) catch {")
line(
out,
" _ => raise Decode(" +
quoted(op.operation_id) +
", \"failed to parse JSON response\")",
)
line(out, " }")
}
Some(_) =>
line(
out,
" (decode_json(response, operation_id=" +
quoted(op.operation_id) +
") : " +
tname +
")",
)
None => abort("validated type is missing its name")
}
"named" =>
line(
out,
" (decode_json(response, operation_id=" +
quoted(op.operation_id) +
") : " +
tname +
")",
)
"array" =>
line(
out,
" (decode_json(response, operation_id=" +
quoted(op.operation_id) +
") : " +
tname +
")",
)
_ => abort("validated type has an unsupported kind")
}
}
UnitResult => line(out, " ()")
NoContent => line(out, " ()")
ResponseEnum(variants) => {
line(out, " match response.status {")
for v in variants {
line(out, " " + v.status.to_string() + " => {")
let tname = render_type(v.type_ref)
line(
out,
" " + response_enum_name(op) + "::" + variant_name(v) + "(",
)
line(
out,
" (decode_json(response, operation_id=" +
quoted(op.operation_id) +
") : " +
tname +
"),",
)
line(out, " )")
line(out, " }")
}
line(
out,
" _ => raise Http(" +
quoted(op.operation_id) +
", response.status, response.headers, response.body)",
)
line(out, " }")
}
UnsupportedMediaType(mt) =>
abort("unsupported media type in codegen: " + mt)
}
line(out, "}")
line(out, "")
}
///|
fn emit_response_enums(out : StringBuilder, api : ApiIr) -> Unit {
for op in api.operations {
match op.response_strategy {
ResponseEnum(variants) => {
let ename = response_enum_name(op)
line(out, "///|")
line(out, "pub enum " + ename + " {")
for v in variants {
line(
out,
" " + variant_name(v) + "(" + render_type(v.type_ref) + ")",
)
}
line(out, "} derive(Debug)")
line(out, "")
}
_ => ()
}
}
}
///|
fn emit_client(api : ApiIr) -> String {
let out = StringBuilder()
line(out, "///|")
line(out, "/// Generated typed API client. Do not edit by hand.")
line(out, "")
emit_response_enums(out, api)
line(out, "///|")
line(out, "pub struct Client {")
line(out, " config : Config")
line(out, " capture : CaptureTransport?")
line(out, "} derive(Debug)")
line(out, "")
line(out, "///|")
line(out, "/// Create a client.")
line(out, "///")
line(
out, "/// Without `capture` every operation performs real HTTP through the",
)
line(
out, "/// runtime transport. Supplying a `capture` transport keeps the same",
)
line(out, "/// generated methods but replays an in-memory response instead.")
line(out, "pub fn Client::new(")
line(out, " base_url? : String = \"\",")
line(out, " bearer_token? : String,")
line(out, " basic_username? : String,")
line(out, " basic_password? : String,")
line(out, " api_key_name? : String,")
line(out, " api_key_value? : String,")
line(out, " api_key_location? : String,")
line(out, " capture? : CaptureTransport,")
line(out, ") -> Client {")
line(
out, " let config = Config::new(base_url, bearer_token?, basic_username?, basic_password?, api_key_name?, api_key_value?, api_key_location?)",
)
line(out, " { config, capture }")
line(out, "}")
line(out, "")
line(out, "///|")
line(out, "/// The single place generated operations obtain a response.")
line(out, "async fn Client::send_request(")
line(out, " self : Client,")
line(out, " request : Request,")
line(out, " operation_id : String,")
line(out, ") -> Response raise SdkError {")
line(out, " match self.capture {")
line(out, " Some(transport) => transport.send(request)")
line(out, " None => transmit(self.config, request, operation_id~)")
line(out, " }")
line(out, "}")
line(out, "")
for op in api.operations {
emit_operation(out, op)
}
out.to_string().trim_end(chars="\n").to_owned() + "\n"
}
// ---------------------------------------------------------------------------
// Updated package/main functions
// ---------------------------------------------------------------------------
///|
fn api_uses_int64(api : ApiIr) -> Bool {
for model in api.models {
match model {
EnumModel(_) => ()
StructModel(value) =>
for field in value.fields {
if contains_int64(field.type_ref) {
return true
}
}
}
}
for op in api.operations {
for p in op.parameters {
if contains_int64(p.type_ref) {
return true
}
}
match op.request_body {
Some(body) =>
match body.type_ref {
Some(tr) => if contains_int64(tr) { return true }
None => ()
}
None => ()
}
match op.response_strategy {
SingleResult(tr) => if contains_int64(tr) { return true }
ResponseEnum(variants) =>
for v in variants {
if contains_int64(v.type_ref) {
return true
}
}
_ => ()
}
}
false
}
///|
fn api_has_operations(api : ApiIr) -> Bool {
api.operations.length() > 0
}
///|
fn package_file(needs_string : Bool, has_operations : Bool) -> String {
let imports : Array[String] = []
imports.push("\"moonbitlang/core/json\"")
if has_operations {
imports.push("\"moonbitlang/core/encoding/utf8\"")
imports.push("\"moonbitlang/async/http\"")
}
if needs_string {
imports.push("\"moonbitlang/core/string\"")
}
let mut result = "import {\n"
for imp in imports {
result += " " + imp + ",\n"
}
result += "}\n"
// A test-only `moonbitlang/async` import is deliberately *not* emitted here:
// `async test` needs it, but a package without tests would then carry an
// unused import. Whoever adds tests adds the import.
result += "\nsupported_targets = \"+native\"\n"
result
}
///|
fn main raise {
let args = @env.args()
guard args.length() >= 3 else {
println("usage: oas2moon-codegen ")
@sys.exit(2)
return
}
let source = @fs.read_file_to_string(args[1])
let api = parse_api(@json.parse(source))
@fs.write_string_to_file(
path_join(args[2], "moon.mod"),
module_file(api.module_name, api_has_operations(api)),
)
@fs.write_string_to_file(
path_join(args[2], "moon.pkg"),
package_file(api_uses_int64(api), api_has_operations(api)),
)
@fs.write_string_to_file(path_join(args[2], "models.mbt"), emit_models(api))
if api_has_operations(api) {
@fs.write_string_to_file(path_join(args[2], "client.mbt"), emit_client(api))
}
}