pushkin-compiler 0.2.1

Schema compiler for pushkin: canonical JSON Schema emission and generated bindings
Documentation
//! TS/Zod emission: `.strict()` objects, `z.infer` types (spec ยง5.2 table).

use std::fmt::Write as _;

use crate::schema::{ArrayElement, ContractSchema, PropertyKind};
use crate::targets::{header, number_literal, quoted_string, type_name};

pub fn emit(schema: &ContractSchema, epoch: u32) -> String {
    let mut fields = String::new();
    for property in &schema.properties {
        let (mut chain, default_literal) = chain_and_default(&property.kind);
        if let Some(literal) = default_literal {
            let _ = write!(chain, ".default({literal})");
        } else if !property.required {
            chain.push_str(".optional()");
        }
        let _ = writeln!(fields, "    {}: {chain},", property.name);
    }

    let name = type_name(schema);
    format!(
        "{header}import {{ z }} from \"zod\";\n\n\
         export const {name}Schema = z\n  .object({{\n{fields}  }})\n  .strict();\n\n\
         export type {name} = z.infer<typeof {name}Schema>;\n",
        header = header(schema, epoch, "//"),
    )
}

/// Base validator chain + the already-spelled default literal, per kind.
fn chain_and_default(kind: &PropertyKind) -> (String, Option<String>) {
    match kind {
        PropertyKind::String {
            min_length,
            max_length,
            format,
            enum_values,
            default,
        } => {
            let mut chain = match enum_values {
                Some(values) => {
                    let list = values
                        .iter()
                        .map(|value| quoted_string(value))
                        .collect::<Vec<_>>()
                        .join(", ");
                    format!("z.enum([{list}])")
                }
                None if format.as_deref() == Some("email") => "z.email()".to_owned(),
                None => "z.string()".to_owned(),
            };
            if enum_values.is_none() {
                if let Some(min) = min_length {
                    let _ = write!(chain, ".min({min})");
                }
                if let Some(max) = max_length {
                    let _ = write!(chain, ".max({max})");
                }
            }
            (chain, default.as_deref().map(quoted_string))
        }
        PropertyKind::Integer { default } => (
            "z.number().int()".to_owned(),
            default.map(|value| value.to_string()),
        ),
        PropertyKind::Number { default } => ("z.number()".to_owned(), default.map(number_literal)),
        PropertyKind::Boolean { default } => (
            "z.boolean()".to_owned(),
            default.map(|value| value.to_string()),
        ),
        // D4: wrap the spelling the scalar path already produces, so the
        // element spellings cannot drift from the scalar ones.
        PropertyKind::Array { element } => {
            let inner = match element {
                ArrayElement::String => "z.string()",
                ArrayElement::Integer => "z.number().int()",
                ArrayElement::Number => "z.number()",
                ArrayElement::Boolean => "z.boolean()",
            };
            (format!("z.array({inner})"), None)
        }
    }
}