use serde::Serialize;
use crate::source_fingerprint::SourceFingerprint;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AuditConfigFingerprint {
pub path: Option<String>,
pub resolved_hash: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AuditCoverageFingerprint {
pub path: String,
pub resolved_path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<SourceFingerprint>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content_hash: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub len: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct AuditCacheKeyPayload {
pub cache_version: u8,
pub cli_version: String,
pub base_sha: String,
pub config_file: AuditConfigFingerprint,
pub changed_files: Vec<String>,
pub production: bool,
pub production_dead_code: Option<bool>,
pub production_health: Option<bool>,
pub production_dupes: Option<bool>,
pub workspace: Option<Vec<String>>,
pub changed_workspaces: Option<String>,
pub group_by: Option<String>,
pub include_entry_exports: bool,
pub max_crap: Option<f64>,
pub coverage: Option<AuditCoverageFingerprint>,
pub coverage_root: Option<String>,
pub dead_code_baseline: Option<String>,
pub health_baseline: Option<String>,
pub dupes_baseline: Option<String>,
}
#[derive(Debug, Clone)]
pub struct AuditCacheKeyBuilder {
payload: AuditCacheKeyPayload,
}
impl AuditCacheKeyBuilder {
#[must_use]
pub fn new(
cache_version: u8,
cli_version: impl Into<String>,
base_sha: impl Into<String>,
config_file: AuditConfigFingerprint,
changed_files: Vec<String>,
) -> Self {
Self {
payload: AuditCacheKeyPayload {
cache_version,
cli_version: cli_version.into(),
base_sha: base_sha.into(),
config_file,
changed_files,
production: false,
production_dead_code: None,
production_health: None,
production_dupes: None,
workspace: None,
changed_workspaces: None,
group_by: None,
include_entry_exports: false,
max_crap: None,
coverage: None,
coverage_root: None,
dead_code_baseline: None,
health_baseline: None,
dupes_baseline: None,
},
}
}
#[must_use]
pub const fn production(
mut self,
production: bool,
dead_code: Option<bool>,
health: Option<bool>,
dupes: Option<bool>,
) -> Self {
self.payload.production = production;
self.payload.production_dead_code = dead_code;
self.payload.production_health = health;
self.payload.production_dupes = dupes;
self
}
#[must_use]
pub fn scope(
mut self,
workspace: Option<Vec<String>>,
changed_workspaces: Option<String>,
group_by: Option<String>,
include_entry_exports: bool,
) -> Self {
self.payload.workspace = workspace;
self.payload.changed_workspaces = changed_workspaces;
self.payload.group_by = group_by;
self.payload.include_entry_exports = include_entry_exports;
self
}
#[must_use]
pub fn health(
mut self,
max_crap: Option<f64>,
coverage: Option<AuditCoverageFingerprint>,
coverage_root: Option<String>,
) -> Self {
self.payload.max_crap = max_crap;
self.payload.coverage = coverage;
self.payload.coverage_root = coverage_root;
self
}
#[must_use]
pub fn baselines(
mut self,
dead_code: Option<String>,
health: Option<String>,
dupes: Option<String>,
) -> Self {
self.payload.dead_code_baseline = dead_code;
self.payload.health_baseline = health;
self.payload.dupes_baseline = dupes;
self
}
#[must_use]
pub const fn payload(&self) -> &AuditCacheKeyPayload {
&self.payload
}
pub fn to_json_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
serde_json::to_vec(&self.payload)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn config() -> AuditConfigFingerprint {
AuditConfigFingerprint {
path: Some("fallow.toml".to_string()),
resolved_hash: Some("abc".to_string()),
}
}
#[test]
fn audit_cache_key_builder_preserves_typed_fields() {
let coverage = AuditCoverageFingerprint {
path: "coverage".to_string(),
resolved_path: "coverage/coverage-final.json".to_string(),
source: Some(SourceFingerprint::new(12, 34)),
content_hash: Some("hash".to_string()),
len: Some(34),
error: None,
};
let builder =
AuditCacheKeyBuilder::new(3, "1.2.3", "abc123", config(), vec!["src/a.ts".to_string()])
.production(true, Some(false), Some(true), None)
.scope(
Some(vec!["web".to_string()]),
Some("main".to_string()),
Some("Package".to_string()),
true,
)
.health(Some(42.0), Some(coverage), Some("/workspace".to_string()))
.baselines(
Some("dead.json".to_string()),
Some("health.json".to_string()),
Some("dupes.json".to_string()),
);
let payload = builder.payload();
assert_eq!(payload.cache_version, 3);
assert_eq!(payload.base_sha, "abc123");
assert_eq!(payload.workspace.as_deref(), Some(&["web".to_string()][..]));
assert!(payload.include_entry_exports);
assert_eq!(
payload.coverage.as_ref().and_then(|c| c.source),
Some(SourceFingerprint::new(12, 34))
);
}
#[test]
fn audit_cache_key_bytes_reflect_changed_file_order() {
let first = AuditCacheKeyBuilder::new(
1,
"1.0.0",
"base",
config(),
vec!["src/a.ts".to_string(), "src/b.ts".to_string()],
)
.to_json_bytes()
.expect("payload should serialize");
let second = AuditCacheKeyBuilder::new(
1,
"1.0.0",
"base",
config(),
vec!["src/b.ts".to_string(), "src/a.ts".to_string()],
)
.to_json_bytes()
.expect("payload should serialize");
assert_ne!(first, second);
}
}