///|
/// **@core.Schema → moon_zod source code**
///
/// Best-effort conversion from @core.Schema objects to moon_zod method chain source code.
/// Custom closures and opaque rules cannot be faithfully serialized; unsupported
/// runtime-only semantics are emitted as explicit placeholders where possible.
/// Supports inline expansion and named schema references.
///|
/// Generate best-effort moon_zod source code from a @core.Schema @core.object (inline expansion).
///
/// # Example
/// ```mbt nocheck
/// let schema = @moon_zod.string().min(3).describe("username")
///
/// let code = schema_to_moon_zod_code(schema)
/// // "@moon_zod.string().min(3).describe(\"username\")"
/// ```
/// If root schema has no name, it is assigned the name "Root".
pub fn schema_to_moon_zod_code(schema : @core.Schema) -> String {
let schema_name = if schema.name.is_empty() { "Root" } else { schema.name }
let code = schema_to_moon_zod_code_inline(schema, None)
let result = "let " +
@core.escape_variable_name(schema_name) +
" = " +
code +
".name(\"" +
@core.escape_mbt_string(schema_name) +
"\")"
result
}
///|
/// Generate best-effort inline code with optional named schema reference substitution.
///
/// If `include_names` is None, renders all schemas inline.
/// If `include_names` is Some(names), replaces references to named schemas with variable names.
///
/// For use with `schema_to_moon_zod_code_named()` which produces separate definitions.
pub fn schema_to_moon_zod_code_inline_with_refs(
schema : @core.Schema,
include_names : Array[String]?,
) -> String {
schema_to_moon_zod_code_inline(schema, include_names)
}
///|
/// Generate best-effort separate named schema definitions and return as newline-separated list.
///
/// # Example
/// ```
/// let named_list = schema_to_moon_zod_code_named(root_schema, None)
/// // Returns:
/// // "User: @moon_zod.object({ ... }).name(\"User\")\n
/// // Profile: @moon_zod.object({ ... }).name(\"Profile\")"
/// ```
/// If root schema has no name, it is assigned the name "Root".
pub fn schema_to_moon_zod_code_named(
schema : @core.Schema,
include_names? : Array[String]? = None,
) -> String {
let mut schema = schema
if schema.name.is_empty() {
schema = schema.name("Root")
}
let all_named = @core.collect_named_schemas(schema)
let selected = @core.filter_named_schemas(all_named, include_names)
let sorted = @core.topological_sort_schemas(selected)
let defined_names = selected.map(fn(x) { x.name })
let mut result = ""
for s in sorted {
if !s.name.is_empty() {
let self_names = defined_names.filter(fn(n) { n != s.name })
let code = schema_to_moon_zod_code_inline(s, Some(self_names))
if !result.is_empty() {
result = result + "\n"
}
let line = "let " +
@core.escape_variable_name(s.name) +
" = " +
code +
".name(\"" +
@core.escape_mbt_string(s.name) +
"\")"
result = result + line
}
}
result
}
///|
/// Internal: Generate code with optional named schema reference support.
fn schema_to_moon_zod_code_inline(
schema : @core.Schema,
include_names : Array[String]?,
) -> String {
let defined_names = match include_names {
None => []
Some(names) => names
}
// If this schema has a name and it's in the defined_names list, use reference
if !schema.name.is_empty() && @core.value_in_array(schema.name, defined_names) {
return @core.escape_variable_name(schema.name)
}
let base = schema_type_to_code(schema.schema_type, defined_names)
let mut result = apply_rules_to_code(base, schema)
// Apply description if present
if !schema.description.is_empty() {
result = result +
".describe(\"" +
@core.escape_mbt_string(schema.description) +
"\")"
}
// Apply type-level error messages if present
if !schema.required_error.is_empty() {
result = result +
".required_error(\"" +
@core.escape_mbt_string(schema.required_error) +
"\")"
}
if !schema.invalid_type_error.is_empty() {
result = result +
".invalid_type_error(\"" +
@core.escape_mbt_string(schema.invalid_type_error) +
"\")"
}
result
}
///|
/// Generate base type constructor code.
fn schema_type_to_code(
schema_type : @core.SchemaType,
defined_names : Array[String],
) -> String {
match schema_type {
StringType => "@moon_zod.string()"
NumberType => "@moon_zod.number()"
BooleanType => "@moon_zod.boolean()"
NullType => "@moon_zod.null()"
AnyType => "@moon_zod.any()"
UnknownType => "@moon_zod.unknown()"
ObjectType(fields, mode) => {
let base = if fields.length() == 0 {
"@moon_zod.object({})"
} else {
let parts : Array[String] = []
for key, field_schema in fields {
let field_code = schema_to_moon_zod_code_inline(
field_schema,
Some(defined_names),
)
parts.push("\"" + @core.escape_mbt_string(key) + "\": " + field_code)
}
"@moon_zod.object({ " + join_with(parts, ", ") + " })"
}
// Apply @core.object mode if not default Strip
match mode {
Strip => base
Passthrough => base + ".passthrough()"
Strict => base + ".strict()"
}
}
ArrayType(elem) => {
let elem_code = schema_to_moon_zod_code_inline(elem, Some(defined_names))
"@moon_zod.array(" + elem_code + ")"
}
TupleType(items) => {
let parts : Array[String] = []
for item in items {
parts.push(schema_to_moon_zod_code_inline(item, Some(defined_names)))
}
"@moon_zod.tuple([" + join_with(parts, ", ") + "])"
}
OptionalType(inner) =>
schema_to_moon_zod_code_inline(inner, Some(defined_names)) + ".optional()"
DefaultType(inner, default_val) =>
schema_to_moon_zod_code_inline(inner, Some(defined_names)) +
".default(" +
json_to_literal(default_val) +
")"
EnumType(values) => {
let parts : Array[String] = []
for v in values {
parts.push("\"" + @core.escape_mbt_string(v) + "\"")
}
"@moon_zod.enum_values([" + join_with(parts, ", ") + "])"
}
UnionType(schemas) => {
let parts : Array[String] = []
for s in schemas {
parts.push(schema_to_moon_zod_code_inline(s, Some(defined_names)))
}
"@moon_zod.union([" + join_with(parts, ", ") + "])"
}
IntersectionType(schemas) => {
let parts : Array[String] = []
for s in schemas {
parts.push(schema_to_moon_zod_code_inline(s, Some(defined_names)))
}
"@moon_zod.intersection([" + join_with(parts, ", ") + "])"
}
PreprocessType(_, inner) => {
let inner_code = schema_to_moon_zod_code_inline(
inner,
Some(defined_names),
)
"@moon_zod.preprocess(fn(x) { Ok(x) }, " + inner_code + ")" // TODO: Handle preprocess function code export
}
LiteralType(val) => "@moon_zod.literal(" + json_to_literal(val) + ")"
TransformType(inner, _) =>
schema_to_moon_zod_code_inline(inner, Some(defined_names)) +
".transform(fn(x) { Ok(x) })" // TODO: Handle transform function code export
}
}
///|
/// Apply rules as method chain.
/// Note: Only outputs constraint annotations, not rule messages,
/// since rule.message may be auto-generated defaults.
fn apply_rules_to_code(base : String, schema : @core.Schema) -> String {
let mut result = base
let mut has_int_type = false
for rule in schema.rules {
match rule.annotation {
Object(map) => {
if map.contains("type") {
match map.get("type") {
Some(String("integer")) => has_int_type = true
_ => ()
}
}
// String constraints
if map.contains("minLength") {
match map.get("minLength") {
Some(Number(v, ..)) =>
result = result + ".min(" + @core.format_double_simple(v) + ")"
_ => ()
}
}
if map.contains("maxLength") {
match map.get("maxLength") {
Some(Number(v, ..)) =>
result = result + ".max(" + @core.format_double_simple(v) + ")"
_ => ()
}
}
if map.contains("pattern") {
match map.get("pattern") {
Some(String(p)) =>
result = result + ".regex(\"" + @core.escape_mbt_string(p) + "\")"
_ => ()
}
}
if map.contains("format") {
match map.get("format") {
Some(String("email")) => result = result + ".email()"
Some(String("uri")) => result = result + ".url()"
Some(String("date-time")) => result = result + ".datetime()"
Some(String("ipv4")) => result = result + ".ipv4()"
Some(String("ipv6")) => result = result + ".ipv6()"
Some(String("uuid")) => result = result + ".uuid()"
_ => ()
}
}
// Number constraints
if map.contains("minimum") {
match map.get("minimum") {
Some(Number(v, ..)) =>
result = result + ".min(" + @core.format_double_simple(v) + ")"
_ => ()
}
}
if map.contains("maximum") {
match map.get("maximum") {
Some(Number(v, ..)) =>
result = result + ".max(" + @core.format_double_simple(v) + ")"
_ => ()
}
}
if map.contains("exclusiveMinimum") {
match map.get("exclusiveMinimum") {
Some(Number(v, ..)) =>
if v == 0.0 {
result = result + ".positive()"
} else {
result = result + ".min(" + @core.format_double_simple(v) + ")"
}
_ => ()
}
}
if map.contains("exclusiveMaximum") {
match map.get("exclusiveMaximum") {
Some(Number(v, ..)) =>
if v == 0.0 {
result = result + ".negative()"
} else {
result = result + ".max(" + @core.format_double_simple(v) + ")"
}
_ => ()
}
}
if map.contains("multipleOf") {
match map.get("multipleOf") {
Some(Number(v, ..)) =>
result = result +
".multipleOf(" +
@core.format_double_simple(v) +
")"
_ => ()
}
}
// Array constraints
if map.contains("minItems") {
match map.get("minItems") {
Some(Number(v, ..)) =>
result = result + ".min(" + @core.format_double_simple(v) + ")"
_ => ()
}
}
if map.contains("maxItems") {
match map.get("maxItems") {
Some(Number(v, ..)) =>
result = result + ".max(" + @core.format_double_simple(v) + ")"
_ => ()
}
}
}
_ => ()
}
}
if has_int_type {
result = result + ".int()"
}
result
}
///|
/// Convert a Json value to moon_zod @core.literal expression.
fn json_to_literal(val : Json) -> String {
match val {
String(s) => "Json::string(\"" + @core.escape_mbt_string(s) + "\")"
Number(v, ..) => "Json::number(" + @core.format_double_simple(v) + ")"
True => "true"
False => "false"
Null => "null"
Array(arr) => {
let parts = arr.map(fn(v) { json_to_literal(v) })
"Json::array([" + join_with(parts, ", ") + "])"
}
Object(map) => {
let parts : Array[String] = []
for k, v in map {
let lit = json_to_literal(v)
parts.push("\"" + @core.escape_mbt_string(k) + "\": " + lit)
}
"Json::object({" + join_with(parts, ", ") + "})"
}
}
}
///|
fn join_with(parts : Array[String], sep : String) -> String {
if parts.is_empty() {
return ""
}
let mut result = parts[0]
for i = 1; i < parts.length(); i = i + 1 {
result = result + sep + parts[i]
}
result
}