harn-cli 0.10.28

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

#[derive(Debug)]
pub(super) struct ProtocolArtifactSource {
    repo_root: PathBuf,
}

impl ProtocolArtifactSource {
    pub(super) fn discover() -> Result<Self, String> {
        let cwd = std::env::current_dir()
            .map_err(|error| format!("failed to inspect the current directory: {error}"))?;
        Self::from_anchor(&cwd)
    }

    pub(super) fn from_anchor(anchor: &Path) -> Result<Self, String> {
        let repo_root = repo_root_from(anchor).ok_or_else(|| {
            format!(
                "could not find the Harn workspace above {}; run this command from a Harn checkout",
                anchor.display()
            )
        })?;
        Ok(Self { repo_root })
    }

    #[cfg(test)]
    pub(super) fn repo_root(&self) -> &Path {
        &self.repo_root
    }

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

    pub(super) fn schema_provenance(
        &self,
        relative_path: &str,
    ) -> Result<serde_json::Value, String> {
        let source: serde_json::Value = serde_json::from_str(&self.read_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 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 repo_root_from(start: &Path) -> Option<PathBuf> {
    start
        .ancestors()
        .find(|dir| {
            dir.join("Cargo.toml").is_file() && dir.join("conformance/protocols/schemas").is_dir()
        })
        .map(Path::to_path_buf)
}

/// 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")
}

pub(super) fn concat_unique_wire_values(parts: &[&[&str]]) -> Vec<String> {
    let mut out = Vec::new();
    for part in parts {
        for value in *part {
            if !out.iter().any(|existing| existing == value) {
                out.push((*value).to_string());
            }
        }
    }
    out
}