Skip to main content

cc_audit/types/
newtypes.rs

1//! NewType wrappers for primitive types.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6/// Git reference (branch, tag, or commit hash).
7///
8/// Wraps a string to provide type safety when passing git refs.
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(transparent)]
11pub struct GitRef(String);
12
13impl GitRef {
14    /// Create a new GitRef from any string-like type.
15    pub fn new(s: impl Into<String>) -> Self {
16        Self(s.into())
17    }
18
19    /// Get the underlying string reference.
20    pub fn as_str(&self) -> &str {
21        &self.0
22    }
23
24    /// Consume self and return the inner String.
25    pub fn into_inner(self) -> String {
26        self.0
27    }
28}
29
30impl Default for GitRef {
31    fn default() -> Self {
32        Self("HEAD".to_string())
33    }
34}
35
36impl From<&str> for GitRef {
37    fn from(s: &str) -> Self {
38        Self(s.to_string())
39    }
40}
41
42impl From<String> for GitRef {
43    fn from(s: String) -> Self {
44        Self(s)
45    }
46}
47
48impl AsRef<str> for GitRef {
49    fn as_ref(&self) -> &str {
50        &self.0
51    }
52}
53
54impl fmt::Display for GitRef {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        write!(f, "{}", self.0)
57    }
58}
59
60/// GitHub/Git authentication token.
61///
62/// Implements a secure Debug that doesn't leak the token value.
63#[derive(Clone, Serialize, Deserialize)]
64#[serde(transparent)]
65pub struct AuthToken(String);
66
67impl AuthToken {
68    /// Create a new AuthToken from any string-like type.
69    pub fn new(s: impl Into<String>) -> Self {
70        Self(s.into())
71    }
72
73    /// Get the underlying token string.
74    pub fn as_str(&self) -> &str {
75        &self.0
76    }
77
78    /// Consume self and return the inner String.
79    pub fn into_inner(self) -> String {
80        self.0
81    }
82
83    /// Check if the token is empty.
84    pub fn is_empty(&self) -> bool {
85        self.0.is_empty()
86    }
87}
88
89impl fmt::Debug for AuthToken {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        if self.0.is_empty() {
92            write!(f, "AuthToken(empty)")
93        } else {
94            write!(f, "AuthToken(***)")
95        }
96    }
97}
98
99impl From<&str> for AuthToken {
100    fn from(s: &str) -> Self {
101        Self(s.to_string())
102    }
103}
104
105impl From<String> for AuthToken {
106    fn from(s: String) -> Self {
107        Self(s)
108    }
109}
110
111/// Rule identifier (e.g., "PE-001", "EX-002").
112///
113/// Provides type-safe rule references throughout the codebase.
114#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
115#[serde(transparent)]
116pub struct RuleId(String);
117
118impl RuleId {
119    /// Create a new RuleId from any string-like type.
120    pub fn new(s: impl Into<String>) -> Self {
121        Self(s.into())
122    }
123
124    /// Get the underlying string reference.
125    pub fn as_str(&self) -> &str {
126        &self.0
127    }
128
129    /// Consume self and return the inner String.
130    pub fn into_inner(self) -> String {
131        self.0
132    }
133}
134
135impl From<&str> for RuleId {
136    fn from(s: &str) -> Self {
137        Self(s.to_string())
138    }
139}
140
141impl From<String> for RuleId {
142    fn from(s: String) -> Self {
143        Self(s)
144    }
145}
146
147impl AsRef<str> for RuleId {
148    fn as_ref(&self) -> &str {
149        &self.0
150    }
151}
152
153impl fmt::Display for RuleId {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        write!(f, "{}", self.0)
156    }
157}
158
159/// SHA256 file hash for baseline comparison.
160#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
161#[serde(transparent)]
162pub struct FileHash(String);
163
164impl FileHash {
165    /// Create a new FileHash from any string-like type.
166    pub fn new(s: impl Into<String>) -> Self {
167        Self(s.into())
168    }
169
170    /// Get the underlying hash string.
171    pub fn as_str(&self) -> &str {
172        &self.0
173    }
174
175    /// Consume self and return the inner String.
176    pub fn into_inner(self) -> String {
177        self.0
178    }
179}
180
181impl From<&str> for FileHash {
182    fn from(s: &str) -> Self {
183        Self(s.to_string())
184    }
185}
186
187impl From<String> for FileHash {
188    fn from(s: String) -> Self {
189        Self(s)
190    }
191}
192
193impl AsRef<str> for FileHash {
194    fn as_ref(&self) -> &str {
195        &self.0
196    }
197}
198
199impl fmt::Display for FileHash {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        write!(f, "{}", self.0)
202    }
203}
204
205/// MCP server name.
206#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
207#[serde(transparent)]
208pub struct ServerName(String);
209
210impl ServerName {
211    /// Create a new ServerName.
212    pub fn new(s: impl Into<String>) -> Self {
213        Self(s.into())
214    }
215
216    /// Get the underlying string reference.
217    pub fn as_str(&self) -> &str {
218        &self.0
219    }
220}
221
222impl From<&str> for ServerName {
223    fn from(s: &str) -> Self {
224        Self(s.to_string())
225    }
226}
227
228impl From<String> for ServerName {
229    fn from(s: String) -> Self {
230        Self(s)
231    }
232}
233
234impl fmt::Display for ServerName {
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236        write!(f, "{}", self.0)
237    }
238}
239
240/// Command line arguments wrapper.
241#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
242#[serde(transparent)]
243pub struct CommandArgs(Vec<String>);
244
245impl CommandArgs {
246    /// Create new CommandArgs from an iterator.
247    pub fn new(args: impl IntoIterator<Item = impl Into<String>>) -> Self {
248        Self(args.into_iter().map(Into::into).collect())
249    }
250
251    /// Get the arguments as a slice.
252    pub fn as_slice(&self) -> &[String] {
253        &self.0
254    }
255
256    /// Join arguments with a separator.
257    pub fn join(&self, sep: &str) -> String {
258        self.0.join(sep)
259    }
260
261    /// Check if empty.
262    pub fn is_empty(&self) -> bool {
263        self.0.is_empty()
264    }
265
266    /// Get the number of arguments.
267    pub fn len(&self) -> usize {
268        self.0.len()
269    }
270}
271
272impl From<Vec<String>> for CommandArgs {
273    fn from(args: Vec<String>) -> Self {
274        Self(args)
275    }
276}
277
278impl<'a> From<&'a [&'a str]> for CommandArgs {
279    fn from(args: &'a [&'a str]) -> Self {
280        Self(args.iter().map(|s| s.to_string()).collect())
281    }
282}
283
284/// Compiled regex pattern for efficient reuse.
285#[derive(Debug, Clone)]
286pub struct CompiledPattern {
287    pattern: regex::Regex,
288    source: String,
289}
290
291impl CompiledPattern {
292    /// Create a new CompiledPattern from a regex string.
293    pub fn new(pattern: &str) -> Result<Self, regex::Error> {
294        let regex = regex::Regex::new(pattern)?;
295        Ok(Self {
296            pattern: regex,
297            source: pattern.to_string(),
298        })
299    }
300
301    /// Check if the pattern matches the text.
302    pub fn is_match(&self, text: &str) -> bool {
303        self.pattern.is_match(text)
304    }
305
306    /// Get the source pattern string.
307    pub fn as_str(&self) -> &str {
308        &self.source
309    }
310
311    /// Get the underlying regex.
312    pub fn regex(&self) -> &regex::Regex {
313        &self.pattern
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn test_git_ref_default() {
323        assert_eq!(GitRef::default().as_str(), "HEAD");
324    }
325
326    #[test]
327    fn test_git_ref_from_str() {
328        let ref1: GitRef = "main".into();
329        assert_eq!(ref1.as_str(), "main");
330
331        let ref2 = GitRef::from("develop");
332        assert_eq!(ref2.as_str(), "develop");
333    }
334
335    #[test]
336    fn test_git_ref_display() {
337        let git_ref = GitRef::new("v1.0.0");
338        assert_eq!(format!("{}", git_ref), "v1.0.0");
339    }
340
341    #[test]
342    fn test_auth_token_debug_hides_value() {
343        let token = AuthToken::new("secret123");
344        let debug = format!("{:?}", token);
345        assert!(!debug.contains("secret123"));
346        assert!(debug.contains("***"));
347    }
348
349    #[test]
350    fn test_auth_token_empty_debug() {
351        let token = AuthToken::new("");
352        let debug = format!("{:?}", token);
353        assert!(debug.contains("empty"));
354    }
355
356    #[test]
357    fn test_rule_id_display() {
358        let id = RuleId::new("PE-001");
359        assert_eq!(format!("{}", id), "PE-001");
360    }
361
362    #[test]
363    fn test_rule_id_equality() {
364        let id1 = RuleId::new("EX-001");
365        let id2 = RuleId::new("EX-001");
366        let id3 = RuleId::new("EX-002");
367        assert_eq!(id1, id2);
368        assert_ne!(id1, id3);
369    }
370
371    #[test]
372    fn test_file_hash_from_string() {
373        let hash = FileHash::new("abc123def456");
374        assert_eq!(hash.as_str(), "abc123def456");
375    }
376
377    #[test]
378    fn test_into_inner() {
379        let git_ref = GitRef::new("main");
380        assert_eq!(git_ref.into_inner(), "main".to_string());
381
382        let rule_id = RuleId::new("PE-001");
383        assert_eq!(rule_id.into_inner(), "PE-001".to_string());
384    }
385
386    #[test]
387    fn test_server_name() {
388        let name = ServerName::new("my-server");
389        assert_eq!(name.as_str(), "my-server");
390        assert_eq!(format!("{}", name), "my-server");
391    }
392
393    #[test]
394    fn test_command_args() {
395        let args = CommandArgs::new(["arg1", "arg2", "arg3"]);
396        assert_eq!(args.len(), 3);
397        assert_eq!(args.join(" "), "arg1 arg2 arg3");
398        assert!(!args.is_empty());
399    }
400
401    #[test]
402    fn test_command_args_empty() {
403        let args = CommandArgs::default();
404        assert!(args.is_empty());
405        assert_eq!(args.len(), 0);
406    }
407
408    #[test]
409    fn test_compiled_pattern() {
410        let pattern = CompiledPattern::new(r"hello\s+world").unwrap();
411        assert!(pattern.is_match("hello   world"));
412        assert!(!pattern.is_match("helloworld"));
413        assert_eq!(pattern.as_str(), r"hello\s+world");
414    }
415
416    #[test]
417    fn test_compiled_pattern_invalid() {
418        let result = CompiledPattern::new(r"[invalid");
419        assert!(result.is_err());
420    }
421
422    #[test]
423    fn test_auth_token_is_empty() {
424        let empty = AuthToken::new("");
425        assert!(empty.is_empty());
426
427        let non_empty = AuthToken::new("token");
428        assert!(!non_empty.is_empty());
429    }
430
431    #[test]
432    fn test_auth_token_into_inner() {
433        let token = AuthToken::new("secret");
434        assert_eq!(token.into_inner(), "secret".to_string());
435    }
436
437    #[test]
438    fn test_auth_token_from_string() {
439        let token: AuthToken = String::from("token123").into();
440        assert_eq!(token.as_str(), "token123");
441
442        let token2: AuthToken = "token456".into();
443        assert_eq!(token2.as_str(), "token456");
444    }
445
446    #[test]
447    fn test_file_hash_into_inner() {
448        let hash = FileHash::new("abc123");
449        assert_eq!(hash.into_inner(), "abc123".to_string());
450    }
451
452    #[test]
453    fn test_file_hash_display() {
454        let hash = FileHash::new("sha256:abc123");
455        assert_eq!(format!("{}", hash), "sha256:abc123");
456    }
457
458    #[test]
459    fn test_file_hash_as_ref() {
460        let hash = FileHash::new("abc123");
461        let s: &str = hash.as_ref();
462        assert_eq!(s, "abc123");
463    }
464
465    #[test]
466    fn test_server_name_from_implementations() {
467        let name1: ServerName = "server1".into();
468        assert_eq!(name1.as_str(), "server1");
469
470        let name2: ServerName = String::from("server2").into();
471        assert_eq!(name2.as_str(), "server2");
472    }
473
474    #[test]
475    fn test_command_args_as_slice() {
476        let args = CommandArgs::new(["a", "b", "c"]);
477        assert_eq!(
478            args.as_slice(),
479            &["a".to_string(), "b".to_string(), "c".to_string()]
480        );
481    }
482
483    #[test]
484    fn test_command_args_from_vec() {
485        let vec = vec!["x".to_string(), "y".to_string()];
486        let args: CommandArgs = vec.into();
487        assert_eq!(args.len(), 2);
488    }
489
490    #[test]
491    fn test_command_args_from_slice() {
492        let slice: &[&str] = &["p", "q", "r"];
493        let args: CommandArgs = slice.into();
494        assert_eq!(args.len(), 3);
495    }
496
497    #[test]
498    fn test_compiled_pattern_regex() {
499        let pattern = CompiledPattern::new(r"\d+").unwrap();
500        let regex = pattern.regex();
501        assert!(regex.is_match("123"));
502    }
503
504    #[test]
505    fn test_git_ref_as_ref() {
506        let git_ref = GitRef::new("main");
507        let s: &str = git_ref.as_ref();
508        assert_eq!(s, "main");
509    }
510
511    #[test]
512    fn test_git_ref_from_string() {
513        let git_ref: GitRef = String::from("develop").into();
514        assert_eq!(git_ref.as_str(), "develop");
515    }
516
517    #[test]
518    fn test_rule_id_as_ref() {
519        let rule_id = RuleId::new("PE-001");
520        let s: &str = rule_id.as_ref();
521        assert_eq!(s, "PE-001");
522    }
523
524    #[test]
525    fn test_rule_id_from_string() {
526        let rule_id: RuleId = String::from("EX-001").into();
527        assert_eq!(rule_id.as_str(), "EX-001");
528    }
529
530    #[test]
531    fn test_file_hash_from_owned_string() {
532        let hash: FileHash = String::from("hash123").into();
533        assert_eq!(hash.as_str(), "hash123");
534    }
535}