///|
/// **@core.Schema → moon_zod source code**
///
/// Convert representable schemas into moon_zod method chain source code.
/// Custom closures and opaque rules fail explicitly; use the checked API for errors.
/// 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 {
match source_export_error(schema) {
Some(message) => abort(message)
None => ()
}
if contains_lazy(schema, Map([])) {
return schema_to_moon_zod_code_named(schema)
}
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 {
match source_export_error(schema) {
Some(message) => abort(message)
None => ()
}
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 {
match source_export_error(schema) {
Some(message) => abort(message)
None => ()
}
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 recursive_graph = contains_lazy(schema, Map([]))
let mut result = ""
for s in sorted {
if !s.name.is_empty() {
let code = schema_to_moon_zod_code_inline(
s,
Some(defined_names),
inline_root=true,
)
if !result.is_empty() {
result = result + "\n"
}
let line = if recursive_graph {
"fn " +
@core.escape_function_name(s.name) +
"_schema() -> @moon_zod.Schema { " +
code +
".name(\"" +
@core.escape_mbt_string(s.name) +
"\") }\nlet " +
@core.escape_variable_name(s.name) +
" = " +
@core.escape_function_name(s.name) +
"_schema()"
} else {
"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]?,
inline_root~ : Bool = false,
) -> 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 !inline_root &&
!schema.name.is_empty() &&
@core.value_in_array(schema.name, defined_names) {
if schema.schema_type is LazyType(_) {
return "@moon_zod.recursive(" +
@core.escape_function_name(schema.name) +
"_schema)"
}
return @core.escape_variable_name(schema.name)
}
let base = schema_type_to_code(schema.schema_type, defined_names, schema.name)
let mut result = apply_rules_to_code(base, schema)
// Apply description if present
if !schema.brand.is_empty() {
result = result +
".brand(\"" +
@core.escape_mbt_string(schema.brand) +
"\")"
}
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],
wrapper_name : 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, ", ") + "])"
}
NullableType(inner) =>
schema_to_moon_zod_code_inline(
if inner.name == wrapper_name {
{ ..inner, name: "", }
} else {
inner
},
Some(defined_names),
) +
".nullable()"
PrefaultType(inner, value) =>
schema_to_moon_zod_code_inline(
if inner.name == wrapper_name {
{ ..inner, name: "", }
} else {
inner
},
Some(defined_names),
) +
".prefault(" +
json_to_literal(value) +
")"
OptionalType(inner) =>
schema_to_moon_zod_code_inline(
if inner.name == wrapper_name {
{ ..inner, name: "", }
} else {
inner
},
Some(defined_names),
) +
".optional()"
DefaultType(inner, default_val) =>
schema_to_moon_zod_code_inline(
if inner.name == wrapper_name {
{ ..inner, name: "", }
} else {
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(_, _) =>
abort(
"Custom preprocess cannot be serialized; use try_schema_to_moon_zod_code()",
)
LiteralType(val) => "@moon_zod.literal(" + json_to_literal(val) + ")"
TransformType(inner, closure) => {
let base = schema_to_moon_zod_code_inline(
if inner.name == wrapper_name {
{ ..inner, name: "", }
} else {
inner
},
Some(defined_names),
)
let suffix = match closure.kind {
TrimTransform => ".trim()"
LowerTransform => ".to_lower()"
UpperTransform => ".to_upper()"
IdentityTransform => ""
CustomTransform => abort("Custom transform cannot be serialized")
}
base + suffix
}
LazyType(resolve) => {
let resolved = resolve()
if !resolved.name.is_empty() && defined_names.contains(resolved.name) {
"@moon_zod.recursive(" +
@core.escape_function_name(resolved.name) +
"_schema)"
} else {
"@moon_zod.recursive(fn() { " +
schema_to_moon_zod_code_inline(
resolved,
Some(defined_names),
inline_root=resolved.name == wrapper_name,
) +
" })"
}
}
DiscriminatedUnionType(disc, options) => {
let parts : Array[String] = []
for key, option in options {
let opt_code = schema_to_moon_zod_code_inline(
option,
Some(defined_names),
)
parts.push("\"" + @core.escape_mbt_string(key) + "\": " + opt_code)
}
"@moon_zod.discriminated_union(\"" +
@core.escape_mbt_string(disc) +
"\", {" +
join_with(parts, ", ") +
"})"
}
PipeType(input, _, output) => {
let input = if input.name == wrapper_name {
{ ..input, name: "", }
} else {
input
}
let input_code = schema_to_moon_zod_code_inline(
input,
Some(defined_names),
)
let output_code = schema_to_moon_zod_code_inline(
output,
Some(defined_names),
)
input_code + ".pipe(" + output_code + ")"
}
}
}
///|
/// 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
for rule in schema.rules {
let operation = match rule.constraint {
Minimum(_, value, inclusive) =>
if !inclusive && value == 0.0 {
".positive()"
} else {
(if inclusive { ".min(" } else { ".gt(" }) +
format_mbt_number(value) +
")"
}
Maximum(_, value, inclusive) =>
if !inclusive && value == 0.0 {
".negative()"
} else {
(if inclusive { ".max(" } else { ".lt(" }) +
format_mbt_number(value) +
")"
}
ExactLength(_, value) => ".length(" + value.to_string() + ")"
Multiple(value) => ".multipleOf(" + format_mbt_number(value) + ")"
Integer => ".int()"
SafeInteger => ".safe()"
Finite => ".finite()"
Pattern(value) => ".regex(\"" + @core.escape_mbt_string(value) + "\")"
Prefix(value) => ".startsWith(\"" + @core.escape_mbt_string(value) + "\")"
Suffix(value) => ".endsWith(\"" + @core.escape_mbt_string(value) + "\")"
Contains(value) => ".includes(\"" + @core.escape_mbt_string(value) + "\")"
Format(value) =>
match value {
"uri" => ".url()"
"date-time" => datetime_to_code(rule.annotation)
"email" => ".email()"
"uuid" => ".uuid()"
"ipv4" => ".ipv4()"
"ipv6" => ".ipv6()"
"ip" => ".ip()"
"ulid" => ".ulid()"
"cuid" => ".cuid()"
_ => abort("Unsupported format in source export: " + value)
}
Opaque =>
abort(
"Opaque rule cannot be serialized; use try_schema_to_moon_zod_code()",
)
}
result = result + operation
if rule.explicit_message {
result = result +
".message(\"" +
@core.escape_mbt_string(rule.message) +
"\")"
}
}
result
}
///|
fn datetime_to_code(annotation : Json) -> String {
guard annotation is Object(fields) &&
fields.get("x-moon-zod-datetime") is Some(Object(params)) else {
return ".datetime()"
}
let offset = params.get("offset") is Some(True)
let allow_local = params.get("allow_local") is Some(True)
let precision = match params.get("precision") {
Some(Number(value, ..)) => value.to_int()
_ => -2
}
if !offset && !allow_local && precision == -2 {
".datetime()"
} else {
".datetime(offset=" +
offset.to_string() +
", allow_local=" +
allow_local.to_string() +
", precision=" +
precision.to_string() +
")"
}
}
///|
fn source_export_error(schema : @core.Schema) -> String? {
let nodes = match @core.try_schema_nodes(schema) {
Ok(nodes) => nodes
Err(message) => return Some(message)
}
let identifiers : Map[String, String] = Map([])
for node in nodes {
if !node.name.is_empty() {
let identifier = @core.escape_variable_name(node.name)
match identifiers.get(identifier) {
Some(previous) =>
if previous != node.name {
return Some(
"Schema names collide after identifier escaping: " + node.name,
)
}
None => identifiers.set(identifier, node.name)
}
}
if node.schema_type is PreprocessType(_, _) {
return Some("Custom preprocess callbacks cannot be serialized")
}
if node.schema_type is TransformType(_, closure) &&
closure.kind is CustomTransform {
return Some("Custom transform callbacks cannot be serialized")
}
if node.rules.any(fn(rule) { rule.constraint is Opaque }) {
return Some("Opaque refinement callbacks cannot be serialized")
}
}
if contains_lazy(schema, Map([])) {
for node in nodes {
if !node.name.is_empty() {
let helper = @core.escape_function_name(node.name) + "_schema"
if identifiers.contains(helper) {
return Some(
"Recursive helper name collides with schema variable: " + helper,
)
}
}
}
}
None
}
///|
/// Generate faithful source or return an explicit unsupported-schema error.
pub fn try_schema_to_moon_zod_code(
schema : @core.Schema,
) -> Result[String, String] {
match source_export_error(schema) {
Some(message) => Err(message)
None => Ok(schema_to_moon_zod_code(schema))
}
}
///|
/// 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(" + format_mbt_number(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
}
///|
fn format_mbt_number(value : Double) -> String {
let text = @core.format_double_simple(value)
let parts = text.split("e").to_array()
if parts.length() == 2 && !parts[0].contains(".") {
parts[0].to_owned() + ".0e" + parts[1].to_owned()
} else if parts.length() == 1 &&
!text.contains(".") &&
(value > 2147483647.0 || value < -2147483648.0) {
text + ".0"
} else {
text
}
}