pushkin-compiler 0.2.0

Schema compiler for pushkin: canonical JSON Schema emission and generated bindings
Documentation
//! Target emitters. Each produces a deterministic, epoch-stamped, strict
//! binding. Rust emission generates through the pinned `typify`
//! `TypeSpace`, with a `schemars` round-trip suite pinning fidelity in
//! tests (PHASE-LOG: Phase 6 task 2 — the typify swap that discharged
//! the R5 condition; history of the earlier hand-rolled emitter lives
//! there, not here). The Zod, Pydantic, and SQL emitters are hand-rolled
//! string assembly. For every target, the constrained-subset parser in
//! `schema.rs` stays the loud FRONT gate: emitters only ever receive
//! already-validated schemas (N6/§5.1).

pub mod pydantic;
pub mod rust;
pub mod sql;
pub mod zod;

use crate::schema::ContractSchema;

/// Shared generated-file header (spec §5.2 schema-epoch header).
pub fn header(schema: &ContractSchema, epoch: u32, comment: &str) -> String {
    format!(
        "{comment} GENERATED by pushkin compile from contract '{name}' — do not hand-edit.\n\
         {comment} pushkin-epoch: {epoch}\n",
        name = schema.contract_name,
    )
}

/// `user` → `UserCreate` (matches the Phase 0 spike's naming convention).
pub fn type_name(schema: &ContractSchema) -> String {
    let mut pascal = String::new();
    for part in schema.contract_name.split(['-', '_']) {
        let mut chars = part.chars();
        if let Some(first) = chars.next() {
            pascal.extend(first.to_uppercase());
            pascal.push_str(chars.as_str());
        }
    }
    format!("{pascal}Create")
}

/// Deterministic spelling for a `number` default. Portable as-is to JS
/// (one number type) and SQL (the engine coerces an integer literal into
/// `DOUBLE PRECISION`). Rust's shortest-round-trip float formatting keeps
/// it stable, so an integral `5.0` prints `5`.
///
/// NOT portable to Python, which is why `python_number_literal` exists:
/// Python's literal grammar is the only one in the target set that
/// distinguishes `5` from `5.0`, and Pydantic under `ConfigDict(strict=True)`
/// does not validate or coerce DEFAULTS — so a bare `5` on a field declared
/// `float` leaves the field holding an `int` (F35).
pub fn number_literal(value: f64) -> String {
    format!("{value}")
}

/// Python spelling for a `number` default: `number_literal`, widened to a
/// float literal when it carries no fractional part or exponent, so the
/// emitted default agrees with the declared `float` annotation (F35).
///
/// Derived from the shared spelling rather than reformatting the float
/// independently — the two must never drift.
pub fn python_number_literal(value: f64) -> String {
    let shared = number_literal(value);
    if shared.contains(['.', 'e', 'E']) {
        return shared;
    }
    format!("{shared}.0")
}

/// Produces a double-quoted literal valid in both JavaScript and Python.
pub fn quoted_string(value: &str) -> String {
    let mut output = String::with_capacity(value.len() + 2);
    output.push('"');
    for character in value.chars() {
        match character {
            '"' => output.push_str("\\\""),
            '\\' => output.push_str("\\\\"),
            '\n' => output.push_str("\\n"),
            '\r' => output.push_str("\\r"),
            '\t' => output.push_str("\\t"),
            '\u{0008}' => output.push_str("\\b"),
            '\u{000C}' => output.push_str("\\f"),
            '\u{2028}' => output.push_str("\\u2028"),
            '\u{2029}' => output.push_str("\\u2029"),
            control if control.is_control() => {
                use std::fmt::Write as _;
                let _ = write!(output, "\\u{:04x}", u32::from(control));
            }
            other => output.push(other),
        }
    }
    output.push('"');
    output
}