Skip to main content

zenbase_llml/
lib.rs

1/*!
2# LLML - Lightweight Markup Language
3
4Converts data structures to XML-like markup with specific formatting rules.
5
6## Features
7
8- Converts key-value pairs to XML tags: `{"key": "value"}` → `<key>value</key>`
9- Formats arrays as numbered lists with wrapper tags
10- Supports nested objects and kebab-case conversion
11- Handles indentation, prefixes, and multiline strings
12
13## Usage
14
15```rust
16use zenbase_llml::{llml, llml_with_options, LLMLOptions};
17use serde_json::json;
18
19let data = json!({"instructions": "Follow these steps"});
20let result = llml(&data);
21// Output: "<instructions>Follow these steps</instructions>"
22
23// For custom formatting:
24let options = LLMLOptions { indent: "  ".to_string(), prefix: String::new(), strict: false };
25let result = llml_with_options(&data, Some(options));
26```
27*/
28
29use serde_json::Value;
30
31mod utils;
32use utils::format_value;
33
34/// Configuration LLMLOptions for LLML formatting
35#[derive(Debug, Clone, Default)]
36pub struct LLMLOptions {
37    /// Indentation string to use for nested elements
38    pub indent: String,
39    /// Prefix to prepend to all tags
40    pub prefix: String,
41    /// Whether to use strict mode (include parent keys as prefixes in nested objects)
42    pub strict: bool,
43}
44
45/// Main LLML function - converts data structures to XML-like markup
46///
47/// Supports various call patterns:
48/// - `llml(&Value::Null)` → `"null"`
49/// - `llml(&json!([]))` → `""`
50/// - `llml(&json!({}))` → `""`
51/// - `llml(&json!({"key": "value"}))` → `"<key>value</key>"`
52/// - `llml_with_options(&data, LLMLOptions)` → formatted with custom LLMLOptions
53pub fn llml(data: &Value) -> String {
54    format_value(data, &LLMLOptions::default())
55}
56
57/// LLML function with explicit LLMLOptions - use when you need custom formatting
58///
59/// Examples:
60/// - `llml_with_options(&data, None)` → same as `llml(&data)`
61/// - `llml_with_options(&data, Some(LLMLOptions))` → formatted with custom LLMLOptions
62pub fn llml_with_options(data: &Value, options: Option<LLMLOptions>) -> String {
63    let opts = options.unwrap_or_default();
64    format_value(data, &opts)
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use serde_json::json;
71
72    #[test]
73    fn test_empty_values() {
74        assert_eq!(llml(&json!({})), "");
75        assert_eq!(llml(&json!([])), "");
76        assert_eq!(llml(&Value::Null), "null");
77    }
78
79    #[test]
80    fn test_simple_values() {
81        let result = llml(&json!({"instructions": "Follow these steps"}));
82        assert_eq!(result, "<instructions>Follow these steps</instructions>");
83
84        let result = llml(&json!({"count": 42}));
85        assert_eq!(result, "<count>42</count>");
86
87        let result = llml(&json!({"enabled": true}));
88        assert_eq!(result, "<enabled>true</enabled>");
89    }
90
91    #[test]
92    fn test_list_formatting() {
93        let result = llml(&json!({"rules": ["first", "second", "third"]}));
94        let expected = "<rules>\n  <rules-1>first</rules-1>\n  <rules-2>second</rules-2>\n  <rules-3>third</rules-3>\n</rules>";
95        assert_eq!(result, expected);
96    }
97
98    #[test]
99    fn test_nested_objects() {
100        let result = llml(&json!({
101            "config": {
102                "debug": true,
103                "timeout": 30
104            }
105        }));
106
107        assert!(result.contains("<config>"));
108        assert!(result.contains("</config>"));
109        assert!(result.contains("<debug>true</debug>"));
110        assert!(result.contains("<timeout>30</timeout>"));
111    }
112
113    #[test]
114    fn test_optional_second_argument() {
115        let data = json!({"instructions": "Follow these steps"});
116
117        // Test new simple syntax (no second argument)
118        let result1 = llml(&data);
119        assert_eq!(result1, "<instructions>Follow these steps</instructions>");
120
121        // Test with LLMLOptions using the new function
122        let options = Some(LLMLOptions {
123            indent: "  ".to_string(),
124            prefix: String::new(),
125            strict: false,
126        });
127        let result2 = llml_with_options(&data, options);
128        assert_eq!(result2, "  <instructions>Follow these steps</instructions>");
129
130        // Test backward compatibility (explicit None with new function)
131        let result3 = llml_with_options(&data, None);
132        assert_eq!(result3, "<instructions>Follow these steps</instructions>");
133
134        // Simple function and explicit None should be identical
135        assert_eq!(result1, result3);
136    }
137
138    #[test]
139    fn test_with_LLMLOptions_function() {
140        let data = json!({"test": "value"});
141
142        // Test with indentation
143        let options = LLMLOptions {
144            indent: "    ".to_string(),
145            prefix: String::new(),
146            strict: false,
147        };
148        let result = llml_with_options(&data, Some(options));
149        assert_eq!(result, "    <test>value</test>");
150
151        // Test with prefix
152        let options = LLMLOptions {
153            indent: String::new(),
154            prefix: "app".to_string(),
155            strict: false,
156        };
157        let result = llml_with_options(&data, Some(options));
158        assert_eq!(result, "<app-test>value</app-test>");
159    }
160
161    #[test]
162    fn test_insertion_order_preservation() {
163        // Test that json! macro and our formatter preserve key insertion order
164        let result = llml(&json!({
165            "first": "1st",
166            "second": "2nd",
167            "third": "3rd"
168        }));
169
170        // Verify exact order matches insertion order
171        let expected = "<first>1st</first>\n<second>2nd</second>\n<third>3rd</third>";
172        assert_eq!(result, expected);
173    }
174
175    #[test]
176    fn test_deterministic_output() {
177        // Test that output is deterministic across multiple runs
178        let data = json!({
179            "alpha": "value1",
180            "beta": "value2",
181            "gamma": "value3",
182            "delta": "value4"
183        });
184
185        let result1 = llml(&data);
186        let result2 = llml(&data);
187        let result3 = llml(&data);
188
189        // All results should be identical
190        assert_eq!(result1, result2);
191        assert_eq!(result2, result3);
192
193        // And should have a predictable order (alphabetical in this case)
194        let expected = "<alpha>value1</alpha>\n<beta>value2</beta>\n<delta>value4</delta>\n<gamma>value3</gamma>";
195        assert_eq!(result1, expected);
196    }
197}