agent_chain_core/utils/
formatting.rs1use std::collections::{HashMap, HashSet};
6
7#[derive(Debug, Clone, Default)]
13pub struct StrictFormatter;
14
15impl StrictFormatter {
16 pub fn new() -> Self {
18 Self
19 }
20
21 pub fn format(
46 &self,
47 format_string: &str,
48 kwargs: &HashMap<String, String>,
49 ) -> Result<String, FormattingError> {
50 let placeholders = self.extract_placeholders(format_string);
51 let mut result = format_string.to_string();
52
53 for placeholder in &placeholders {
54 if let Some(value) = kwargs.get(placeholder) {
55 result = result.replace(&format!("{{{}}}", placeholder), value);
56 } else {
57 return Err(FormattingError::MissingKey(placeholder.clone()));
58 }
59 }
60
61 Ok(result)
62 }
63
64 pub fn validate_input_variables(
85 &self,
86 format_string: &str,
87 input_variables: &[String],
88 ) -> Result<(), FormattingError> {
89 let mut dummy_inputs = HashMap::new();
90 for var in input_variables {
91 dummy_inputs.insert(var.clone(), "foo".to_string());
92 }
93
94 self.format(format_string, &dummy_inputs).map(|_| ())
95 }
96
97 pub fn extract_placeholders(&self, format_string: &str) -> HashSet<String> {
107 let mut placeholders = HashSet::new();
108 let mut chars = format_string.chars().peekable();
109 let mut in_placeholder = false;
110 let mut current_placeholder = String::new();
111
112 while let Some(c) = chars.next() {
113 match c {
114 '{' => {
115 if chars.peek() == Some(&'{') {
116 chars.next();
117 } else {
118 in_placeholder = true;
119 current_placeholder.clear();
120 }
121 }
122 '}' => {
123 if in_placeholder {
124 if !current_placeholder.is_empty() {
125 let name = current_placeholder.split(':').next().unwrap_or("");
126 let name = name.split('!').next().unwrap_or("");
127 if !name.is_empty() {
128 placeholders.insert(name.to_string());
129 }
130 }
131 in_placeholder = false;
132 current_placeholder.clear();
133 } else if chars.peek() == Some(&'}') {
134 chars.next();
135 }
136 }
137 _ => {
138 if in_placeholder {
139 current_placeholder.push(c);
140 }
141 }
142 }
143 }
144
145 placeholders
146 }
147}
148
149pub static FORMATTER: std::sync::LazyLock<StrictFormatter> =
151 std::sync::LazyLock::new(StrictFormatter::new);
152
153pub fn format_string(
166 format_string: &str,
167 kwargs: &HashMap<String, String>,
168) -> Result<String, FormattingError> {
169 FORMATTER.format(format_string, kwargs)
170}
171
172#[derive(Debug, Clone, PartialEq)]
174pub enum FormattingError {
175 MissingKey(String),
177 InvalidFormat(String),
179}
180
181impl std::fmt::Display for FormattingError {
182 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183 match self {
184 FormattingError::MissingKey(key) => {
185 write!(f, "Missing key in format string: {}", key)
186 }
187 FormattingError::InvalidFormat(msg) => {
188 write!(f, "Invalid format string: {}", msg)
189 }
190 }
191 }
192}
193
194impl std::error::Error for FormattingError {}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn test_format_basic() {
202 let formatter = StrictFormatter::new();
203 let mut kwargs = HashMap::new();
204 kwargs.insert("name".to_string(), "World".to_string());
205
206 let result = formatter.format("Hello, {name}!", &kwargs).unwrap();
207 assert_eq!(result, "Hello, World!");
208 }
209
210 #[test]
211 fn test_format_multiple() {
212 let formatter = StrictFormatter::new();
213 let mut kwargs = HashMap::new();
214 kwargs.insert("first".to_string(), "John".to_string());
215 kwargs.insert("last".to_string(), "Doe".to_string());
216
217 let result = formatter.format("{first} {last}", &kwargs).unwrap();
218 assert_eq!(result, "John Doe");
219 }
220
221 #[test]
222 fn test_format_missing_key() {
223 let formatter = StrictFormatter::new();
224 let kwargs = HashMap::new();
225
226 let result = formatter.format("Hello, {name}!", &kwargs);
227 assert!(matches!(result, Err(FormattingError::MissingKey(_))));
228 }
229
230 #[test]
231 fn test_extract_placeholders() {
232 let formatter = StrictFormatter::new();
233
234 let placeholders =
235 formatter.extract_placeholders("Hello, {name}! You are {age} years old.");
236 assert!(placeholders.contains("name"));
237 assert!(placeholders.contains("age"));
238 assert_eq!(placeholders.len(), 2);
239 }
240
241 #[test]
242 fn test_extract_placeholders_escaped() {
243 let formatter = StrictFormatter::new();
244
245 let placeholders = formatter.extract_placeholders("Hello, {{name}}!");
246 assert!(placeholders.is_empty());
247 }
248
249 #[test]
250 fn test_validate_input_variables() {
251 let formatter = StrictFormatter::new();
252
253 let result = formatter.validate_input_variables("Hello, {name}!", &["name".to_string()]);
254 assert!(result.is_ok());
255
256 let result = formatter.validate_input_variables("Hello, {name}!", &[]);
257 assert!(result.is_err());
258 }
259
260 #[test]
261 fn test_format_string_function() {
262 let mut kwargs = HashMap::new();
263 kwargs.insert("greeting".to_string(), "Hi".to_string());
264
265 let result = format_string("{greeting}!", &kwargs).unwrap();
266 assert_eq!(result, "Hi!");
267 }
268}