agent_chain_core/utils/
formatting.rs

1//! Utilities for formatting strings.
2//!
3//! Adapted from langchain_core/utils/formatting.py
4
5use std::collections::{HashMap, HashSet};
6
7/// A strict formatter that checks for extra keys and requires all arguments as keyword arguments.
8///
9/// This formatter is based on Python's string formatting but enforces stricter rules:
10/// - All arguments must be provided as keyword arguments
11/// - All placeholders in the format string must be used
12#[derive(Debug, Clone, Default)]
13pub struct StrictFormatter;
14
15impl StrictFormatter {
16    /// Create a new StrictFormatter.
17    pub fn new() -> Self {
18        Self
19    }
20
21    /// Format a string with keyword arguments.
22    ///
23    /// # Arguments
24    ///
25    /// * `format_string` - The format string with placeholders like `{name}`.
26    /// * `kwargs` - The keyword arguments to substitute.
27    ///
28    /// # Returns
29    ///
30    /// The formatted string, or an error if formatting fails.
31    ///
32    /// # Example
33    ///
34    /// ```
35    /// use std::collections::HashMap;
36    /// use agent_chain_core::utils::formatting::StrictFormatter;
37    ///
38    /// let formatter = StrictFormatter::new();
39    /// let mut kwargs = HashMap::new();
40    /// kwargs.insert("name".to_string(), "World".to_string());
41    ///
42    /// let result = formatter.format("Hello, {name}!", &kwargs).unwrap();
43    /// assert_eq!(result, "Hello, World!");
44    /// ```
45    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    /// Validate that input variables can be used with the format string.
65    ///
66    /// # Arguments
67    ///
68    /// * `format_string` - The format string to validate.
69    /// * `input_variables` - The input variables that will be provided.
70    ///
71    /// # Returns
72    ///
73    /// Ok(()) if validation passes, or an error if any input variables are not used.
74    ///
75    /// # Example
76    ///
77    /// ```
78    /// use agent_chain_core::utils::formatting::StrictFormatter;
79    ///
80    /// let formatter = StrictFormatter::new();
81    /// let result = formatter.validate_input_variables("Hello, {name}!", &["name".to_string()]);
82    /// assert!(result.is_ok());
83    /// ```
84    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    /// Extract placeholders from a format string.
98    ///
99    /// # Arguments
100    ///
101    /// * `format_string` - The format string to extract placeholders from.
102    ///
103    /// # Returns
104    ///
105    /// A set of placeholder names found in the format string.
106    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
149/// A global formatter instance.
150pub static FORMATTER: std::sync::LazyLock<StrictFormatter> =
151    std::sync::LazyLock::new(StrictFormatter::new);
152
153/// Format a string using the global formatter.
154///
155/// This is a convenience function that uses the global [`FORMATTER`] instance.
156///
157/// # Arguments
158///
159/// * `format_string` - The format string with placeholders.
160/// * `kwargs` - The keyword arguments to substitute.
161///
162/// # Returns
163///
164/// The formatted string, or an error if formatting fails.
165pub 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/// Error types for formatting operations.
173#[derive(Debug, Clone, PartialEq)]
174pub enum FormattingError {
175    /// A required key was missing from the kwargs.
176    MissingKey(String),
177    /// An invalid format string was provided.
178    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}