alef-core 0.7.2

Core types, config schema, and backend trait for the alef polyglot binding generator
Documentation
//! Content hashing and generated-file headers.
//!
//! Every file produced by alef gets a standard header that identifies it as
//! generated, tells agents/developers how to fix issues, and embeds a blake3
//! content hash so `alef verify` can detect staleness without external state.

const HASH_PREFIX: &str = "alef:hash:";

/// The standard header text (without comment delimiters).
/// Used by [`header`] to produce language-specific comment blocks.
const HEADER_BODY: &str = "\
This file is auto-generated by alef — DO NOT EDIT.
To regenerate: alef generate
To verify freshness: alef verify --exit-code
Issues & docs: https://github.com/kreuzberg-dev/alef";

/// Comment style for the generated header.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommentStyle {
    /// `// line comment`  (Rust, Go, Java, C#, TypeScript, C, PHP)
    DoubleSlash,
    /// `# line comment`   (Python, Ruby, Elixir, R, TOML, Shell, Makefile)
    Hash,
    /// `/* block comment */` (C headers)
    Block,
}

/// Return the standard alef header as a comment block.
///
/// ```text
/// // This file is auto-generated by alef — DO NOT EDIT.
/// // To regenerate: alef generate
/// // To verify freshness: alef verify --exit-code
/// // Issues & docs: https://github.com/kreuzberg-dev/alef
/// ```
pub fn header(style: CommentStyle) -> String {
    match style {
        CommentStyle::DoubleSlash => HEADER_BODY.lines().map(|l| format!("// {l}\n")).collect(),
        CommentStyle::Hash => HEADER_BODY.lines().map(|l| format!("# {l}\n")).collect(),
        CommentStyle::Block => {
            let mut out = String::from("/*\n");
            for line in HEADER_BODY.lines() {
                out.push_str(&format!(" * {line}\n"));
            }
            out.push_str(" */\n");
            out
        }
    }
}

/// The marker string that `inject_hash_line` and `extract_hash` look for.
/// Every alef-generated header contains this on the first line.
const HEADER_MARKER: &str = "auto-generated by alef";

/// Blake3 hash of a content string, returned as hex.
pub fn hash_content(content: &str) -> String {
    blake3::hash(content.as_bytes()).to_hex().to_string()
}

/// Inject an `alef:hash:<hex>` line immediately after the first header marker
/// line found in the first 10 lines.  The comment syntax is inferred from the
/// marker line itself.
///
/// If no marker line is found, the content is returned unchanged.
pub fn inject_hash_line(content: &str, hash: &str) -> String {
    let mut result = String::with_capacity(content.len() + 80);
    let mut injected = false;

    for (i, line) in content.lines().enumerate() {
        result.push_str(line);
        result.push('\n');

        if !injected && i < 10 && line.contains(HEADER_MARKER) {
            let trimmed = line.trim();
            let hash_line = if trimmed.starts_with("//") {
                format!("// {HASH_PREFIX}{hash}")
            } else if trimmed.starts_with('#') {
                format!("# {HASH_PREFIX}{hash}")
            } else if trimmed.starts_with("/*") || trimmed.starts_with(" *") || trimmed.ends_with("*/") {
                format!(" * {HASH_PREFIX}{hash}")
            } else {
                format!("// {HASH_PREFIX}{hash}")
            };
            result.push_str(&hash_line);
            result.push('\n');
            injected = true;
        }
    }

    // Preserve original trailing-newline behavior.
    if !content.ends_with('\n') && result.ends_with('\n') {
        result.pop();
    }

    result
}

/// Extract the hash from an `alef:hash:<hex>` token in the first 10 lines.
pub fn extract_hash(content: &str) -> Option<String> {
    for (i, line) in content.lines().enumerate() {
        if i >= 10 {
            break;
        }
        if let Some(pos) = line.find(HASH_PREFIX) {
            let rest = &line[pos + HASH_PREFIX.len()..];
            // Trim trailing comment closers and whitespace.
            let hex = rest.trim().trim_end_matches("*/").trim();
            if !hex.is_empty() {
                return Some(hex.to_string());
            }
        }
    }
    None
}

/// Strip the `alef:hash:` line from content (for fallback comparison).
pub fn strip_hash_line(content: &str) -> String {
    let mut result = String::with_capacity(content.len());
    for line in content.lines() {
        if line.contains(HASH_PREFIX) {
            continue;
        }
        result.push_str(line);
        result.push('\n');
    }
    // Preserve original trailing-newline behavior.
    if !content.ends_with('\n') && result.ends_with('\n') {
        result.pop();
    }
    result
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_header_double_slash() {
        let h = header(CommentStyle::DoubleSlash);
        assert!(h.contains("// This file is auto-generated by alef"));
        assert!(h.contains("// Issues & docs: https://github.com/kreuzberg-dev/alef"));
    }

    #[test]
    fn test_header_hash() {
        let h = header(CommentStyle::Hash);
        assert!(h.contains("# This file is auto-generated by alef"));
    }

    #[test]
    fn test_header_block() {
        let h = header(CommentStyle::Block);
        assert!(h.starts_with("/*\n"));
        assert!(h.contains(" * This file is auto-generated by alef"));
        assert!(h.ends_with(" */\n"));
    }

    #[test]
    fn test_inject_and_extract_rust() {
        let h = header(CommentStyle::DoubleSlash);
        let content = format!("{h}use foo;\n");
        let hash = hash_content(&content);
        let injected = inject_hash_line(&content, &hash);
        assert!(injected.contains(HASH_PREFIX));
        assert_eq!(extract_hash(&injected), Some(hash));
    }

    #[test]
    fn test_inject_and_extract_python() {
        let h = header(CommentStyle::Hash);
        let content = format!("{h}import foo\n");
        let hash = hash_content(&content);
        let injected = inject_hash_line(&content, &hash);
        assert!(injected.contains(&format!("# {HASH_PREFIX}")));
        assert_eq!(extract_hash(&injected), Some(hash));
    }

    #[test]
    fn test_inject_and_extract_c_block() {
        let h = header(CommentStyle::Block);
        let content = format!("{h}#include <stdio.h>\n");
        let hash = hash_content(&content);
        let injected = inject_hash_line(&content, &hash);
        assert!(injected.contains(HASH_PREFIX));
        assert_eq!(extract_hash(&injected), Some(hash));
    }

    #[test]
    fn test_inject_php_line2() {
        let h = header(CommentStyle::DoubleSlash);
        let content = format!("<?php\n{h}namespace Foo;\n");
        let hash = hash_content(&content);
        let injected = inject_hash_line(&content, &hash);
        let lines: Vec<&str> = injected.lines().collect();
        assert_eq!(lines[0], "<?php");
        assert!(lines[1].contains(HEADER_MARKER));
        assert!(lines.iter().any(|l| l.contains(HASH_PREFIX)));
        assert_eq!(extract_hash(&injected), Some(hash));
    }

    #[test]
    fn test_no_header_returns_unchanged() {
        let content = "fn main() {}\n";
        let injected = inject_hash_line(content, "abc123");
        assert_eq!(injected, content);
        assert_eq!(extract_hash(&injected), None);
    }

    #[test]
    fn test_strip_hash_line() {
        let content = "// auto-generated by alef\n// alef:hash:abc123\nuse foo;\n";
        let stripped = strip_hash_line(content);
        assert_eq!(stripped, "// auto-generated by alef\nuse foo;\n");
    }

    #[test]
    fn test_roundtrip() {
        let h = header(CommentStyle::Hash);
        let original = format!("{h}import sys\n");
        let hash = hash_content(&original);
        let injected = inject_hash_line(&original, &hash);
        let stripped = strip_hash_line(&injected);
        assert_eq!(stripped, original);
        assert_eq!(hash_content(&stripped), hash);
    }
}