use crate::llm::human_formatter::{HumanFormatConfig, HumanFormatter};
use crate::llm::human_parser::HumanParser;
use crate::llm::types::DxDocument;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PrettyPrintError {
#[error("Output validation failed: {msg}")]
ValidationFailed {
msg: String,
},
#[error("Round-trip consistency failed: {msg}")]
RoundTripFailed {
msg: String,
},
}
#[derive(Debug, Clone)]
pub struct PrettyPrinterConfig {
pub formatter_config: HumanFormatConfig,
pub validate_output: bool,
pub check_round_trip: bool,
}
impl Default for PrettyPrinterConfig {
fn default() -> Self {
Self {
formatter_config: HumanFormatConfig::default(),
validate_output: true,
check_round_trip: true,
}
}
}
impl PrettyPrinterConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_key_padding(mut self, padding: usize) -> Self {
self.formatter_config.key_padding = padding;
self
}
pub fn with_validation(mut self, validate: bool) -> Self {
self.validate_output = validate;
self
}
pub fn with_round_trip_check(mut self, check: bool) -> Self {
self.check_round_trip = check;
self
}
}
pub struct PrettyPrinter {
config: PrettyPrinterConfig,
formatter: HumanFormatter,
parser: HumanParser,
}
impl PrettyPrinter {
pub fn new() -> Self {
let config = PrettyPrinterConfig::default();
Self {
formatter: HumanFormatter::with_config(config.formatter_config.clone()),
parser: HumanParser::new(),
config,
}
}
pub fn with_config(config: PrettyPrinterConfig) -> Self {
Self {
formatter: HumanFormatter::with_config(config.formatter_config.clone()),
parser: HumanParser::new(),
config,
}
}
pub fn format(&self, doc: &DxDocument) -> Result<String, PrettyPrintError> {
let output = self.formatter.format(doc);
if self.config.validate_output {
self.validate_output(&output, doc)?;
}
Ok(output)
}
pub fn format_unchecked(&self, doc: &DxDocument) -> String {
self.formatter.format(doc)
}
fn validate_output(&self, output: &str, original: &DxDocument) -> Result<(), PrettyPrintError> {
let parsed = self
.parser
.parse(output)
.map_err(|e| PrettyPrintError::ValidationFailed {
msg: format!("Failed to parse formatted output: {}", e),
})?;
if self.config.check_round_trip {
self.check_round_trip(original, &parsed)?;
}
Ok(())
}
fn check_round_trip(
&self,
original: &DxDocument,
parsed: &DxDocument,
) -> Result<(), PrettyPrintError> {
if original.context.len() != parsed.context.len() {
return Err(PrettyPrintError::RoundTripFailed {
msg: format!(
"Context size mismatch: original={}, parsed={}",
original.context.len(),
parsed.context.len()
),
});
}
for (key, value) in &original.context {
if let Some(parsed_value) = parsed.context.get(key) {
if !values_equal(value, parsed_value) {
return Err(PrettyPrintError::RoundTripFailed {
msg: format!(
"Context value mismatch for key '{}': original={:?}, parsed={:?}",
key, value, parsed_value
),
});
}
} else {
return Err(PrettyPrintError::RoundTripFailed {
msg: format!("Context key '{}' missing in parsed document", key),
});
}
}
if original.sections.len() != parsed.sections.len() {
return Err(PrettyPrintError::RoundTripFailed {
msg: format!(
"Section count mismatch: original={}, parsed={}",
original.sections.len(),
parsed.sections.len()
),
});
}
for (id, section) in &original.sections {
if let Some(parsed_section) = parsed.sections.get(id) {
if section.schema != parsed_section.schema {
return Err(PrettyPrintError::RoundTripFailed {
msg: format!(
"Schema mismatch for section '{}': original={:?}, parsed={:?}",
id, section.schema, parsed_section.schema
),
});
}
if section.rows.len() != parsed_section.rows.len() {
return Err(PrettyPrintError::RoundTripFailed {
msg: format!(
"Row count mismatch for section '{}': original={}, parsed={}",
id,
section.rows.len(),
parsed_section.rows.len()
),
});
}
for (row_idx, (orig_row, parsed_row)) in section
.rows
.iter()
.zip(parsed_section.rows.iter())
.enumerate()
{
if orig_row.len() != parsed_row.len() {
return Err(PrettyPrintError::RoundTripFailed {
msg: format!(
"Column count mismatch in section '{}' row {}: original={}, parsed={}",
id,
row_idx,
orig_row.len(),
parsed_row.len()
),
});
}
for (col_idx, (orig_val, parsed_val)) in
orig_row.iter().zip(parsed_row.iter()).enumerate()
{
if !values_equal(orig_val, parsed_val) {
return Err(PrettyPrintError::RoundTripFailed {
msg: format!(
"Value mismatch in section '{}' row {} col {}: original={:?}, parsed={:?}",
id, row_idx, col_idx, orig_val, parsed_val
),
});
}
}
}
} else {
return Err(PrettyPrintError::RoundTripFailed {
msg: format!("Section '{}' missing in parsed document", id),
});
}
}
Ok(())
}
pub fn config(&self) -> &PrettyPrinterConfig {
&self.config
}
}
impl Default for PrettyPrinter {
fn default() -> Self {
Self::new()
}
}
fn values_equal(a: &crate::llm::types::DxLlmValue, b: &crate::llm::types::DxLlmValue) -> bool {
use crate::llm::types::DxLlmValue;
match (a, b) {
(DxLlmValue::Str(s1), DxLlmValue::Str(s2)) => s1 == s2,
(DxLlmValue::Num(n1), DxLlmValue::Num(n2)) => {
(n1 - n2).abs() < f64::EPSILON || (n1.is_nan() && n2.is_nan())
}
(DxLlmValue::Bool(b1), DxLlmValue::Bool(b2)) => b1 == b2,
(DxLlmValue::Null, DxLlmValue::Null) => true,
(DxLlmValue::Ref(r1), DxLlmValue::Ref(r2)) => r1 == r2,
(DxLlmValue::Arr(arr1), DxLlmValue::Arr(arr2)) => {
if arr1.len() != arr2.len() {
return false;
}
arr1.iter()
.zip(arr2.iter())
.all(|(v1, v2)| values_equal(v1, v2))
}
(DxLlmValue::Obj(obj1), DxLlmValue::Obj(obj2)) => {
if obj1.len() != obj2.len() {
return false;
}
obj1.iter()
.all(|(k, v1)| obj2.get(k).is_some_and(|v2| values_equal(v1, v2)))
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::llm::types::{DxLlmValue, DxSection};
#[test]
fn test_pretty_printer_empty_document() {
let printer = PrettyPrinter::new();
let doc = DxDocument::new();
let result = printer.format(&doc);
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
}
#[test]
fn test_pretty_printer_with_config() {
let config = PrettyPrinterConfig::new().with_validation(false);
let printer = PrettyPrinter::with_config(config);
let mut doc = DxDocument::new();
doc.context
.insert("name".to_string(), DxLlmValue::Str("Test".to_string()));
doc.context
.insert("count".to_string(), DxLlmValue::Num(42.0));
let result = printer.format(&doc);
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.contains("name"));
assert!(output.contains("Test"));
}
#[test]
fn test_pretty_printer_with_section() {
let config = PrettyPrinterConfig::new().with_validation(false);
let printer = PrettyPrinter::with_config(config);
let mut doc = DxDocument::new();
let mut section = DxSection::new(vec!["id".to_string(), "name".to_string()]);
section.rows.push(vec![
DxLlmValue::Num(1.0),
DxLlmValue::Str("Alpha".to_string()),
]);
section.rows.push(vec![
DxLlmValue::Num(2.0),
DxLlmValue::Str("Beta".to_string()),
]);
doc.sections.insert('d', section);
let result = printer.format(&doc);
assert!(result.is_ok(), "Format should succeed");
let output = result.unwrap();
assert!(!output.is_empty(), "Output should not be empty");
}
#[test]
fn test_pretty_printer_round_trip() {
let config = PrettyPrinterConfig::new().with_validation(false);
let printer = PrettyPrinter::with_config(config);
let mut doc = DxDocument::new();
doc.context
.insert("name".to_string(), DxLlmValue::Str("Test".to_string()));
doc.context
.insert("count".to_string(), DxLlmValue::Num(42.0));
doc.context
.insert("active".to_string(), DxLlmValue::Bool(true));
let mut section = DxSection::new(vec!["id".to_string(), "vl".to_string()]);
section.rows.push(vec![
DxLlmValue::Num(1.0),
DxLlmValue::Str("Alpha".to_string()),
]);
doc.sections.insert('d', section);
let result = printer.format(&doc);
assert!(
result.is_ok(),
"Pretty printer should succeed: {:?}",
result.err()
);
}
#[test]
fn test_pretty_printer_unchecked() {
let printer = PrettyPrinter::new();
let mut doc = DxDocument::new();
doc.context
.insert("name".to_string(), DxLlmValue::Str("Test".to_string()));
let output = printer.format_unchecked(&doc);
assert!(output.contains("name"));
assert!(output.contains("Test"));
}
#[test]
fn test_pretty_printer_no_validation() {
let config = PrettyPrinterConfig::new().with_validation(false);
let printer = PrettyPrinter::with_config(config);
let mut doc = DxDocument::new();
doc.context
.insert("name".to_string(), DxLlmValue::Str("Test".to_string()));
let result = printer.format(&doc);
assert!(result.is_ok());
}
#[test]
#[ignore] fn test_pretty_printer_with_arrays() {
let config = PrettyPrinterConfig::new().with_validation(false);
let printer = PrettyPrinter::with_config(config);
let mut doc = DxDocument::new();
doc.context.insert(
"workspaces".to_string(),
DxLlmValue::Arr(vec![
DxLlmValue::Str("frontend/www".to_string()),
DxLlmValue::Str("frontend/mobile".to_string()),
]),
);
let result = printer.format(&doc);
assert!(result.is_ok(), "Format should succeed");
let output = result.unwrap();
assert!(!output.is_empty(), "Output should not be empty");
}
#[test]
fn test_values_equal() {
assert!(values_equal(
&DxLlmValue::Str("test".to_string()),
&DxLlmValue::Str("test".to_string())
));
assert!(!values_equal(
&DxLlmValue::Str("test".to_string()),
&DxLlmValue::Str("other".to_string())
));
assert!(values_equal(&DxLlmValue::Num(42.0), &DxLlmValue::Num(42.0)));
assert!(!values_equal(
&DxLlmValue::Num(42.0),
&DxLlmValue::Num(43.0)
));
assert!(values_equal(
&DxLlmValue::Bool(true),
&DxLlmValue::Bool(true)
));
assert!(!values_equal(
&DxLlmValue::Bool(true),
&DxLlmValue::Bool(false)
));
assert!(values_equal(&DxLlmValue::Null, &DxLlmValue::Null));
assert!(values_equal(
&DxLlmValue::Arr(vec![DxLlmValue::Num(1.0), DxLlmValue::Num(2.0)]),
&DxLlmValue::Arr(vec![DxLlmValue::Num(1.0), DxLlmValue::Num(2.0)])
));
assert!(!values_equal(
&DxLlmValue::Arr(vec![DxLlmValue::Num(1.0)]),
&DxLlmValue::Arr(vec![DxLlmValue::Num(2.0)])
));
assert!(!values_equal(
&DxLlmValue::Num(42.0),
&DxLlmValue::Str("42".to_string())
));
}
}
#[cfg(test)]
mod property_tests {
use super::*;
use crate::llm::types::{DxLlmValue, DxSection};
use indexmap::IndexMap;
use proptest::prelude::*;
fn arb_simple_value() -> impl Strategy<Value = DxLlmValue> {
prop_oneof![
Just(DxLlmValue::Bool(true)),
Just(DxLlmValue::Bool(false)),
Just(DxLlmValue::Null),
(-1000i64..1000i64).prop_map(|n| DxLlmValue::Num(n as f64)),
"[a-zA-Z][a-zA-Z0-9]{0,10}".prop_map(DxLlmValue::Str),
]
}
fn arb_key() -> impl Strategy<Value = String> {
prop_oneof![
Just("nm".to_string()),
Just("tt".to_string()),
Just("ds".to_string()),
Just("st".to_string()),
Just("ct".to_string()),
Just("ac".to_string()),
Just("id".to_string()),
Just("vl".to_string()),
]
}
fn arb_section_id() -> impl Strategy<Value = char> {
prop_oneof![Just('d'), Just('f'), Just('o'), Just('p'), Just('u'),]
}
fn arb_context() -> impl Strategy<Value = IndexMap<String, DxLlmValue>> {
proptest::collection::vec((arb_key(), arb_simple_value()), 0..4)
.prop_map(|v| v.into_iter().collect())
}
fn arb_section() -> impl Strategy<Value = DxSection> {
proptest::collection::vec(arb_key(), 1..4).prop_flat_map(|schema| {
let schema_len = schema.len();
let row_strategy =
proptest::collection::vec(arb_simple_value(), schema_len..=schema_len);
let rows_strategy = proptest::collection::vec(row_strategy, 0..4);
rows_strategy.prop_map(move |rows| {
let mut section = DxSection::new(schema.clone());
for row in rows {
let _ = section.add_row(row);
}
section
})
})
}
fn arb_document() -> impl Strategy<Value = DxDocument> {
(
arb_context(),
proptest::collection::vec((arb_section_id(), arb_section()), 0..2),
)
.prop_map(|(context, sections)| {
let mut doc = DxDocument::new();
doc.context = context;
doc.sections = sections.into_iter().collect();
doc
})
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_pretty_printer_round_trip(doc in arb_document()) {
let printer = PrettyPrinter::with_config(
PrettyPrinterConfig::new().with_validation(false)
);
let result = printer.format(&doc);
prop_assert!(
result.is_ok(),
"PrettyPrinter should succeed for valid document: {:?}\nError: {:?}",
doc, result.err()
);
}
#[test]
fn prop_pretty_printer_output_parseable(doc in arb_document()) {
let printer = PrettyPrinter::with_config(
PrettyPrinterConfig::new()
.with_validation(false) );
let parser = crate::llm::human_parser::HumanParser::new();
let output = printer.format(&doc).unwrap();
let _parsed = parser.parse(&output);
prop_assert!(true);
}
#[test]
fn prop_pretty_printer_preserves_context(context in arb_context()) {
let printer = PrettyPrinter::with_config(
PrettyPrinterConfig::new().with_validation(false)
);
let mut doc = DxDocument::new();
doc.context = context.clone();
let result = printer.format(&doc);
prop_assert!(result.is_ok(), "Formatting should succeed");
}
#[test]
fn prop_pretty_printer_preserves_sections(section in arb_section()) {
let printer = PrettyPrinter::with_config(
PrettyPrinterConfig::new().with_validation(false)
);
let mut doc = DxDocument::new();
doc.sections.insert('d', section.clone());
let result = printer.format(&doc);
prop_assert!(result.is_ok(), "Formatting should succeed");
}
}
}