inauguration 0.2.0

.in language and general compiler CLI (Core IR, hybrid SIL, staging, plugins)
Documentation
//! Regenerate `PatchType` / `ReloadDecision` Rust + Swift enums from `shared/protocol/events.schema.json`.

use serde_json::Value;
use std::fs;
use std::path::{Path, PathBuf};

struct EnumSpec {
    rust_name: &'static str,
    swift_name: &'static str,
    schema_pointer: &'static str,
}

const ENUMS: &[EnumSpec] = &[
    EnumSpec {
        rust_name: "PatchType",
        swift_name: "GeneratedWirePatchType",
        schema_pointer: "/$defs/ReloadPatch/properties/patch_type/enum",
    },
    EnumSpec {
        rust_name: "ReloadDecision",
        swift_name: "GeneratedWireReloadDecision",
        schema_pointer: "/$defs/ReloadDecision/enum",
    },
];

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let root = workspace_root()?;
    let schema_path = root.join("shared/protocol/events.schema.json");
    let schema_raw = fs::read_to_string(&schema_path)?;
    let schema: Value = serde_json::from_str(&schema_raw)?;

    let rust_out = root.join("in-cli/src/hotreload/generated_protocol.rs");
    let swift_out =
        root.join("runtime/swift-preview-host/Sources/SwiftPreviewHost/GeneratedProtocol.swift");

    write_rust(&rust_out, &schema, ENUMS)?;
    write_swift(&swift_out, &schema, ENUMS)?;

    println!("generated: {}", rust_out.display());
    println!("generated: {}", swift_out.display());
    Ok(())
}

fn workspace_root() -> Result<PathBuf, Box<dyn std::error::Error>> {
    if let Some(arg) = std::env::args().nth(1) {
        return Ok(PathBuf::from(arg));
    }
    let mut dir = std::env::current_dir()?;
    loop {
        if dir.join("shared/protocol/events.schema.json").exists() {
            return Ok(dir);
        }
        if !dir.pop() {
            break;
        }
    }
    Err(
        "could not find repository root containing shared/protocol/events.schema.json (pass root as argv[1])"
            .into(),
    )
}

fn parse_enum_values(
    schema: &Value,
    pointer: &str,
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
    let arr = schema
        .pointer(pointer)
        .and_then(|v| v.as_array())
        .ok_or_else(|| format!("schema missing {pointer}"))?;
    let mut out = Vec::new();
    for item in arr {
        let s = item.as_str().ok_or("enum values must be strings")?;
        out.push(s.to_string());
    }
    if out.is_empty() {
        return Err(format!("enum at {pointer} is empty").into());
    }
    Ok(out)
}

fn snake_to_pascal(value: &str) -> String {
    value
        .split('_')
        .filter(|p| !p.is_empty())
        .map(|part| {
            let mut c = part.chars();
            match c.next() {
                None => String::new(),
                Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
            }
        })
        .collect()
}

fn snake_to_camel(value: &str) -> String {
    let pascal = snake_to_pascal(value);
    let mut ch = pascal.chars();
    match ch.next() {
        None => String::new(),
        Some(f) => f.to_lowercase().collect::<String>() + ch.as_str(),
    }
}

fn write_rust(
    out_path: &Path,
    schema: &Value,
    enums: &[EnumSpec],
) -> Result<(), Box<dyn std::error::Error>> {
    let mut lines: Vec<String> = vec![
        "// AUTO-GENERATED by inauguration protocol-gen (Rust)".to_string(),
        "// Source: shared/protocol/events.schema.json".to_string(),
        String::new(),
    ];
    for spec in enums {
        let values = parse_enum_values(schema, spec.schema_pointer)?;
        lines.push(
            "#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]"
                .to_string(),
        );
        lines.push("#[serde(rename_all = \"snake_case\")]".to_string());
        lines.push(format!("pub enum {} {{", spec.rust_name));
        for v in &values {
            lines.push(format!("    {},", snake_to_pascal(v)));
        }
        lines.push("}".to_string());
        lines.push(String::new());
    }
    fs::write(out_path, lines.join("\n"))?;
    Ok(())
}

fn write_swift(
    out_path: &Path,
    schema: &Value,
    enums: &[EnumSpec],
) -> Result<(), Box<dyn std::error::Error>> {
    let mut lines: Vec<String> = vec![
        "// AUTO-GENERATED by inauguration protocol-gen (Rust)".to_string(),
        "// Source: shared/protocol/events.schema.json".to_string(),
        String::new(),
    ];
    for spec in enums {
        let values = parse_enum_values(schema, spec.schema_pointer)?;
        lines.push(format!(
            "public enum {}: String, Codable, Sendable {{",
            spec.swift_name
        ));
        for v in &values {
            lines.push(format!("    case {} = \"{}\"", snake_to_camel(v), v));
        }
        lines.push("}".to_string());
        lines.push(String::new());
    }
    fs::write(out_path, lines.join("\n"))?;
    Ok(())
}