///|
/// Convert a moon_zod schema into MoonBit type definitions.
///
/// Every `ObjectType` in the schema tree is emitted as a standalone `pub struct`.
/// Schema names are treated only as type-name hints; anonymous nested objects are
/// named from their owner and field path.
pub fn schema_to_moonbit_struct(schema : @core.Schema) -> String {
let root_name = schema_type_name(schema, "Root")
let defs : Array[StructDef] = []
collect_type_defs(schema, root_name, defs)
render_defs(defs, include_to_schema=false)
}
///|
/// Convert a moon_zod schema into MoonBit type definitions plus static
/// `Type::to_schema() -> @moon_zod.Schema` functions for each generated type.
pub fn schema_to_moonbit_struct_full(schema : @core.Schema) -> String {
let root_name = schema_type_name(schema, "Root")
let defs : Array[StructDef] = []
collect_type_defs(schema, root_name, defs)
render_defs(defs, include_to_schema=true)
}
///|
priv enum StructDef {
StructDef(String, @core.Schema)
EnumDef(String, Array[String])
}
///|
fn collect_type_defs(
schema : @core.Schema,
type_hint : String,
defs : Array[StructDef],
) -> Unit {
match schema.schema_type {
ObjectType(spec, _) => {
let type_name = schema_type_name(schema, type_hint)
for key, field_schema in spec {
collect_field_defs(field_schema, child_type_name(type_name, key), defs)
}
push_struct_def(defs, type_name, schema)
}
EnumType(values) =>
push_enum_def(defs, schema_type_name(schema, type_hint), values)
OptionalType(inner) | DefaultType(inner, _) | TransformType(inner, _) =>
collect_type_defs(inner, type_hint, defs)
PreprocessType(_, inner) => collect_type_defs(inner, type_hint, defs)
ArrayType(elem) => collect_type_defs(elem, type_hint + "Item", defs)
UnionType(schemas) | IntersectionType(schemas) =>
for item in schemas {
collect_type_defs(item, type_hint, defs)
}
_ => ()
}
}
///|
fn collect_field_defs(
schema : @core.Schema,
type_hint : String,
defs : Array[StructDef],
) -> Unit {
match schema.schema_type {
OptionalType(inner) | DefaultType(inner, _) | TransformType(inner, _) =>
collect_field_defs(inner, type_hint, defs)
PreprocessType(_, inner) => collect_field_defs(inner, type_hint, defs)
ArrayType(elem) => collect_field_defs(elem, type_hint + "Item", defs)
UnionType(schemas) | IntersectionType(schemas) =>
for item in schemas {
collect_field_defs(item, type_hint, defs)
}
ObjectType(_, _) | EnumType(_) => collect_type_defs(schema, type_hint, defs)
_ => ()
}
}
///|
fn push_struct_def(
defs : Array[StructDef],
type_name : String,
schema : @core.Schema,
) -> Unit {
if !def_exists(defs, type_name) {
defs.push(StructDef(type_name, schema))
}
}
///|
fn push_enum_def(
defs : Array[StructDef],
type_name : String,
values : Array[String],
) -> Unit {
if !def_exists(defs, type_name) {
defs.push(EnumDef(type_name, values))
}
}
///|
fn def_exists(defs : Array[StructDef], type_name : String) -> Bool {
for def in defs {
match def {
StructDef(name, _) | EnumDef(name, _) =>
if name == type_name {
return true
}
}
}
false
}
///|
fn render_defs(defs : Array[StructDef], include_to_schema~ : Bool) -> String {
if defs.is_empty() {
return "// TODO: schema does not contain any object or enum definitions"
}
let parts : Array[String] = []
for def in defs {
match def {
StructDef(type_name, schema) => {
parts.push(render_struct_def(type_name, schema))
if include_to_schema {
parts.push(render_to_schema_fn(type_name, schema))
}
}
EnumDef(type_name, values) => {
parts.push(render_enum_def(type_name, values))
if include_to_schema {
parts.push(render_enum_to_schema_fn(type_name, values))
}
}
}
}
parts.join("\n\n")
}
///|
fn render_struct_def(type_name : String, schema : @core.Schema) -> String {
match schema.schema_type {
ObjectType(spec, _) => {
let escaped_type = @core.escape_type_name(type_name)
if spec.is_empty() {
return "pub struct " + escaped_type + " {} derive(ToJson, FromJson)"
}
let mut result = "pub struct " + escaped_type + " {\n"
for key, field_schema in spec {
let field_name = @core.escape_variable_name(key)
let field_type = field_to_moonbit_type(
field_schema,
child_type_name(escaped_type, key),
)
let opt_mark = if is_optional_field(field_schema) { "?" } else { "" }
let comment = @core.constraint_comment(field_schema)
let line = " " + field_name + " : " + field_type + opt_mark
if comment.is_empty() {
result = result + line + "\n"
} else {
result = result + line + " // " + comment + "\n"
}
}
result + "} derive(ToJson, FromJson)"
}
_ => "// TODO: " + type_name + " is not an object schema"
}
}
///|
fn render_enum_def(type_name : String, values : Array[String]) -> String {
let escaped_type = @core.escape_type_name(type_name)
let mut result = "pub enum " + escaped_type + " {\n"
for value in values {
result = result + " " + moonbit_variant_name(value) + "\n"
}
result + "} derive(ToJson, FromJson)"
}
///|
fn render_to_schema_fn(type_name : String, schema : @core.Schema) -> String {
let escaped_type = @core.escape_type_name(type_name)
let code = schema_to_moon_zod_code(schema.name(escaped_type))
let var_name = @core.escape_variable_name(escaped_type)
"pub fn " +
escaped_type +
"::to_schema() -> @moon_zod.Schema {\n" +
" " +
code +
"\n" +
" " +
var_name +
"\n" +
"}"
}
///|
fn render_enum_to_schema_fn(
type_name : String,
values : Array[String],
) -> String {
let escaped_type = @core.escape_type_name(type_name)
let var_name = @core.escape_variable_name(escaped_type)
let parts = values.map(fn(value) {
"\"" + @core.escape_mbt_string(value) + "\""
})
"pub fn " +
escaped_type +
"::to_schema() -> @moon_zod.Schema {\n" +
" let " +
var_name +
" = @moon_zod.enum_values([" +
parts.join(", ") +
"]).name(\"" +
@core.escape_mbt_string(escaped_type) +
"\")\n" +
" " +
var_name +
"\n" +
"}"
}
///|
fn field_to_moonbit_type(schema : @core.Schema, type_hint : String) -> String {
match schema.schema_type {
OptionalType(inner) | DefaultType(inner, _) =>
field_to_moonbit_type(inner, type_hint)
TransformType(inner, _) => field_to_moonbit_type(inner, type_hint)
PreprocessType(_, inner) => field_to_moonbit_type(inner, type_hint)
StringType => "String"
NumberType => if has_int_rule(schema) { "Int64" } else { "Double" }
BooleanType => "Bool"
NullType => "Unit"
AnyType | UnknownType => "Json"
ObjectType(_, _) => schema_type_name(schema, type_hint)
ArrayType(elem) =>
"Array[" + field_to_moonbit_type(elem, type_hint + "Item") + "]"
TupleType(_) => "Json // TODO: tuple type"
EnumType(_) => schema_type_name(schema, type_hint)
UnionType(schemas) => union_to_moonbit_type(schemas, type_hint)
IntersectionType(schemas) =>
intersection_to_moonbit_type(schemas, type_hint)
LiteralType(value) => literal_to_moonbit_type(value)
}
}
///|
fn union_to_moonbit_type(
schemas : Array[@core.Schema],
type_hint : String,
) -> String {
if schemas.length() == 2 {
let left = schemas[0]
let right = schemas[1]
if is_null_schema(left) {
return field_to_moonbit_type(right, type_hint)
}
if is_null_schema(right) {
return field_to_moonbit_type(left, type_hint)
}
}
"Json // TODO: union type"
}
///|
fn intersection_to_moonbit_type(
schemas : Array[@core.Schema],
type_hint : String,
) -> String {
if schemas.length() == 1 {
return field_to_moonbit_type(schemas[0], type_hint)
}
"Json // TODO: intersection type"
}
///|
fn is_optional_field(schema : @core.Schema) -> Bool {
match schema.schema_type {
OptionalType(_) | DefaultType(_, _) => true
UnionType(schemas) => schemas.any(fn(item) { is_null_schema(item) })
_ => false
}
}
///|
fn is_null_schema(schema : @core.Schema) -> Bool {
match schema.schema_type {
NullType => true
_ => false
}
}
///|
fn schema_type_name(schema : @core.Schema, fallback : String) -> String {
if schema.name.is_empty() {
@core.escape_type_name(fallback)
} else {
@core.escape_type_name(schema.name)
}
}
///|
fn child_type_name(parent_type : String, field_name : String) -> String {
@core.escape_type_name(parent_type + pascal_name(field_name))
}
///|
fn has_int_rule(schema : @core.Schema) -> Bool {
for rule in schema.rules {
match rule.annotation {
Object(m) =>
match m.get("type") {
Some(String(v)) if v == "integer" => return true
_ => ()
}
_ => ()
}
}
false
}
///|
fn literal_to_moonbit_type(json : Json) -> String {
match json {
String(_) => "String"
Number(_) => "Double"
True | False => "Bool"
Null => "Unit"
Array(_) => "Array[Json]"
Object(_) => "Json"
}
}
///|
fn moonbit_variant_name(value : String) -> String {
pascal_name(value)
}
///|
fn pascal_name(value : String) -> String {
let chars = value.to_array()
let mut result = ""
let mut capitalize_next = true
for char in chars {
if (char >= 'a' && char <= 'z') ||
(char >= 'A' && char <= 'Z') ||
(char >= '0' && char <= '9') {
if capitalize_next {
result = result + char_to_upper_string(char)
capitalize_next = false
} else {
result = result + char.to_string()
}
} else {
capitalize_next = true
}
}
if result.is_empty() {
"Variant"
} else if result[0] >= '0' && result[0] <= '9' {
"V" + result
} else {
result
}
}
///|
fn char_to_upper_string(char : Char) -> String {
if char >= 'a' && char <= 'z' {
match (char.to_int() - 32).to_char() {
Some(upper) => upper.to_string()
None => char.to_string()
}
} else {
char.to_string()
}
}