pushkin-compiler 0.1.0

Schema compiler for pushkin: canonical JSON Schema emission and generated bindings
Documentation
//! Compiler conformance: canonical JSON Schema 2020-12 → four strict targets,
//! epoch headers, byte-identical regeneration, loud constrained-subset
//! rejection (Phase 1 test plan — read-only once committed).

use pushkin_compiler::{compile, CompileError, CompileRequest, Target};

const USER_SCHEMA: &str = r#"{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "name": { "type": "string", "minLength": 1, "maxLength": 200 },
    "email": { "type": "string", "format": "email" },
    "role": { "default": "member", "type": "string", "enum": ["member", "admin"] }
  },
  "required": ["name", "email"],
  "additionalProperties": false
}"#;

fn request(target: Target) -> CompileRequest {
    CompileRequest {
        contract_name: "user".to_owned(),
        schema_json: USER_SCHEMA.to_owned(),
        target,
        epoch: 1,
    }
}

#[test]
fn emits_strict_zod_binding() {
    let out = compile(&request(Target::Zod)).unwrap();
    assert!(
        out.content.contains(".strict()"),
        "Zod objects must be strict: {}",
        out.content
    );
    assert!(out.content.contains("name"));
    assert!(out.content.contains("email"));
    assert!(out.content.contains("z.infer"));
}

#[test]
fn emits_strict_pydantic_binding() {
    let out = compile(&request(Target::Pydantic)).unwrap();
    assert!(
        out.content.contains("extra='forbid'") || out.content.contains("extra=\"forbid\""),
        "Pydantic must forbid unknown fields: {}",
        out.content
    );
    assert!(out.content.contains("strict=True"));
    assert!(out.content.contains("class UserCreate"));
}

#[test]
fn emits_strict_rust_binding() {
    let out = compile(&request(Target::Rust)).unwrap();
    assert!(
        out.content.contains("deny_unknown_fields"),
        "Rust serde must deny unknown fields: {}",
        out.content
    );
    assert!(out.content.contains("pub struct UserCreate"));
}

#[test]
fn emits_sql_ddl() {
    let out = compile(&request(Target::Sql)).unwrap();
    assert!(out.content.contains("CREATE TABLE"));
    assert!(out.content.to_lowercase().contains("not null"));
    assert!(
        out.content.contains("CHECK"),
        "enum must become a CHECK constraint: {}",
        out.content
    );
}

#[test]
fn bindings_carry_epoch_header() {
    for target in [Target::Zod, Target::Pydantic, Target::Rust, Target::Sql] {
        let out = compile(&request(target)).unwrap();
        assert!(
            out.content.contains("pushkin-epoch: 1"),
            "{target:?} missing epoch header:\n{}",
            out.content
        );
        assert!(
            out.content.to_lowercase().contains("generated"),
            "{target:?} must be marked generated"
        );
    }
}

#[test]
fn regeneration_is_byte_identical() {
    for target in [Target::Zod, Target::Pydantic, Target::Rust, Target::Sql] {
        let first = compile(&request(target)).unwrap();
        let second = compile(&request(target)).unwrap();
        assert_eq!(
            first.content, second.content,
            "{target:?} regeneration must be deterministic"
        );
    }
}

#[test]
fn rejects_unrepresentable_construct_with_alternatives() {
    // patternProperties does not round-trip to all four targets in Phase 1's
    // constrained subset: rejection must name the construct and alternatives.
    let unrepresentable = r#"{
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "type": "object",
      "patternProperties": { "^x-": { "type": "string" } },
      "additionalProperties": false
    }"#;
    let err = compile(&CompileRequest {
        contract_name: "user".to_owned(),
        schema_json: unrepresentable.to_owned(),
        target: Target::Rust,
        epoch: 1,
    })
    .unwrap_err();

    let message = err.to_string();
    assert!(
        message.contains("patternProperties"),
        "must name the construct: {message}"
    );
    assert!(
        message.contains("properties") || message.contains("alternative"),
        "must suggest a named alternative: {message}"
    );
    assert!(matches!(err, CompileError::Unrepresentable { .. }));
}

#[test]
fn rejects_invalid_schema_json() {
    let err = compile(&CompileRequest {
        contract_name: "user".to_owned(),
        schema_json: "{ not json".to_owned(),
        target: Target::Zod,
        epoch: 1,
    })
    .unwrap_err();
    assert!(matches!(err, CompileError::InvalidSchema { .. }));
}