pushkin-compiler 0.2.0

Schema compiler for pushkin: canonical JSON Schema emission and generated bindings
Documentation
//! Regression coverage for the S3 and S5 human decisions (identifier
//! totality pass). S3-a: the SQL emitter double-quotes every table and
//! column identifier so S2-valid names that collide with SQL reserved
//! words (`user`, `order`, `group`) emit executable DDL. S5-a: a property
//! that is both `required` and carries a `default` is a loud rejection —
//! every target would silently demote it to optional.

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

fn request(contract_name: &str, schema_json: &str, target: Target) -> CompileRequest {
    CompileRequest {
        contract_name: contract_name.to_owned(),
        schema_json: schema_json.to_owned(),
        target,
        epoch: 1,
    }
}

// --- S3-a: reserved-word identifiers are quoted in emitted DDL ---

#[test]
fn sql_quotes_reserved_word_table_and_column_names() {
    let schema = r#"{
      "type":"object",
      "properties":{
        "order":{"type":"string"},
        "group":{"type":"string","enum":["a","b"]}
      },
      "required":["order"],
      "additionalProperties":false
    }"#;
    let sql = compile(&request("user", schema, Target::Sql))
        .unwrap()
        .content;
    assert!(sql.contains("CREATE TABLE \"user\""), "{sql}");
    assert!(sql.contains("\"order\" TEXT NOT NULL"), "{sql}");
    assert!(sql.contains("\"group\" TEXT"), "{sql}");
    assert!(sql.contains("CHECK (\"group\" IN ('a', 'b'))"), "{sql}");
}

#[test]
fn sql_quoting_is_total_over_ordinary_names() {
    let schema = r#"{
      "type":"object",
      "properties":{"name":{"type":"string","maxLength":10}},
      "required":["name"],
      "additionalProperties":false
    }"#;
    let sql = compile(&request("profile", schema, Target::Sql))
        .unwrap()
        .content;
    assert!(sql.contains("CREATE TABLE \"profile\""), "{sql}");
    assert!(sql.contains("\"name\" VARCHAR(10) NOT NULL"), "{sql}");
}

// --- S5-a: required + default is contradictory and rejects loudly ---

#[test]
fn required_property_with_default_rejects_naming_both_keywords() {
    let schema = r#"{
      "type":"object",
      "properties":{"role":{"type":"string","default":"member"}},
      "required":["role"],
      "additionalProperties":false
    }"#;
    for target in [Target::Zod, Target::Pydantic, Target::Rust, Target::Sql] {
        let error = compile(&request("user", schema, target)).unwrap_err();
        let message = error.to_string();
        assert!(
            matches!(error, CompileError::InvalidSchema { .. }),
            "{message}"
        );
        assert!(message.contains("role"), "{message}");
        assert!(message.contains("required"), "{message}");
        assert!(message.contains("default"), "{message}");
    }
}

#[test]
fn optional_property_with_default_still_compiles() {
    let schema = r#"{
      "type":"object",
      "properties":{"role":{"type":"string","default":"member"}},
      "required":[],
      "additionalProperties":false
    }"#;
    for target in [Target::Zod, Target::Pydantic, Target::Rust, Target::Sql] {
        compile(&request("user", schema, target)).unwrap();
    }
}