harn-cli 0.8.87

CLI for the Harn programming language — run, test, REPL, format, and lint
use std::fs;
use std::path::{Path, PathBuf};

pub(super) fn collapse_repeated_underscores(value: &str) -> String {
    let mut out = String::with_capacity(value.len());
    let mut last_underscore = false;
    for ch in value.chars() {
        if ch == '_' {
            if !last_underscore {
                out.push(ch);
            }
            last_underscore = true;
        } else {
            out.push(ch);
            last_underscore = false;
        }
    }
    out
}

pub(super) fn generated_header(command: &str, language: &str) -> String {
    match language {
        "typescript" => format!(
            "// GENERATED by `{command}` - do not edit by hand.\n\
             // Source: Harn adapter schemas and Rust wire vocabulary.\n\n"
        ),
        "swift" => format!(
            "// GENERATED by `{command}` - do not edit by hand.\n\
             // Source: Harn adapter schemas and Rust wire vocabulary.\n\n"
        ),
        _ => String::new(),
    }
}

pub(super) fn schema_provenance(relative_path: &str) -> Result<serde_json::Value, String> {
    let source: serde_json::Value = serde_json::from_str(&read_repo_text(relative_path)?)
        .map_err(|error| format!("failed to parse {relative_path}: {error}"))?;
    Ok(source
        .get("x-harn-provenance")
        .cloned()
        .unwrap_or(serde_json::Value::Null))
}

pub(super) fn read_repo_text(relative_path: &str) -> Result<String, String> {
    let path = repo_root().join(relative_path);
    fs::read_to_string(&path).map_err(|error| format!("failed to read {}: {error}", path.display()))
}

pub(super) fn repo_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).join("..").join("..")
}

/// Output is also valid TypeScript/Swift/Python/Go because all four share
/// JSON's escape rules for the characters in our wire vocabulary
/// (printable ASCII plus `/` and `_`).
pub(super) fn json_string_literal(value: &str) -> String {
    serde_json::to_string(value).expect("string serializes")
}

pub(super) fn ensure_trailing_newline(mut text: String) -> String {
    if !text.ends_with('\n') {
        text.push('\n');
    }
    text
}

pub(super) fn normalize_line_endings(text: &str) -> String {
    text.replace("\r\n", "\n").replace('\r', "\n")
}