raz_override/
key.rs

1use crate::error::Result;
2use serde::{Deserialize, Serialize};
3use sha2::{Digest, Sha256};
4use std::path::{Path, PathBuf};
5
6/// Context for generating an override key
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct FunctionContext {
9    /// The file path relative to workspace root
10    pub file_path: PathBuf,
11    /// The function name if known
12    pub function_name: Option<String>,
13    /// The line number in the file
14    pub line_number: usize,
15    /// Additional context for disambiguation
16    pub context: Option<String>,
17}
18
19/// A stable override key that can survive code refactoring
20#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
21pub struct OverrideKey {
22    /// Primary key based on function signature
23    pub primary: String,
24    /// Fallback keys for resolution
25    pub fallbacks: Vec<String>,
26    /// Human-readable representation
27    pub display: String,
28}
29
30impl OverrideKey {
31    /// Create a new override key from function context
32    pub fn new(context: &FunctionContext) -> Result<Self> {
33        let mut fallbacks = Vec::new();
34
35        // Generate primary key based on function name if available
36        let primary = if let Some(ref func_name) = context.function_name {
37            // Primary: file_path:function_name
38            let key = format!("{}:{}", Self::normalize_path(&context.file_path), func_name);
39
40            // Add fallback with line context
41            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            // No function name, use file + line as primary
51            format!(
52                "{}:L{}",
53                Self::normalize_path(&context.file_path),
54                context.line_number
55            )
56        };
57
58        // Add hash-based fallback for extreme cases
59        let hash_key = Self::generate_hash_key(context);
60        fallbacks.push(hash_key);
61
62        // Generate display string
63        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    /// Normalize a path for consistent key generation
77    fn normalize_path(path: &Path) -> String {
78        path.to_string_lossy()
79            .replace('\\', "/")
80            .trim_start_matches("./")
81            .to_string()
82    }
83
84    /// Generate a hash-based key as ultimate fallback
85    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    /// Check if this key matches a given pattern
104    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    /// Get all possible keys for resolution
111    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}