1use crate::error::Result;
2use serde::{Deserialize, Serialize};
3use sha2::{Digest, Sha256};
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct FunctionContext {
9 pub file_path: PathBuf,
11 pub function_name: Option<String>,
13 pub line_number: usize,
15 pub context: Option<String>,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
21pub struct OverrideKey {
22 pub primary: String,
24 pub fallbacks: Vec<String>,
26 pub display: String,
28}
29
30impl OverrideKey {
31 pub fn new(context: &FunctionContext) -> Result<Self> {
33 let mut fallbacks = Vec::new();
34
35 let primary = if let Some(ref func_name) = context.function_name {
37 let key = format!("{}:{}", Self::normalize_path(&context.file_path), func_name);
39
40 fallbacks.push(format!(
42 "{}:{}:L{}",
43 Self::normalize_path(&context.file_path),
44 func_name,
45 context.line_number
46 ));
47
48 key
49 } else {
50 format!(
52 "{}:L{}",
53 Self::normalize_path(&context.file_path),
54 context.line_number
55 )
56 };
57
58 let hash_key = Self::generate_hash_key(context);
60 fallbacks.push(hash_key);
61
62 let display = if let Some(ref func_name) = context.function_name {
64 format!("{} in {}", func_name, context.file_path.display())
65 } else {
66 format!("{}:{}", context.file_path.display(), context.line_number)
67 };
68
69 Ok(Self {
70 primary,
71 fallbacks,
72 display,
73 })
74 }
75
76 fn normalize_path(path: &Path) -> String {
78 path.to_string_lossy()
79 .replace('\\', "/")
80 .trim_start_matches("./")
81 .to_string()
82 }
83
84 fn generate_hash_key(context: &FunctionContext) -> String {
86 let mut hasher = Sha256::new();
87 hasher.update(Self::normalize_path(&context.file_path).as_bytes());
88
89 if let Some(ref func_name) = context.function_name {
90 hasher.update(b":");
91 hasher.update(func_name.as_bytes());
92 }
93
94 if let Some(ref ctx) = context.context {
95 hasher.update(b":");
96 hasher.update(ctx.as_bytes());
97 }
98
99 let hash = hasher.finalize();
100 format!("hash:{}", hex::encode(&hash[..8]))
101 }
102
103 pub fn matches(&self, pattern: &str) -> bool {
105 self.primary == pattern
106 || self.fallbacks.iter().any(|k| k == pattern)
107 || self.display.contains(pattern)
108 }
109
110 pub fn all_keys(&self) -> Vec<&str> {
112 let mut keys = vec![self.primary.as_str()];
113 keys.extend(self.fallbacks.iter().map(|s| s.as_str()));
114 keys
115 }
116}
117
118impl std::fmt::Display for OverrideKey {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 write!(f, "{}", self.display)
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn test_override_key_with_function() {
130 let context = FunctionContext {
131 file_path: PathBuf::from("src/main.rs"),
132 function_name: Some("handle_request".to_string()),
133 line_number: 42,
134 context: None,
135 };
136
137 let key = OverrideKey::new(&context).unwrap();
138 assert_eq!(key.primary, "src/main.rs:handle_request");
139 assert!(
140 key.fallbacks
141 .contains(&"src/main.rs:handle_request:L42".to_string())
142 );
143 assert_eq!(key.display, "handle_request in src/main.rs");
144 }
145
146 #[test]
147 fn test_override_key_without_function() {
148 let context = FunctionContext {
149 file_path: PathBuf::from("src/lib.rs"),
150 function_name: None,
151 line_number: 100,
152 context: None,
153 };
154
155 let key = OverrideKey::new(&context).unwrap();
156 assert_eq!(key.primary, "src/lib.rs:L100");
157 assert_eq!(key.display, "src/lib.rs:100");
158 }
159
160 #[test]
161 fn test_path_normalization() {
162 let context = FunctionContext {
163 file_path: PathBuf::from("./src\\module\\file.rs"),
164 function_name: Some("test".to_string()),
165 line_number: 1,
166 context: None,
167 };
168
169 let key = OverrideKey::new(&context).unwrap();
170 assert!(key.primary.starts_with("src/module/file.rs"));
171 }
172}