serializer/llm/
human_formatter.rs1use crate::llm::types::{DxDocument, DxLlmValue, DxSection};
33use indexmap::IndexMap;
34
35#[derive(Debug, Clone)]
37pub struct HumanFormatConfig {
38 pub key_padding: usize,
40}
41
42impl Default for HumanFormatConfig {
43 fn default() -> Self {
44 Self { key_padding: 28 }
45 }
46}
47
48impl HumanFormatConfig {
49 pub fn new() -> Self {
51 Self::default()
52 }
53
54 pub fn with_key_padding(mut self, padding: usize) -> Self {
56 self.key_padding = padding;
57 self
58 }
59
60 pub fn for_tables() -> Self {
62 Self { key_padding: 28 }
63 }
64}
65
66pub struct HumanFormatter {
68 config: HumanFormatConfig,
69}
70
71impl HumanFormatter {
72 pub fn new() -> Self {
74 Self {
75 config: HumanFormatConfig::default(),
76 }
77 }
78
79 pub fn with_config(config: HumanFormatConfig) -> Self {
81 Self { config }
82 }
83
84 pub fn format(&self, doc: &DxDocument) -> String {
86 let mut sections: Vec<String> = Vec::new();
87
88 if !doc.entry_order.is_empty() {
90 let mut root_scalar_keys = Vec::new();
92 for entry_ref in &doc.entry_order {
93 if let crate::llm::types::EntryRef::Context(key) = entry_ref {
94 if let Some(value) = doc.context.get(key) {
95 if !matches!(value, DxLlmValue::Obj(_) | DxLlmValue::Arr(_)) {
96 root_scalar_keys.push(key.clone());
97 }
98 }
99 }
100 }
101
102 if !root_scalar_keys.is_empty() {
103 let mut root_scalars_map = IndexMap::new();
104 for key in root_scalar_keys {
105 if let Some(value) = doc.context.get(&key) {
106 root_scalars_map.insert(key, value.clone());
107 }
108 }
109 let root_scalars = self.format_root_scalars(&root_scalars_map);
110 if !root_scalars.is_empty() {
111 sections.push(root_scalars);
112 }
113 }
114
115 for entry_ref in &doc.entry_order {
117 match entry_ref {
118 crate::llm::types::EntryRef::Context(key) => {
119 if let Some(DxLlmValue::Obj(fields)) = doc.context.get(key) {
120 let section_output = self.format_object_as_section(key, fields);
121 if !section_output.is_empty() {
122 sections.push(section_output);
123 }
124 }
125 }
126 crate::llm::types::EntryRef::Section(id) => {
127 if let Some(section) = doc.sections.get(id) {
128 let section_name = doc
129 .section_names
130 .get(id)
131 .cloned()
132 .unwrap_or_else(|| id.to_string());
133
134 let section_output = if self.is_tabular_section(section) {
135 self.format_tabular_section_multi_row(§ion_name, section)
136 } else {
137 self.format_section(§ion_name, section)
138 };
139
140 if !section_output.is_empty() {
141 sections.push(section_output);
142 }
143 }
144 }
145 }
146 }
147 } else {
148 let root_scalars = self.format_root_scalars(&doc.context);
150 if !root_scalars.is_empty() {
151 sections.push(root_scalars);
152 }
153
154 let mut context_keys: Vec<_> = doc.context.keys().collect();
155 context_keys.sort();
156
157 for key in context_keys {
158 if let Some(DxLlmValue::Obj(fields)) = doc.context.get(key) {
159 let section_output = self.format_object_as_section(key, fields);
160 if !section_output.is_empty() {
161 sections.push(section_output);
162 }
163 }
164 }
165
166 let mut section_ids: Vec<_> = doc.sections.keys().collect();
167 section_ids.sort();
168
169 for id in section_ids {
170 if let Some(section) = doc.sections.get(id) {
171 let section_name = doc
172 .section_names
173 .get(id)
174 .cloned()
175 .unwrap_or_else(|| id.to_string());
176
177 let section_output = if self.is_tabular_section(section) {
178 self.format_tabular_section_multi_row(§ion_name, section)
179 } else {
180 self.format_section(§ion_name, section)
181 };
182
183 if !section_output.is_empty() {
184 sections.push(section_output);
185 }
186 }
187 }
188 }
189
190 sections.join("\n\n")
191 }
192
193 fn format_object_as_section(
195 &self,
196 section_id: &str,
197 fields: &IndexMap<String, DxLlmValue>,
198 ) -> String {
199 let mut lines: Vec<String> = Vec::new();
200
201 lines.push(format!("[{}]", section_id));
202
203 let mut scalars: Vec<(String, String)> = Vec::new();
205 let mut arrays: Vec<(String, Vec<String>)> = Vec::new();
206
207 for (key, value) in fields {
208 if let DxLlmValue::Arr(items) = value {
209 let array_items: Vec<String> = items.iter().map(|v| self.format_value(v)).collect();
210 arrays.push((key.clone(), array_items));
211 } else {
212 scalars.push((key.clone(), self.format_value(value)));
213 }
214 }
215
216 for (key, value) in scalars {
218 lines.push(self.format_key_value(&key, &value));
219 }
220
221 for (key, items) in arrays {
223 lines.push(format!("{}:", key));
224 for item in items {
225 lines.push(format!("- {}", item));
226 }
227 }
228
229 lines.join("\n")
230 }
231
232 fn format_root_scalars(&self, context: &IndexMap<String, DxLlmValue>) -> String {
234 let mut lines: Vec<String> = Vec::new();
235
236 for (key, value) in context.iter() {
237 if !matches!(value, DxLlmValue::Arr(_) | DxLlmValue::Obj(_)) {
239 lines.push(self.format_key_value(key, &self.format_value(value)));
240 }
241 }
242
243 lines.join("\n")
244 }
245
246 fn format_section(&self, section_id: &str, section: &DxSection) -> String {
248 let mut lines: Vec<String> = Vec::new();
249
250 lines.push(format!("[{}]", section_id));
251
252 if section.rows.is_empty() {
253 return lines.join("\n");
254 }
255
256 let row = §ion.rows[0];
257
258 let mut scalars: Vec<(String, String)> = Vec::new();
260 let mut arrays: Vec<(String, Vec<String>)> = Vec::new();
261
262 for (i, field) in section.schema.iter().enumerate() {
263 if i >= row.len() {
264 break;
265 }
266
267 let key = field.clone();
268 let value = &row[i];
269
270 if let DxLlmValue::Arr(items) = value {
271 let array_items: Vec<String> = items.iter().map(|v| self.format_value(v)).collect();
272 arrays.push((key, array_items));
273 } else {
274 scalars.push((key, self.format_value(value)));
275 }
276 }
277
278 for (key, value) in scalars {
280 lines.push(self.format_key_value(&key, &value));
281 }
282
283 for (key, items) in arrays {
285 lines.push(format!("{}:", key));
286 for item in items {
287 lines.push(format!("- {}", item));
288 }
289 }
290
291 lines.join("\n")
292 }
293
294 #[allow(dead_code)]
296 fn format_tabular_section(&self, section_id: &str, section: &DxSection) -> String {
297 let mut lines: Vec<String> = Vec::new();
298
299 lines.push(format!("[{}]", section_id));
300
301 for row in §ion.rows {
302 if row.len() >= 2 {
303 let key = self.format_value(&row[0]);
304 let value = self.format_value(&row[1]);
305 lines.push(self.format_key_value(&key, &value));
306 }
307 }
308
309 lines.join("\n")
310 }
311
312 fn format_tabular_section_multi_row(&self, section_id: &str, section: &DxSection) -> String {
314 let mut all_sections: Vec<String> = Vec::new();
315
316 for (row_idx, row) in section.rows.iter().enumerate() {
317 let mut lines: Vec<String> = Vec::new();
318
319 lines.push(format!("[{}:{}]", section_id, row_idx + 1));
321
322 for (i, field) in section.schema.iter().enumerate() {
324 if i >= row.len() {
325 break;
326 }
327
328 let key = field.clone();
329 let value = &row[i];
330
331 if let DxLlmValue::Arr(items) = value {
332 lines.push(format!("{}:", key));
334 for item in items {
335 lines.push(format!("- {}", self.format_value(item)));
336 }
337 } else {
338 lines.push(self.format_key_value(&key, &self.format_value(value)));
339 }
340 }
341
342 all_sections.push(lines.join("\n"));
343 }
344
345 all_sections.join("\n\n")
346 }
347
348 fn is_tabular_section(&self, section: &DxSection) -> bool {
350 if section.rows.len() <= 1 {
351 return false;
352 }
353
354 let schema_names: Vec<String> = section.schema.iter().map(|f| f.to_lowercase()).collect();
355
356 schema_names.contains(&"name".to_string()) && schema_names.contains(&"version".to_string())
357 }
358
359 fn format_value(&self, value: &DxLlmValue) -> String {
361 match value {
362 DxLlmValue::Str(s) => {
363 s.replace('_', " ")
365 }
366 DxLlmValue::Num(n) => {
367 if n.fract() == 0.0 {
368 format!("{}", *n as i64)
369 } else {
370 format!("{}", n)
371 }
372 }
373 DxLlmValue::Bool(b) => {
374 if *b {
375 "true".to_string()
376 } else {
377 "false".to_string()
378 }
379 }
380 DxLlmValue::Null => "none".to_string(),
381 DxLlmValue::Arr(_) => String::new(),
382 DxLlmValue::Obj(_) => String::new(),
383 DxLlmValue::Ref(key) => format!("^{}", key),
384 }
385 }
386
387 fn format_key_value(&self, key: &str, value: &str) -> String {
389 let padding = self.config.key_padding.saturating_sub(key.len()).max(1);
390 format!("{}{} = {}", key, " ".repeat(padding), value)
391 }
392}
393
394impl Default for HumanFormatter {
395 fn default() -> Self {
396 Self::new()
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 #[test]
405 fn test_format_empty_document() {
406 let formatter = HumanFormatter::new();
407 let doc = DxDocument::new();
408 let output = formatter.format(&doc);
409 assert!(output.is_empty());
410 }
411
412 #[test]
413 fn test_format_scalar_values() {
414 let formatter = HumanFormatter::new();
415 let mut doc = DxDocument::new();
416 doc.context
417 .insert("name".to_string(), DxLlmValue::Str("dx".to_string()));
418 doc.context
419 .insert("version".to_string(), DxLlmValue::Str("0.0.1".to_string()));
420 doc.context
421 .insert("count".to_string(), DxLlmValue::Num(42.0));
422 doc.context
423 .insert("active".to_string(), DxLlmValue::Bool(true));
424
425 let output = formatter.format(&doc);
426 assert!(output.contains("name"));
427 assert!(output.contains("dx"));
428 assert!(output.contains("version"));
429 assert!(output.contains("0.0.1"));
430 assert!(output.contains("count"));
431 assert!(output.contains("42"));
432 assert!(output.contains("active"));
433 assert!(output.contains("true"));
434 }
435}