1use serde::Serialize;
4
5use crate::source_fingerprint::SourceFingerprint;
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
9pub struct AuditConfigFingerprint {
10 pub path: Option<String>,
12 pub resolved_hash: Option<String>,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
18pub struct AuditCoverageFingerprint {
19 pub path: String,
21 pub resolved_path: String,
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub source: Option<SourceFingerprint>,
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub content_hash: Option<String>,
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub len: Option<usize>,
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub error: Option<String>,
35}
36
37#[derive(Debug, Clone, PartialEq, Serialize)]
39pub struct AuditCacheKeyPayload {
40 pub cache_version: u8,
42 pub cli_version: String,
44 pub base_sha: String,
46 pub config_file: AuditConfigFingerprint,
48 pub changed_files: Vec<String>,
50 pub production: bool,
52 pub production_dead_code: Option<bool>,
54 pub production_health: Option<bool>,
56 pub production_dupes: Option<bool>,
58 pub workspace: Option<Vec<String>>,
60 pub changed_workspaces: Option<String>,
62 pub group_by: Option<String>,
64 pub include_entry_exports: bool,
66 pub max_crap: Option<f64>,
68 pub coverage: Option<AuditCoverageFingerprint>,
70 pub coverage_root: Option<String>,
72 pub css: bool,
74 pub css_deep: bool,
76 pub dead_code_baseline: Option<String>,
78 pub health_baseline: Option<String>,
80 pub dupes_baseline: Option<String>,
82}
83
84#[derive(Debug, Clone)]
86pub struct AuditCacheKeyBuilder {
87 payload: AuditCacheKeyPayload,
88}
89
90impl AuditCacheKeyBuilder {
91 #[must_use]
93 pub fn new(
94 cache_version: u8,
95 cli_version: impl Into<String>,
96 base_sha: impl Into<String>,
97 config_file: AuditConfigFingerprint,
98 changed_files: Vec<String>,
99 ) -> Self {
100 Self {
101 payload: AuditCacheKeyPayload {
102 cache_version,
103 cli_version: cli_version.into(),
104 base_sha: base_sha.into(),
105 config_file,
106 changed_files,
107 production: false,
108 production_dead_code: None,
109 production_health: None,
110 production_dupes: None,
111 workspace: None,
112 changed_workspaces: None,
113 group_by: None,
114 include_entry_exports: false,
115 max_crap: None,
116 coverage: None,
117 coverage_root: None,
118 css: false,
119 css_deep: false,
120 dead_code_baseline: None,
121 health_baseline: None,
122 dupes_baseline: None,
123 },
124 }
125 }
126
127 #[must_use]
129 pub const fn production(
130 mut self,
131 production: bool,
132 dead_code: Option<bool>,
133 health: Option<bool>,
134 dupes: Option<bool>,
135 ) -> Self {
136 self.payload.production = production;
137 self.payload.production_dead_code = dead_code;
138 self.payload.production_health = health;
139 self.payload.production_dupes = dupes;
140 self
141 }
142
143 #[must_use]
145 pub fn scope(
146 mut self,
147 workspace: Option<Vec<String>>,
148 changed_workspaces: Option<String>,
149 group_by: Option<String>,
150 include_entry_exports: bool,
151 ) -> Self {
152 self.payload.workspace = workspace;
153 self.payload.changed_workspaces = changed_workspaces;
154 self.payload.group_by = group_by;
155 self.payload.include_entry_exports = include_entry_exports;
156 self
157 }
158
159 #[must_use]
161 pub fn health(
162 mut self,
163 max_crap: Option<f64>,
164 coverage: Option<AuditCoverageFingerprint>,
165 coverage_root: Option<String>,
166 ) -> Self {
167 self.payload.max_crap = max_crap;
168 self.payload.coverage = coverage;
169 self.payload.coverage_root = coverage_root;
170 self
171 }
172
173 #[must_use]
175 pub const fn styling(mut self, css: bool, css_deep: bool) -> Self {
176 self.payload.css = css;
177 self.payload.css_deep = css_deep;
178 self
179 }
180
181 #[must_use]
183 pub fn baselines(
184 mut self,
185 dead_code: Option<String>,
186 health: Option<String>,
187 dupes: Option<String>,
188 ) -> Self {
189 self.payload.dead_code_baseline = dead_code;
190 self.payload.health_baseline = health;
191 self.payload.dupes_baseline = dupes;
192 self
193 }
194
195 #[must_use]
197 pub const fn payload(&self) -> &AuditCacheKeyPayload {
198 &self.payload
199 }
200
201 pub fn to_json_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
207 serde_json::to_vec(&self.payload)
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 fn config() -> AuditConfigFingerprint {
216 AuditConfigFingerprint {
217 path: Some("fallow.toml".to_string()),
218 resolved_hash: Some("abc".to_string()),
219 }
220 }
221
222 #[test]
223 fn audit_cache_key_builder_preserves_typed_fields() {
224 let coverage = AuditCoverageFingerprint {
225 path: "coverage".to_string(),
226 resolved_path: "coverage/coverage-final.json".to_string(),
227 source: Some(SourceFingerprint::new(12, 34)),
228 content_hash: Some("hash".to_string()),
229 len: Some(34),
230 error: None,
231 };
232
233 let builder =
234 AuditCacheKeyBuilder::new(3, "1.2.3", "abc123", config(), vec!["src/a.ts".to_string()])
235 .production(true, Some(false), Some(true), None)
236 .scope(
237 Some(vec!["web".to_string()]),
238 Some("main".to_string()),
239 Some("Package".to_string()),
240 true,
241 )
242 .health(Some(42.0), Some(coverage), Some("/workspace".to_string()))
243 .styling(true, true)
244 .baselines(
245 Some("dead.json".to_string()),
246 Some("health.json".to_string()),
247 Some("dupes.json".to_string()),
248 );
249
250 let payload = builder.payload();
251 assert_eq!(payload.cache_version, 3);
252 assert_eq!(payload.base_sha, "abc123");
253 assert_eq!(payload.workspace.as_deref(), Some(&["web".to_string()][..]));
254 assert!(payload.include_entry_exports);
255 assert!(payload.css);
256 assert!(payload.css_deep);
257 assert_eq!(
258 payload.coverage.as_ref().and_then(|c| c.source),
259 Some(SourceFingerprint::new(12, 34))
260 );
261 }
262
263 #[test]
264 fn audit_cache_key_bytes_reflect_changed_file_order() {
265 let first = AuditCacheKeyBuilder::new(
266 1,
267 "1.0.0",
268 "base",
269 config(),
270 vec!["src/a.ts".to_string(), "src/b.ts".to_string()],
271 )
272 .to_json_bytes()
273 .expect("payload should serialize");
274 let second = AuditCacheKeyBuilder::new(
275 1,
276 "1.0.0",
277 "base",
278 config(),
279 vec!["src/b.ts".to_string(), "src/a.ts".to_string()],
280 )
281 .to_json_bytes()
282 .expect("payload should serialize");
283
284 assert_ne!(first, second);
285 }
286}