pushkin-compiler 0.1.0

Schema compiler for pushkin: canonical JSON Schema emission and generated bindings
Documentation
//! Phase 6 task 2 (R6 widening step 1, R5 precondition): Rust generation
//! adopts `typify` — `deny_unknown_fields` preserved, length/pattern
//! constraints now ENFORCED in the generated code (a fidelity gain over
//! the hand-rolled emitter), deterministic output, epoch header first.
//! The constrained-subset parser stays the loud front gate. Committed
//! per the red-locally/commit-green protocol; read-only hereafter
//! (charter §4.1, N10).

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",
      "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
    },
    "role": { "default": "member", "type": "string", "enum": ["member", "admin"] }
  },
  "required": ["name", "email"],
  "additionalProperties": false
}"#;

fn rust_binding(epoch: u32) -> Result<String, CompileError> {
    compile(&CompileRequest {
        contract_name: "user".to_owned(),
        schema_json: USER_SCHEMA.to_owned(),
        target: Target::Rust,
        epoch,
    })
    .map(|output| output.content)
}

#[test]
fn rust_binding_is_typify_generated_with_deny_unknown_fields() {
    let binding = rust_binding(1).unwrap();
    assert!(
        binding.contains("#[serde(deny_unknown_fields)]"),
        "strictness must survive the emitter swap: {binding}"
    );
    assert!(
        binding.contains("pub struct UserCreate"),
        "root type name unchanged: {binding}"
    );
    assert!(
        binding.contains("pub struct UserCreateName"),
        "constrained strings become validated newtypes: {binding}"
    );
    assert!(
        !binding.contains("fn default_role()"),
        "the hand-rolled emitter's shape must be gone: {binding}"
    );
}

#[test]
fn rust_binding_validates_length_and_pattern_in_generated_code() {
    let binding = rust_binding(1).unwrap();
    assert!(
        binding.contains("chars().count() > 200"),
        "maxLength must be enforced at construction: {binding}"
    );
    assert!(
        binding.contains("chars().count() < 1"),
        "minLength must be enforced at construction: {binding}"
    );
    assert!(
        binding.contains("::regress::Regex::new"),
        "pattern must be enforced at construction: {binding}"
    );
}

#[test]
fn rust_binding_regeneration_is_byte_identical() {
    assert_eq!(
        rust_binding(1).unwrap(),
        rust_binding(1).unwrap(),
        "typify emission must stay deterministic"
    );
}

#[test]
fn rust_binding_epoch_header_precedes_typify_doc_comments() {
    // The daemon's startup probe reads the FIRST `pushkin-epoch:` marker;
    // typify doc comments may embed the canonical schema (including its
    // own $comment), so the header must stay on top.
    let binding = rust_binding(9).unwrap();
    let header_line = binding.lines().nth(1).unwrap_or_default();
    assert!(
        header_line.contains("pushkin-epoch: 9"),
        "line 2 must be the epoch header, got: {header_line}"
    );
}

#[test]
fn unrepresentable_constructs_still_reject_before_typify() {
    // The front gate is unchanged: an integer property is outside the
    // current subset and must reject loudly BEFORE typify ever runs
    // (widening to scalars is a later, separately-gated step).
    let widened = r#"{
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "type": "object",
      "properties": { "age": { "type": "integer" } },
      "required": ["age"],
      "additionalProperties": false
    }"#;
    let err = compile(&CompileRequest {
        contract_name: "user".to_owned(),
        schema_json: widened.to_owned(),
        target: Target::Rust,
        epoch: 1,
    })
    .unwrap_err();
    let message = err.to_string();
    assert!(
        message.contains("integer") || message.contains("age"),
        "rejection must name the construct: {message}"
    );
}