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.trim().split_whitespace().next() {
30            if !dep.is_empty() {
31                unique_deps.insert(dep.to_string());
32            }
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            if 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            {
62                if let Some(start) = name_line.find(r#""name":""#) {
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    }
71
72    Err("No binary target found".to_string())
73}
74
75/// Build in release mode
76pub fn build_release() -> Result<(), String> {
77    let status = Command::new("cargo")
78        .args(["build", "--release", "--quiet"])
79        .status()
80        .map_err(|e| format!("Failed to run cargo build: {}", e))?;
81
82    if !status.success() {
83        return Err("cargo build failed".to_string());
84    }
85
86    Ok(())
87}
88
89/// Get binary size in bytes
90pub fn get_binary_size(binary_name: &str) -> Result<u64, String> {
91    let path = format!("target/release/{}", binary_name);
92    std::fs::metadata(&path)
93        .map(|m| m.len())
94        .map_err(|e| format!("Failed to get size for {}: {}", path, e))
95}
96
97/// Collect all metrics
98pub fn collect_metrics(binary_name: &str, should_build: bool) -> Result<Metrics, String> {
99    if should_build {
100        build_release()?;
101    }
102
103    let dep_count = count_dependencies()?;
104    let binary_size_bytes = get_binary_size(binary_name)?;
105
106    Ok(Metrics {
107        dep_count,
108        binary_size_bytes,
109    })
110}
111
112/// Format bytes as human-readable string
113pub fn format_size(bytes: u64) -> String {
114    if bytes < 1024 {
115        format!("{}B", bytes)
116    } else if bytes < 1024 * 1024 {
117        format!("{:.1}K", bytes as f64 / 1024.0)
118    } else {
119        format!("{:.1}M", bytes as f64 / (1024.0 * 1024.0))
120    }
121}
122
123/// Generate shields.io badge URLs and markdown
124pub fn generate_badges(metrics: &Metrics, crate_name: &str) -> String {
125    let crates_io_url = format!("https://crates.io/crates/{}", crate_name);
126    let size_formatted = format_size(metrics.binary_size_bytes);
127
128    let deps_badge = format!(
129        "[![Dependencies: {}](https://img.shields.io/badge/cargo%20tree-{}-blue)]({})",
130        metrics.dep_count, metrics.dep_count, crates_io_url
131    );
132
133    let size_badge = format!(
134        "[![Binary Size: {}](https://img.shields.io/badge/build%20size-{}-green)]({})",
135        size_formatted, size_formatted, crates_io_url
136    );
137
138    format!("{}\n{}", deps_badge, size_badge)
139}
140
141/// Update README file with generated badges using textum
142pub fn update_readme(readme_path: &str, badge_content: &str) -> Result<(), String> {
143    use textum::{Boundary, BoundaryMode, Patch, Snippet, Target};
144
145    let start = Boundary::new(
146        Target::Literal("<!-- auto-generated badges -->".to_string()),
147        BoundaryMode::Exclude,
148    );
149    let end = Boundary::new(
150        Target::Literal("<!-- /auto-generated badges -->".to_string()),
151        BoundaryMode::Exclude,
152    );
153
154    let snippet = Snippet::Between { start, end };
155
156    let patch = Patch {
157        file: Some(readme_path.to_string()),
158        snippet,
159        replacement: format!("\n{}\n", badge_content),
160    };
161
162    // Read the file content
163    let content = std::fs::read_to_string(readme_path)
164        .map_err(|e| format!("Failed to read {}: {}", readme_path, e))?;
165
166    // Apply patch to string
167    let updated = patch
168        .apply_to_string(&content)
169        .map_err(|e| format!("Failed to apply patch: {:?}", e))?;
170
171    // Write back
172    std::fs::write(readme_path, updated)
173        .map_err(|e| format!("Failed to write {}: {}", readme_path, e))?;
174
175    Ok(())
176}
177
178/// Macro for debug output
179#[macro_export]
180macro_rules! blazon_debug {
181    ($($arg:tt)*) => {
182        if $crate::debug::is_enabled() {
183            eprintln!("[BLAZON DEBUG] {}", format!($($arg)*));
184        }
185    };
186}