pushkin-compiler 0.2.1

Schema compiler for pushkin: canonical JSON Schema emission and generated bindings
Documentation
//! Regression coverage for the identifier-totality remediation (S1-S2):
//! the NAME side of compiler totality. S1 — the structural walk must not
//! keyword-match user-defined property names. S2 — property and contract
//! names outside `^[A-Za-z_][A-Za-z0-9_]*$` are loud rejections, never
//! emitted verbatim into target code.

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

const ALL_TARGETS: [Target; 4] = [Target::Zod, Target::Pydantic, Target::Rust, Target::Sql];

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,
    }
}

fn object_schema(properties_json: &str) -> String {
    format!(
        r#"{{"type":"object","properties":{properties_json},"required":[],"additionalProperties":false}}"#
    )
}

// --- S1: property names are user data, not structural keywords ---

#[test]
fn property_named_like_structural_keyword_compiles() {
    let schema = object_schema(
        r#"{
          "not":{"type":"string"},
          "allOf":{"type":"string"},
          "anyOf":{"type":"string"},
          "oneOf":{"type":"string"},
          "patternProperties":{"type":"string"},
          "prefixItems":{"type":"string"}
        }"#,
    );
    for target in ALL_TARGETS {
        compile(&request("remediation", &schema, target)).unwrap();
    }
}

#[test]
fn structural_keyword_inside_property_spec_still_rejects() {
    let schema = object_schema(r#"{"value":{"type":"string","allOf":[]}}"#);
    let error = compile(&request("remediation", &schema, Target::Zod)).unwrap_err();
    let message = error.to_string();
    assert!(matches!(error, CompileError::Unrepresentable { .. }));
    assert!(message.contains("allOf"), "{message}");
}

#[test]
fn structural_keyword_at_top_level_still_rejects() {
    let schema = r#"{"type":"object","properties":{},"not":{},"additionalProperties":false}"#;
    let error = compile(&request("remediation", schema, Target::Pydantic)).unwrap_err();
    assert!(matches!(error, CompileError::Unrepresentable { .. }));
}

// --- S2: names must be portable identifiers across TS/Python/Rust/SQL ---

#[test]
fn property_name_outside_identifier_pattern_rejects() {
    for name in ["first name", "first-name", "1st", "", "naïve", "a.b"] {
        let schema = object_schema(&format!(r#"{{"{name}":{{"type":"string"}}}}"#));
        for target in ALL_TARGETS {
            let error = compile(&request("remediation", &schema, target)).unwrap_err();
            let message = error.to_string();
            assert!(
                matches!(error, CompileError::InvalidSchema { .. }),
                "property {name:?} must reject as invalid schema, got: {message}"
            );
            assert!(message.contains("[A-Za-z_][A-Za-z0-9_]*"), "{message}");
        }
    }
}

#[test]
fn dollar_ref_property_name_rejects_as_identifier_not_structural() {
    let schema = object_schema(r#"{"$ref":{"type":"string"}}"#);
    let error = compile(&request("remediation", &schema, Target::Rust)).unwrap_err();
    let message = error.to_string();
    assert!(
        matches!(error, CompileError::InvalidSchema { .. }),
        "'$ref' as a property NAME is an identifier problem, not a structural construct: {message}"
    );
    assert!(message.contains("[A-Za-z_][A-Za-z0-9_]*"), "{message}");
}

#[test]
fn contract_name_outside_identifier_pattern_rejects() {
    let schema = object_schema(r#"{"value":{"type":"string"}}"#);
    for contract_name in ["user profile", "user-profile", "2user", "", "user;drop"] {
        for target in ALL_TARGETS {
            let error = compile(&request(contract_name, &schema, target)).unwrap_err();
            let message = error.to_string();
            assert!(
                matches!(error, CompileError::InvalidSchema { .. }),
                "contract {contract_name:?} must reject, got: {message}"
            );
            assert!(message.contains("[A-Za-z_][A-Za-z0-9_]*"), "{message}");
        }
    }
}

#[test]
fn valid_identifier_names_compile_across_targets() {
    let schema = object_schema(r#"{"_private":{"type":"string"},"name2":{"type":"string"}}"#);
    for target in ALL_TARGETS {
        compile(&request("user_create", &schema, target)).unwrap();
    }
}