harn-cli 0.8.68

CLI for the Harn programming language — run, test, REPL, format, and lint
//! `harn connector-schema-codegen` — generate the Rust normalized-event
//! structs from the canonical Harn schema module.
//!
//! Today the normalized-event Rust structs (the `GitHubEventPayload` family in
//! `crates/harn-vm/src/triggers/event/payloads.rs`) are hand-written, and
//! `.harn` connectors emit JSON that the boundary wraps untyped. This command
//! inverts the source of truth: the canonical schema is authored ONCE as Harn
//! `type` declarations (`crates/harn-stdlib/src/stdlib/stdlib_event_schemas.harn`,
//! embedded as [`harn_stdlib::CONNECTOR_EVENT_SCHEMAS_SOURCE`]) and the Rust
//! structs are generated from them, so a connector's output matches the Rust
//! struct by construction.
//!
//! It mirrors `pg_codegen`'s read -> parse -> emit -> write/check flow and the
//! `dump-protocol-artifacts` gen + CI-staleness-check workflow. `--check`
//! re-renders and compares against the vendored output without writing, exiting
//! non-zero on drift — wired into CI via `make check-connector-schemas`.

mod emit;

use std::path::Path;

use crate::cli::ConnectorSchemaCodegenArgs;

/// The vendored generated file, relative to the repo root.
const DEFAULT_OUTPUT: &str = "crates/harn-vm/src/triggers/event/schemas_generated.rs";

pub(crate) fn run(args: &ConnectorSchemaCodegenArgs) -> i32 {
    match run_inner(args) {
        Ok(code) => code,
        Err(message) => {
            eprintln!("harn connector-schema-codegen: {message}");
            1
        }
    }
}

fn run_inner(args: &ConnectorSchemaCodegenArgs) -> Result<i32, String> {
    let source = schema_source(args.schema.as_deref())?;
    let header = header_for();
    let rendered = emit::render(&source, &header)?;

    let out = Path::new(args.out.as_deref().unwrap_or(DEFAULT_OUTPUT));

    if args.check {
        return Ok(check_against(out, &rendered));
    }

    harn_vm::atomic_io::atomic_write(out, rendered.as_bytes())
        .map_err(|error| format!("failed to write {}: {error}", out.display()))?;
    eprintln!("wrote {}", out.display());
    Ok(0)
}

/// Read the schema source. Defaults to the embedded canonical copy so the
/// generator does not depend on the current working directory; an explicit
/// `--schema` path overrides it (used by tests and local experiments).
fn schema_source(schema_path: Option<&str>) -> Result<String, String> {
    match schema_path {
        Some(path) => std::fs::read_to_string(path)
            .map_err(|error| format!("failed to read schema module {path}: {error}")),
        None => Ok(harn_stdlib::CONNECTOR_EVENT_SCHEMAS_SOURCE.to_string()),
    }
}

fn check_against(out: &Path, rendered: &str) -> i32 {
    match std::fs::read_to_string(out) {
        Ok(existing) if normalize(&existing) == normalize(rendered) => 0,
        Ok(_) => {
            eprintln!(
                "{} is out of date; regenerate with `make gen-connector-schemas`",
                out.display()
            );
            1
        }
        Err(_) => {
            eprintln!(
                "{} is missing; generate it with `make gen-connector-schemas`",
                out.display()
            );
            1
        }
    }
}

/// Compare ignoring line-ending differences so the check passes on Windows
/// checkouts (mirrors `dump-protocol-artifacts`).
fn normalize(text: &str) -> String {
    text.replace("\r\n", "\n")
}

fn header_for() -> String {
    HEADER.to_string()
}

const HEADER: &str = "\
// DO NOT EDIT — generated by `harn connector-schema-codegen`.
//
// Source of truth: crates/harn-stdlib/src/stdlib/stdlib_event_schemas.harn
// Regenerate with: make gen-connector-schemas
// Verify (CI):     make check-connector-schemas
//
// These structs are generated from canonical Harn `type` declarations so a
// connector's normalized-event JSON matches the Rust struct by construction.
// This file currently coexists with the hand-written `GitHubEventPayload`
// family in `payloads.rs`; switching the trigger boundary to produce these
// typed payloads is a separate follow-up.

";

#[cfg(test)]
mod tests;