terraphim_hooks/
replacement.rs1use serde::{Deserialize, Serialize};
4use terraphim_automata::LinkType as AutomataLinkType;
5use terraphim_types::Thesaurus;
6use thiserror::Error;
7
8pub use terraphim_automata::LinkType;
10
11#[derive(Error, Debug)]
13pub enum ReplacementError {
14 #[error("Automata error: {0}")]
15 Automata(#[from] terraphim_automata::TerraphimAutomataError),
16 #[error("UTF-8 conversion error: {0}")]
17 Utf8(#[from] std::string::FromUtf8Error),
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct HookResult {
23 pub result: String,
25 pub original: String,
27 pub replacements: usize,
29 pub changed: bool,
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub error: Option<String>,
34}
35
36impl HookResult {
37 pub fn success(original: String, result: String) -> Self {
39 let changed = original != result;
40 let replacements = if changed { 1 } else { 0 };
41 Self {
42 result,
43 original,
44 replacements,
45 changed,
46 error: None,
47 }
48 }
49
50 pub fn pass_through(original: String) -> Self {
52 Self {
53 result: original.clone(),
54 original,
55 replacements: 0,
56 changed: false,
57 error: None,
58 }
59 }
60
61 pub fn fail_open(original: String, error: String) -> Self {
63 Self {
64 result: original.clone(),
65 original,
66 replacements: 0,
67 changed: false,
68 error: Some(error),
69 }
70 }
71}
72
73pub struct ReplacementService {
75 thesaurus: Thesaurus,
76 link_type: AutomataLinkType,
77}
78
79impl ReplacementService {
80 pub fn new(thesaurus: Thesaurus) -> Self {
82 Self {
83 thesaurus,
84 link_type: AutomataLinkType::PlainText,
85 }
86 }
87
88 pub fn with_link_type(mut self, link_type: AutomataLinkType) -> Self {
90 self.link_type = link_type;
91 self
92 }
93
94 pub fn replace(&self, text: &str) -> Result<HookResult, ReplacementError> {
96 let result_bytes =
97 terraphim_automata::replace_matches(text, self.thesaurus.clone(), self.link_type)?;
98 let result = String::from_utf8(result_bytes)?;
99 Ok(HookResult::success(text.to_string(), result))
100 }
101
102 pub fn replace_fail_open(&self, text: &str) -> HookResult {
106 match self.replace(text) {
107 Ok(result) => result,
108 Err(e) => HookResult::fail_open(text.to_string(), e.to_string()),
109 }
110 }
111
112 pub fn find_matches(
114 &self,
115 text: &str,
116 ) -> Result<Vec<terraphim_automata::Matched>, ReplacementError> {
117 Ok(terraphim_automata::find_matches(
118 text,
119 self.thesaurus.clone(),
120 true,
121 )?)
122 }
123
124 pub fn contains_matches(&self, text: &str) -> bool {
126 self.find_matches(text)
127 .map(|matches| !matches.is_empty())
128 .unwrap_or(false)
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use terraphim_types::{NormalizedTerm, NormalizedTermValue};
136
137 fn create_test_thesaurus() -> Thesaurus {
138 let mut thesaurus = Thesaurus::new("test".to_string());
139
140 let bun_term = NormalizedTerm::new(1, NormalizedTermValue::from("bun"));
142 thesaurus.insert(NormalizedTermValue::from("npm"), bun_term.clone());
143 thesaurus.insert(NormalizedTermValue::from("yarn"), bun_term.clone());
144 thesaurus.insert(NormalizedTermValue::from("pnpm"), bun_term);
145
146 thesaurus
147 }
148
149 #[test]
150 fn test_replacement_service_basic() {
151 let thesaurus = create_test_thesaurus();
152 let service = ReplacementService::new(thesaurus);
153
154 let result = service.replace("npm install").unwrap();
155 assert!(result.changed);
156 assert_eq!(result.result, "bun install");
157 }
158
159 #[test]
160 fn test_replacement_service_no_match() {
161 let thesaurus = create_test_thesaurus();
162 let service = ReplacementService::new(thesaurus);
163
164 let result = service.replace("cargo build").unwrap();
165 assert!(!result.changed);
166 assert_eq!(result.result, "cargo build");
167 }
168
169 #[test]
170 fn test_hook_result_success() {
171 let result = HookResult::success("npm".to_string(), "bun".to_string());
172 assert!(result.changed);
173 assert_eq!(result.replacements, 1);
174 assert!(result.error.is_none());
175 }
176
177 #[test]
178 fn test_hook_result_pass_through() {
179 let result = HookResult::pass_through("unchanged".to_string());
180 assert!(!result.changed);
181 assert_eq!(result.replacements, 0);
182 assert_eq!(result.result, result.original);
183 }
184
185 #[test]
186 fn test_hook_result_fail_open() {
187 let result = HookResult::fail_open("original".to_string(), "error msg".to_string());
188 assert!(!result.changed);
189 assert_eq!(result.result, "original");
190 assert_eq!(result.error, Some("error msg".to_string()));
191 }
192}