agent_chain_core/utils/
strings.rs

1//! String utilities.
2//!
3//! Adapted from langchain_core/utils/strings.py
4
5use serde_json::Value;
6
7/// Stringify a value.
8///
9/// # Arguments
10///
11/// * `val` - The value to stringify.
12///
13/// # Returns
14///
15/// The stringified value.
16///
17/// # Example
18///
19/// ```
20/// use agent_chain_core::utils::strings::stringify_value;
21///
22/// assert_eq!(stringify_value(&serde_json::json!("hello")), "hello");
23/// assert_eq!(stringify_value(&serde_json::json!(42)), "42");
24/// ```
25pub fn stringify_value(val: &Value) -> String {
26    match val {
27        Value::String(s) => s.clone(),
28        Value::Object(map) => {
29            let inner = map
30                .iter()
31                .map(|(k, v)| format!("{}: {}", k, stringify_value(v)))
32                .collect::<Vec<_>>()
33                .join("\n");
34            format!("\n{}", inner)
35        }
36        Value::Array(arr) => arr
37            .iter()
38            .map(stringify_value)
39            .collect::<Vec<_>>()
40            .join("\n"),
41        Value::Null => "null".to_string(),
42        Value::Bool(b) => b.to_string(),
43        Value::Number(n) => n.to_string(),
44    }
45}
46
47/// Stringify a dictionary.
48///
49/// # Arguments
50///
51/// * `data` - The dictionary to stringify.
52///
53/// # Returns
54///
55/// The stringified dictionary.
56///
57/// # Example
58///
59/// ```
60/// use std::collections::HashMap;
61/// use agent_chain_core::utils::strings::stringify_dict;
62///
63/// let mut data = HashMap::new();
64/// data.insert("key".to_string(), "value".to_string());
65///
66/// let result = stringify_dict(&data);
67/// assert!(result.contains("key: value"));
68/// ```
69pub fn stringify_dict(data: &std::collections::HashMap<String, String>) -> String {
70    data.iter()
71        .map(|(k, v)| format!("{}: {}\n", k, v))
72        .collect()
73}
74
75/// Stringify a JSON object.
76///
77/// # Arguments
78///
79/// * `data` - The JSON object to stringify.
80///
81/// # Returns
82///
83/// The stringified dictionary.
84pub fn stringify_json_dict(data: &serde_json::Map<String, Value>) -> String {
85    data.iter()
86        .map(|(k, v)| format!("{}: {}\n", k, stringify_value(v)))
87        .collect()
88}
89
90/// Convert a list to a comma-separated string.
91///
92/// # Arguments
93///
94/// * `items` - The list to convert.
95///
96/// # Returns
97///
98/// The comma-separated string.
99///
100/// # Example
101///
102/// ```
103/// use agent_chain_core::utils::strings::comma_list;
104///
105/// let items = vec!["a".to_string(), "b".to_string(), "c".to_string()];
106/// assert_eq!(comma_list(&items), "a, b, c");
107/// ```
108pub fn comma_list(items: &[String]) -> String {
109    items.join(", ")
110}
111
112/// Convert items that implement Display to a comma-separated string.
113///
114/// # Arguments
115///
116/// * `items` - The items to convert.
117///
118/// # Returns
119///
120/// The comma-separated string.
121pub fn comma_list_display<T: std::fmt::Display>(items: &[T]) -> String {
122    items
123        .iter()
124        .map(|item| item.to_string())
125        .collect::<Vec<_>>()
126        .join(", ")
127}
128
129/// Sanitize text by removing NUL bytes that are incompatible with PostgreSQL.
130///
131/// PostgreSQL text fields cannot contain NUL (0x00) bytes, which can cause
132/// errors when inserting documents. This function removes or replaces
133/// such characters to ensure compatibility.
134///
135/// # Arguments
136///
137/// * `text` - The text to sanitize.
138/// * `replacement` - String to replace NUL bytes with.
139///
140/// # Returns
141///
142/// The sanitized text with NUL bytes removed or replaced.
143///
144/// # Example
145///
146/// ```
147/// use agent_chain_core::utils::strings::sanitize_for_postgres;
148///
149/// assert_eq!(sanitize_for_postgres("Hello\x00world", ""), "Helloworld");
150/// assert_eq!(sanitize_for_postgres("Hello\x00world", " "), "Hello world");
151/// ```
152pub fn sanitize_for_postgres(text: &str, replacement: &str) -> String {
153    text.replace('\x00', replacement)
154}
155
156/// Truncate a string to a maximum length.
157///
158/// # Arguments
159///
160/// * `text` - The text to truncate.
161/// * `max_length` - The maximum length.
162/// * `suffix` - The suffix to append if truncated (default: "...").
163///
164/// # Returns
165///
166/// The truncated string.
167///
168/// # Example
169///
170/// ```
171/// use agent_chain_core::utils::strings::truncate;
172///
173/// assert_eq!(truncate("Hello, World!", 10, None), "Hello, ...");
174/// assert_eq!(truncate("Hello", 10, None), "Hello");
175/// ```
176pub fn truncate(text: &str, max_length: usize, suffix: Option<&str>) -> String {
177    let suffix = suffix.unwrap_or("...");
178    let text_char_count = text.chars().count();
179    let suffix_char_count = suffix.chars().count();
180
181    if text_char_count <= max_length {
182        text.to_string()
183    } else if max_length <= suffix_char_count {
184        suffix.chars().take(max_length).collect()
185    } else {
186        let truncate_at = max_length - suffix_char_count;
187        let truncated: String = text.chars().take(truncate_at).collect();
188        format!("{}{}", truncated, suffix)
189    }
190}
191
192/// Remove leading and trailing whitespace from each line.
193///
194/// # Arguments
195///
196/// * `text` - The text to process.
197///
198/// # Returns
199///
200/// The text with each line trimmed.
201pub fn strip_lines(text: &str) -> String {
202    text.lines()
203        .map(|line| line.trim())
204        .collect::<Vec<_>>()
205        .join("\n")
206}
207
208/// Indent each line of text.
209///
210/// # Arguments
211///
212/// * `text` - The text to indent.
213/// * `indent` - The indentation string.
214///
215/// # Returns
216///
217/// The indented text.
218pub fn indent(text: &str, indent_str: &str) -> String {
219    text.lines()
220        .map(|line| format!("{}{}", indent_str, line))
221        .collect::<Vec<_>>()
222        .join("\n")
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use serde_json::json;
229
230    #[test]
231    fn test_stringify_value_string() {
232        assert_eq!(stringify_value(&json!("hello")), "hello");
233    }
234
235    #[test]
236    fn test_stringify_value_number() {
237        assert_eq!(stringify_value(&json!(42)), "42");
238        assert_eq!(stringify_value(&json!(1.23)), "1.23");
239    }
240
241    #[test]
242    fn test_stringify_value_bool() {
243        assert_eq!(stringify_value(&json!(true)), "true");
244        assert_eq!(stringify_value(&json!(false)), "false");
245    }
246
247    #[test]
248    fn test_stringify_value_null() {
249        assert_eq!(stringify_value(&json!(null)), "null");
250    }
251
252    #[test]
253    fn test_stringify_value_array() {
254        assert_eq!(stringify_value(&json!(["a", "b", "c"])), "a\nb\nc");
255    }
256
257    #[test]
258    fn test_stringify_value_object() {
259        let result = stringify_value(&json!({"key": "value"}));
260        assert!(result.contains("key: value"));
261    }
262
263    #[test]
264    fn test_comma_list() {
265        let items = vec!["a".to_string(), "b".to_string(), "c".to_string()];
266        assert_eq!(comma_list(&items), "a, b, c");
267    }
268
269    #[test]
270    fn test_comma_list_empty() {
271        let items: Vec<String> = vec![];
272        assert_eq!(comma_list(&items), "");
273    }
274
275    #[test]
276    fn test_sanitize_for_postgres() {
277        assert_eq!(sanitize_for_postgres("Hello\x00world", ""), "Helloworld");
278        assert_eq!(sanitize_for_postgres("Hello\x00world", " "), "Hello world");
279        assert_eq!(sanitize_for_postgres("No nulls here", ""), "No nulls here");
280    }
281
282    #[test]
283    fn test_truncate() {
284        assert_eq!(truncate("Hello, World!", 10, None), "Hello, ...");
285        assert_eq!(truncate("Hello", 10, None), "Hello");
286        assert_eq!(truncate("Hello, World!", 8, Some("…")), "Hello, …");
287    }
288
289    #[test]
290    fn test_strip_lines() {
291        assert_eq!(strip_lines("  a  \n  b  \n  c  "), "a\nb\nc");
292    }
293
294    #[test]
295    fn test_indent() {
296        assert_eq!(indent("a\nb\nc", "  "), "  a\n  b\n  c");
297    }
298}