///|
/// Convert a moon_zod @core.Schema into a standard JSON @core.Schema @core.object.
///
/// Uses JsonSchemaRenderer trait with FullJsonRenderer / SkeletonJsonRenderer / NamedJsonRenderer.
// ── Helper functions (shared across renderers) ──
///|
fn merge_annotations(base : Json, rules : Array[@core.Rule]) -> Json {
match base {
Object(map) => {
for rule in rules {
match rule.annotation {
Object(ann_map) =>
for k, v in ann_map {
map.set(k, v)
}
_ => ()
}
}
base
}
_ => base
}
}
///|
fn json_string_schema() -> Json {
Json::object({ "type": Json::string("string") })
}
///|
fn json_number_schema() -> Json {
Json::object({ "type": Json::string("number") })
}
///|
fn object_allows_additional_properties(mode : @core.ObjectMode) -> Bool {
match mode {
Passthrough => true
Strict | Strip => false
}
}
///|
fn maybe_merge_annotations(
base : Json,
rules : Array[@core.Rule],
include_annotations : Bool,
) -> Json {
if include_annotations {
merge_annotations(base, rules)
} else {
base
}
}
///|
fn nullable_schema(inner_json : Json) -> Json {
Json::object({
"anyOf": Json::array([
inner_json,
Json::object({ "type": Json::string("null") }),
]),
})
}
///|
fn add_default(schema_json : Json, default_val : Json) -> Json {
match schema_json {
Object(map) => {
map.set("default", default_val)
schema_json
}
_ => schema_json
}
}
///|
fn[R : JsonSchemaRenderer] render_json_child(
renderer : R,
schema : @core.Schema,
named_names : Array[String],
) -> Json {
if named_names.is_empty() {
render_json_type(renderer, schema)
} else {
render_json_type_ref(renderer, schema, named_names)
}
}
///|
fn[R : JsonSchemaRenderer] render_object_schema(
renderer : R,
spec : Map[String, @core.Schema],
mode : @core.ObjectMode,
schema : @core.Schema,
named_names : Array[String],
include_annotations : Bool,
) -> Json {
let props : Map[String, Json] = {}
let required_arr : Array[String] = []
for key, val in spec {
props.set(key, render_json_child(renderer, val, named_names))
if !@core.is_optional_schema(val) {
required_arr.push(key)
}
}
maybe_merge_annotations(
Json::object({
"type": Json::string("object"),
"properties": Json::object(props),
"additionalProperties": Json::boolean(
object_allows_additional_properties(mode),
),
"required": Json::array(required_arr.map(fn(k) { Json::string(k) })),
}),
schema.rules,
include_annotations,
)
}
///|
fn[R : JsonSchemaRenderer] render_array_schema(
renderer : R,
elem : @core.Schema,
schema : @core.Schema,
named_names : Array[String],
include_annotations : Bool,
) -> Json {
maybe_merge_annotations(
Json::object({
"type": Json::string("array"),
"items": render_json_child(renderer, elem, named_names),
}),
schema.rules,
include_annotations,
)
}
///|
fn[R : JsonSchemaRenderer] render_union_schema(
renderer : R,
schemas : Array[@core.Schema],
schema : @core.Schema,
named_names : Array[String],
include_annotations : Bool,
) -> Json {
maybe_merge_annotations(
Json::object({
"anyOf": Json::array(
schemas.map(fn(s) { render_json_child(renderer, s, named_names) }),
),
}),
schema.rules,
include_annotations,
)
}
///|
fn[R : JsonSchemaRenderer] render_intersection_schema(
renderer : R,
schemas : Array[@core.Schema],
schema : @core.Schema,
named_names : Array[String],
include_annotations : Bool,
) -> Json {
if can_merge_object_intersection(schemas) {
return maybe_merge_annotations(
render_merged_object_intersection_schema(renderer, schemas, named_names),
schema.rules,
include_annotations,
)
}
maybe_merge_annotations(
Json::object({
"allOf": Json::array(
schemas.map(fn(s) { render_json_child(renderer, s, named_names) }),
),
}),
schema.rules,
include_annotations,
)
}
///|
fn[R : JsonSchemaRenderer] render_json_tuple_type(
renderer : R,
items : Array[@core.Schema],
) -> Json {
Json::object({
"type": Json::string("array"),
"prefixItems": Json::array(
items.map(fn(item) { render_json_type(renderer, item) }),
),
"minItems": Json::number(items.length().to_double()),
"maxItems": Json::number(items.length().to_double()),
})
}
///|
fn can_merge_object_intersection(schemas : Array[@core.Schema]) -> Bool {
for schema in schemas {
match schema.schema_type {
ObjectType(_, _) => ()
IntersectionType(nested) =>
if !can_merge_object_intersection(nested) {
return false
}
_ => return false
}
}
true
}
///|
fn[R : JsonSchemaRenderer] render_merged_object_intersection_schema(
renderer : R,
schemas : Array[@core.Schema],
named_names : Array[String],
) -> Json {
let props : Map[String, Json] = {}
let required_arr : Array[String] = []
collect_object_intersection_parts(
renderer, schemas, named_names, props, required_arr,
)
Json::object({
"type": Json::string("object"),
"properties": Json::object(props),
"additionalProperties": Json::boolean(
object_intersection_allows_additional_properties(schemas),
),
"required": Json::array(required_arr.map(fn(k) { Json::string(k) })),
})
}
///|
fn[R : JsonSchemaRenderer] collect_object_intersection_parts(
renderer : R,
schemas : Array[@core.Schema],
named_names : Array[String],
props : Map[String, Json],
required_arr : Array[String],
) -> Unit {
for schema in schemas {
match schema.schema_type {
ObjectType(spec, _) =>
for key, val in spec {
let rendered = render_json_child(renderer, val, named_names)
match props.get(key) {
Some(existing) =>
props.set(key, combine_property_schema(existing, rendered))
None => props.set(key, rendered)
}
if !@core.is_optional_schema(val) &&
!@core.value_in_array(key, required_arr) {
required_arr.push(key)
}
}
IntersectionType(nested) =>
collect_object_intersection_parts(
renderer, nested, named_names, props, required_arr,
)
_ => ()
}
}
}
///|
fn combine_property_schema(left : Json, right : Json) -> Json {
if left == right {
left
} else {
Json::object({ "allOf": Json::array([left, right]) })
}
}
///|
fn object_intersection_allows_additional_properties(
schemas : Array[@core.Schema],
) -> Bool {
if object_intersection_has_strict(schemas) {
false
} else {
object_intersection_has_passthrough(schemas)
}
}
///|
fn object_intersection_has_strict(schemas : Array[@core.Schema]) -> Bool {
for schema in schemas {
match schema.schema_type {
ObjectType(_, Strict) => return true
IntersectionType(nested) =>
if object_intersection_has_strict(nested) {
return true
}
_ => ()
}
}
false
}
///|
fn object_intersection_has_passthrough(schemas : Array[@core.Schema]) -> Bool {
for schema in schemas {
match schema.schema_type {
ObjectType(_, Passthrough) => return true
IntersectionType(nested) =>
if object_intersection_has_passthrough(nested) {
return true
}
_ => ()
}
}
false
}
// ── NamedJsonRenderer: shared JSON Schema renderer ──
///|
pub struct NamedJsonRenderer {
named_names : Array[String]
include_annotations : Bool
}
///|
pub fn NamedJsonRenderer::NamedJsonRenderer(
named_names~ : Array[String],
include_annotations? : Bool = true,
) -> NamedJsonRenderer {
{ named_names, include_annotations }
}
///|
/// The named renderer's methods call render_json_type_ref for sub-elements,
/// which emits $ref for named schemas in the named_names list.
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_string(
self : NamedJsonRenderer,
schema : @core.Schema,
) -> Json {
maybe_merge_annotations(
json_string_schema(),
schema.rules,
self.include_annotations,
)
}
///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_number(
self : NamedJsonRenderer,
schema : @core.Schema,
) -> Json {
maybe_merge_annotations(
json_number_schema(),
schema.rules,
self.include_annotations,
)
}
///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_boolean(
self : NamedJsonRenderer,
schema : @core.Schema,
) -> Json {
maybe_merge_annotations(
Json::object({ "type": Json::string("boolean") }),
schema.rules,
self.include_annotations,
)
}
///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_null(
self : NamedJsonRenderer,
schema : @core.Schema,
) -> Json {
maybe_merge_annotations(
Json::object({ "type": Json::string("null") }),
schema.rules,
self.include_annotations,
)
}
///|
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_object(
self : NamedJsonRenderer,
spec : Map[String, @core.Schema],
mode : @core.ObjectMode,
schema : @core.Schema,
) -> Json {
render_object_schema(
self,
spec,
mode,
schema,
self.named_names,
self.include_annotations,
)
}
///|
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_array(
self : NamedJsonRenderer,
elem : @core.Schema,
schema : @core.Schema,
) -> Json {
render_array_schema(
self,
elem,
schema,
self.named_names,
self.include_annotations,
)
}
///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_optional(
self : NamedJsonRenderer,
inner : @core.Schema,
schema : @core.Schema,
) -> Json {
nullable_schema(render_json_type_ref(self, inner, self.named_names))
}
///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_default(
self : NamedJsonRenderer,
inner : @core.Schema,
default_val : Json,
schema : @core.Schema,
) -> Json {
add_default(
nullable_schema(render_json_type_ref(self, inner, self.named_names)),
default_val,
)
}
///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_transform(
self : NamedJsonRenderer,
inner : @core.Schema,
_closure : @core.TransformClosure,
schema : @core.Schema,
) -> Json {
render_json_type_ref(self, inner, self.named_names)
}
///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_enum(
self : NamedJsonRenderer,
values : Array[String],
schema : @core.Schema,
) -> Json {
maybe_merge_annotations(
Json::object({
"type": Json::string("string"),
"enum": Json::array(values.map(fn(s) { Json::string(s) })),
}),
schema.rules,
self.include_annotations,
)
}
///|
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_union(
self : NamedJsonRenderer,
schemas : Array[@core.Schema],
schema : @core.Schema,
) -> Json {
render_union_schema(
self,
schemas,
schema,
self.named_names,
self.include_annotations,
)
}
///|
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_intersection(
self : NamedJsonRenderer,
schemas : Array[@core.Schema],
schema : @core.Schema,
) -> Json {
render_intersection_schema(
self,
schemas,
schema,
self.named_names,
self.include_annotations,
)
}
///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_literal(
self : NamedJsonRenderer,
value : Json,
schema : @core.Schema,
) -> Json {
maybe_merge_annotations(
Json::object({ "const": value }),
schema.rules,
self.include_annotations,
)
}
///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_any(
self : NamedJsonRenderer,
_schema : @core.Schema,
) -> Json {
Json::object({})
}
///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_unknown(
self : NamedJsonRenderer,
_schema : @core.Schema,
) -> Json {
Json::object({})
}
///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_tuple(
self : NamedJsonRenderer,
items : Array[@core.Schema],
_schema : @core.Schema,
) -> Json {
render_json_tuple_type(self, items)
}
///|
#warnings("-unused_value")
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_preprocess(
self : NamedJsonRenderer,
inner : @core.Schema,
_schema : @core.Schema,
) -> Json {
render_json_type(self, inner)
}
// ── Public API ──
///|
/// Convert a moon_zod @core.Schema into a standard JSON @core.Schema @core.object with full
/// constraint annotations (minLength, maximum, pattern, format, etc.).
pub fn to_json_schema(schema : @core.Schema) -> Json {
render_json_type(NamedJsonRenderer(named_names=[]), schema)
}
///|
/// Convert a moon_zod @core.Schema into a lightweight JSON @core.Schema skeleton.
/// Only structural type information is exported.
pub fn to_json_schema_skeleton(schema : @core.Schema) -> Json {
render_json_type(
NamedJsonRenderer(named_names=[], include_annotations=false),
schema,
)
}
///|
/// Export a JSON @core.Schema with named sub-schemas in a `$defs` section.
/// If root schema has no name, it is assigned the name "Root".
pub fn to_json_schema_named(
schema : @core.Schema,
include_names? : Array[String]? = None,
) -> Json {
let mut schema = schema
if schema.name.is_empty() {
schema = schema.name("Root")
}
let all_named = @core.collect_named_schemas(schema)
let selected_schemas = @core.filter_named_schemas(all_named, include_names)
let named_names : Array[String] = selected_schemas.map(fn(s) { s.name })
let sorted_schemas = @core.topological_sort_schemas(selected_schemas)
let defs_map : Map[String, Json] = {}
for ns in sorted_schemas {
let ref_names = named_names.filter(fn(n) { n != ns.name })
let renderer = NamedJsonRenderer(named_names=ref_names)
defs_map.set(ns.name, render_json_type(renderer, ns))
}
let renderer = NamedJsonRenderer(named_names~)
let root = render_json_type_ref(renderer, schema, named_names)
let result_map : Map[String, Json] = {}
match root {
Object(m) =>
for k, v in m {
result_map.set(k, v)
}
_ => ()
}
result_map.set("$defs", Json::object(defs_map))
Json::object(result_map)
}