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}