use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RepoSubstrate {
pub repo_root: String,
pub files: Vec<SubstrateFile>,
pub lang_summary: BTreeMap<String, LangSummary>,
#[serde(skip_serializing_if = "Option::is_none")]
pub diff_range: Option<DiffRange>,
pub total_tokens: usize,
pub total_bytes: usize,
pub total_code_lines: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubstrateFile {
pub path: String,
pub lang: String,
pub code: usize,
pub lines: usize,
pub bytes: usize,
pub tokens: usize,
pub module: String,
pub in_diff: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LangSummary {
pub files: usize,
pub code: usize,
pub lines: usize,
pub bytes: usize,
pub tokens: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffRange {
pub base: String,
pub head: String,
pub changed_files: Vec<String>,
pub commit_count: usize,
pub insertions: usize,
pub deletions: usize,
}
impl RepoSubstrate {
pub fn diff_files(&self) -> impl Iterator<Item = &SubstrateFile> {
self.files.iter().filter(|f| f.in_diff)
}
pub fn files_for_lang(&self, lang: &str) -> impl Iterator<Item = &SubstrateFile> {
self.files.iter().filter(move |f| f.lang == lang)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_substrate() -> RepoSubstrate {
RepoSubstrate {
repo_root: "/repo".to_string(),
files: vec![
SubstrateFile {
path: "src/lib.rs".to_string(),
lang: "Rust".to_string(),
code: 100,
lines: 120,
bytes: 3000,
tokens: 750,
module: "src".to_string(),
in_diff: true,
},
SubstrateFile {
path: "src/main.rs".to_string(),
lang: "Rust".to_string(),
code: 50,
lines: 60,
bytes: 1500,
tokens: 375,
module: "src".to_string(),
in_diff: false,
},
],
lang_summary: BTreeMap::from([(
"Rust".to_string(),
LangSummary {
files: 2,
code: 150,
lines: 180,
bytes: 4500,
tokens: 1125,
},
)]),
diff_range: Some(DiffRange {
base: "main".to_string(),
head: "HEAD".to_string(),
changed_files: vec!["src/lib.rs".to_string()],
commit_count: 3,
insertions: 10,
deletions: 5,
}),
total_tokens: 1125,
total_bytes: 4500,
total_code_lines: 150,
}
}
#[test]
fn serde_roundtrip() {
let sub = sample_substrate();
let json = serde_json::to_string(&sub).unwrap();
let back: RepoSubstrate = serde_json::from_str(&json).unwrap();
assert_eq!(back.files.len(), 2);
assert_eq!(back.total_code_lines, 150);
assert!(back.diff_range.is_some());
}
#[test]
fn diff_files_filter() {
let sub = sample_substrate();
let diff: Vec<_> = sub.diff_files().collect();
assert_eq!(diff.len(), 1);
assert_eq!(diff[0].path, "src/lib.rs");
}
#[test]
fn files_for_lang_filter() {
let sub = sample_substrate();
let rust_files: Vec<_> = sub.files_for_lang("Rust").collect();
assert_eq!(rust_files.len(), 2);
let go_files: Vec<_> = sub.files_for_lang("Go").collect();
assert_eq!(go_files.len(), 0);
}
#[test]
fn btreemap_ordering() {
let sub = sample_substrate();
let keys: Vec<_> = sub.lang_summary.keys().collect();
assert_eq!(keys, vec!["Rust"]);
}
}