blazon_core/
lib.rs

1//! blazon-core: badge generation for Rust project metrics
2
3pub mod debug;
4
5use std::collections::HashSet;
6use std::process::Command;
7
8/// Project metrics
9pub struct Metrics {
10    pub dep_count: usize,
11    pub binary_size_bytes: u64,
12}
13
14/// Count unique dependencies using cargo tree
15pub fn count_dependencies() -> Result<usize, String> {
16    let output = Command::new("cargo")
17        .args(["tree", "--edges", "normal", "--prefix", "none"])
18        .output()
19        .map_err(|e| format!("Failed to run cargo tree: {}", e))?;
20
21    if !output.status.success() {
22        return Err("cargo tree failed".to_string());
23    }
24
25    let stdout = String::from_utf8_lossy(&output.stdout);
26    let mut unique_deps: HashSet<String> = HashSet::new();
27
28    for line in stdout.lines() {
29        if let Some(dep) = line.split_whitespace().next()
30            && !dep.is_empty()
31        {
32            unique_deps.insert(dep.to_string());
33        }
34    }
35
36    Ok(unique_deps.len())
37}
38
39/// Get the main binary name from Cargo.toml
40pub fn get_binary_name() -> Result<String, String> {
41    let output = Command::new("cargo")
42        .args(["metadata", "--format-version", "1", "--no-deps"])
43        .output()
44        .map_err(|e| format!("Failed to run cargo metadata: {}", e))?;
45
46    if !output.status.success() {
47        return Err("cargo metadata failed".to_string());
48    }
49
50    let stdout = String::from_utf8_lossy(&output.stdout);
51
52    // Simple string search for binary target
53    for line in stdout.lines() {
54        if (line.contains(r#""kind":["bin"]"#) || line.contains(r#""kind": ["bin"]"#))
55            && let Some(name_line) = stdout
56                .lines()
57                .skip_while(|l| {
58                    !l.contains(r#""kind":["bin"]"#) && !l.contains(r#""kind": ["bin"]"#)
59                })
60                .find(|l| l.contains(r#""name":"#))
61            && let Some(start) = name_line.find(r#""name":""#)
62        {
63            let name_start = start + r#""name":""#.len();
64            if let Some(end) = name_line[name_start..].find('"') {
65                return Ok(name_line[name_start..name_start + end].to_string());
66            }
67        }
68    }
69
70    Err("No binary target found".to_string())
71}
72
73/// Build in release mode
74pub fn build_release() -> Result<(), String> {
75    let status = Command::new("cargo")
76        .args(["build", "--release", "--quiet"])
77        .status()
78        .map_err(|e| format!("Failed to run cargo build: {}", e))?;
79
80    if !status.success() {
81        return Err("cargo build failed".to_string());
82    }
83
84    Ok(())
85}
86
87/// Get binary size in bytes
88pub fn get_binary_size(binary_name: &str) -> Result<u64, String> {
89    let path = format!("target/release/{}", binary_name);
90    std::fs::metadata(&path)
91        .map(|m| m.len())
92        .map_err(|e| format!("Failed to get size for {}: {}", path, e))
93}
94
95/// Collect all metrics
96pub fn collect_metrics(binary_name: &str, should_build: bool) -> Result<Metrics, String> {
97    if should_build {
98        build_release()?;
99    }
100
101    let dep_count = count_dependencies()?;
102    let binary_size_bytes = get_binary_size(binary_name)?;
103
104    Ok(Metrics {
105        dep_count,
106        binary_size_bytes,
107    })
108}
109
110/// Format bytes as human-readable string
111pub fn format_size(bytes: u64) -> String {
112    if bytes < 1024 {
113        format!("{}B", bytes)
114    } else if bytes < 1024 * 1024 {
115        format!("{:.1}K", bytes as f64 / 1024.0)
116    } else {
117        format!("{:.1}M", bytes as f64 / (1024.0 * 1024.0))
118    }
119}
120
121/// Generate shields.io badge URLs and markdown
122pub fn generate_badges(metrics: &Metrics, crate_name: &str) -> String {
123    let crates_io_url = format!("https://crates.io/crates/{}", crate_name);
124    let size_formatted = format_size(metrics.binary_size_bytes);
125
126    let deps_badge = format!(
127        "[![Dependencies: {}](https://img.shields.io/badge/cargo%20tree-{}-blue)]({})",
128        metrics.dep_count, metrics.dep_count, crates_io_url
129    );
130
131    let size_badge = format!(
132        "[![Binary Size: {}](https://img.shields.io/badge/build%20size-{}-green)]({})",
133        size_formatted, size_formatted, crates_io_url
134    );
135
136    format!("{}\n{}", deps_badge, size_badge)
137}
138
139/// Update README file with generated badges using textum
140pub fn update_readme(readme_path: &str, badge_content: &str) -> Result<(), String> {
141    use textum::{Boundary, BoundaryMode, Patch, Snippet, Target};
142
143    let start = Boundary::new(
144        Target::Literal("<!-- blazon -->".to_string()),
145        BoundaryMode::Exclude,
146    );
147    let end = Boundary::new(
148        Target::Literal("<!-- /blazon -->".to_string()),
149        BoundaryMode::Exclude,
150    );
151
152    let snippet = Snippet::Between { start, end };
153
154    let patch = Patch {
155        file: Some(readme_path.to_string()),
156        snippet,
157        replacement: format!("\n{}", badge_content),
158    };
159
160    // Read the file content
161    let content = std::fs::read_to_string(readme_path)
162        .map_err(|e| format!("Failed to read {}: {}", readme_path, e))?;
163
164    // Apply patch to string
165    let updated = patch
166        .apply_to_string(&content)
167        .map_err(|e| format!("Failed to apply patch: {:?}", e))?;
168
169    // Write back
170    std::fs::write(readme_path, updated)
171        .map_err(|e| format!("Failed to write {}: {}", readme_path, e))?;
172
173    Ok(())
174}
175
176/// Macro for debug output
177#[macro_export]
178macro_rules! blazon_debug {
179    ($($arg:tt)*) => {
180        if $crate::debug::is_enabled() {
181            eprintln!("[BLAZON DEBUG] {}", format!($($arg)*));
182        }
183    };
184}