#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
use crate::error::{MAX_INPUT_SIZE, MAX_RECURSION_DEPTH, MAX_TABLE_ROWS};
use crate::llm::{human_to_llm, llm_to_human};
#[cfg(feature = "wasm")]
use crate::llm::tokens::{ModelType, TokenCounter};
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[derive(Debug, Clone)]
pub struct TransformResult {
success: bool,
content: String,
error: Option<String>,
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl TransformResult {
#[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
pub fn success(&self) -> bool {
self.success
}
#[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
pub fn content(&self) -> String {
self.content.clone()
}
#[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
pub fn error(&self) -> Option<String> {
self.error.clone()
}
}
impl TransformResult {
pub fn ok(content: String) -> Self {
Self {
success: true,
content,
error: None,
}
}
pub fn err(error: String) -> Self {
Self {
success: false,
content: String::new(),
error: Some(error),
}
}
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[derive(Debug, Clone)]
pub struct ValidationResult {
success: bool,
error: Option<String>,
line: Option<u32>,
column: Option<u32>,
hint: Option<String>,
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl ValidationResult {
#[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
pub fn success(&self) -> bool {
self.success
}
#[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
pub fn error(&self) -> Option<String> {
self.error.clone()
}
#[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
pub fn line(&self) -> Option<u32> {
self.line
}
#[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
pub fn column(&self) -> Option<u32> {
self.column
}
#[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
pub fn hint(&self) -> Option<String> {
self.hint.clone()
}
}
impl ValidationResult {
pub fn valid() -> Self {
Self {
success: true,
error: None,
line: None,
column: None,
hint: None,
}
}
pub fn invalid(error: String, line: u32, column: u32, hint: String) -> Self {
Self {
success: false,
error: Some(error),
line: Some(line),
column: Some(column),
hint: Some(hint),
}
}
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[derive(Debug, Clone)]
pub struct SerializerConfig {
indent_size: usize,
preserve_comments: bool,
smart_quoting: bool,
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl SerializerConfig {
#[cfg_attr(feature = "wasm", wasm_bindgen(constructor))]
pub fn new() -> Self {
Self {
indent_size: 2,
preserve_comments: true,
smart_quoting: true,
}
}
#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = setIndentSize))]
pub fn set_indent_size(&mut self, size: usize) {
self.indent_size = if size == 4 { 4 } else { 2 };
}
#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = setPreserveComments))]
pub fn set_preserve_comments(&mut self, preserve: bool) {
self.preserve_comments = preserve;
}
#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = setSmartQuoting))]
pub fn set_smart_quoting(&mut self, smart: bool) {
self.smart_quoting = smart;
}
}
impl Default for SerializerConfig {
fn default() -> Self {
Self::new()
}
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub struct DxSerializer {
#[allow(dead_code)]
config: SerializerConfig,
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl DxSerializer {
#[cfg_attr(feature = "wasm", wasm_bindgen(constructor))]
pub fn new() -> Self {
let config = SerializerConfig::new();
Self { config }
}
#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = withConfig))]
pub fn with_config(config: SerializerConfig) -> Self {
Self { config }
}
#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = toHuman))]
pub fn to_human(&self, llm_input: &str) -> TransformResult {
if llm_input.trim().is_empty() {
return TransformResult::ok(String::new());
}
match llm_to_human(llm_input) {
Ok(human) => TransformResult::ok(human),
Err(e) => TransformResult::err(format!("Parse error: {}", e)),
}
}
#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = toDense))]
pub fn to_dense(&self, human_input: &str) -> TransformResult {
if human_input.trim().is_empty() {
return TransformResult::ok(String::new());
}
match human_to_llm(human_input) {
Ok(llm) => TransformResult::ok(llm),
Err(e) => TransformResult::err(format!("Parse error: {}", e)),
}
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub fn validate(&self, content: &str) -> ValidationResult {
let mut bracket_stack: Vec<(char, u32, u32)> = Vec::new();
let mut in_string = false;
let mut string_char = '"';
let mut string_start: Option<(u32, u32)> = None;
for (line_idx, line) in content.lines().enumerate() {
let line_num = (line_idx + 1) as u32;
let mut col = 0u32;
let mut chars = line.chars().peekable();
while let Some(ch) = chars.next() {
col += 1;
if in_string && ch == '\\' {
chars.next(); col += 1;
continue;
}
if !in_string && (ch == '"' || ch == '\'') {
in_string = true;
string_char = ch;
string_start = Some((line_num, col));
continue;
}
if in_string && ch == string_char {
in_string = false;
string_start = None;
continue;
}
if in_string {
continue;
}
match ch {
'{' | '[' | '(' => {
bracket_stack.push((ch, line_num, col));
}
'}' | ']' | ')' => {
let expected = match ch {
'}' => '{',
']' => '[',
')' => '(',
_ => unreachable!(),
};
if let Some((open_char, open_line, open_col)) = bracket_stack.pop() {
if open_char != expected {
return ValidationResult::invalid(
format!(
"Mismatched bracket: expected '{}' but found '{}'",
matching_close(open_char),
ch
),
line_num,
col,
format!(
"Opening '{}' at line {}, column {} expects '{}'",
open_char,
open_line,
open_col,
matching_close(open_char)
),
);
}
} else {
return ValidationResult::invalid(
format!("Unexpected closing bracket '{}'", ch),
line_num,
col,
format!("No matching opening bracket for '{}'", ch),
);
}
}
_ => {}
}
}
}
if in_string {
if let Some((line, col)) = string_start {
return ValidationResult::invalid(
format!("Unclosed string starting with '{}'", string_char),
line,
col,
format!("Add a closing '{}' to complete the string", string_char),
);
}
}
if let Some((ch, line, col)) = bracket_stack.pop() {
return ValidationResult::invalid(
format!("Unclosed bracket '{}'", ch),
line,
col,
format!(
"Add a closing '{}' to match the opening '{}'",
matching_close(ch),
ch
),
);
}
ValidationResult::valid()
}
#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = isSaveable))]
pub fn is_saveable(&self, content: &str) -> bool {
self.validate(content).success
}
#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = maxInputSize))]
pub fn max_input_size(&self) -> usize {
MAX_INPUT_SIZE
}
#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = maxRecursionDepth))]
pub fn max_recursion_depth(&self) -> usize {
MAX_RECURSION_DEPTH
}
#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = maxTableRows))]
pub fn max_table_rows(&self) -> usize {
MAX_TABLE_ROWS
}
}
impl Default for DxSerializer {
fn default() -> Self {
Self::new()
}
}
fn matching_close(open: char) -> char {
match open {
'{' => '}',
'[' => ']',
'(' => ')',
_ => open,
}
}
pub fn smart_quote(value: &str) -> String {
let has_single = value.contains('\'');
let has_double = value.contains('"');
if !has_single && !has_double {
if !value.contains(' ')
&& !value.contains('#')
&& !value.contains('|')
&& !value.contains('^')
&& !value.contains(':')
{
return value.to_string();
}
return format!("\"{}\"", value);
}
if has_single && !has_double {
return format!("\"{}\"", value);
}
if has_double && !has_single {
return format!("'{}'", value);
}
let escaped = value.replace('"', "\\\"");
format!("\"{}\"", escaped)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen(start)]
pub fn init_wasm() {
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
}
#[cfg(feature = "wasm")]
#[wasm_bindgen(js_name = "serializerVersion")]
pub fn serializer_version() -> String {
format!(
"dx-serializer v{} ({})",
env!("CARGO_PKG_VERSION"),
if cfg!(debug_assertions) {
"debug"
} else {
"release"
}
)
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[derive(Debug, Clone)]
pub struct TokenCountResult {
count: usize,
model: String,
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl TokenCountResult {
#[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
pub fn count(&self) -> usize {
self.count
}
#[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
pub fn model(&self) -> String {
self.model.clone()
}
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[derive(Debug, Clone)]
pub struct AllTokenCountsResult {
gpt4o: usize,
claude: usize,
gemini: usize,
other: usize,
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl AllTokenCountsResult {
#[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
pub fn gpt4o(&self) -> usize {
self.gpt4o
}
#[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
pub fn claude(&self) -> usize {
self.claude
}
#[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
pub fn gemini(&self) -> usize {
self.gemini
}
#[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
pub fn other(&self) -> usize {
self.other
}
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn count_tokens(text: &str, model: &str) -> TokenCountResult {
let counter = TokenCounter::new();
let model_type = match model.to_lowercase().as_str() {
"gpt4o" | "gpt-4o" | "openai" => ModelType::Gpt4o,
"claude" | "sonnet" | "claude-sonnet" => ModelType::ClaudeSonnet4,
"gemini" | "gemini3" | "gemini-3" => ModelType::Gemini3,
_ => ModelType::Other,
};
let info = counter.count(text, model_type);
TokenCountResult {
count: info.count,
model: format!("{}", model_type),
}
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn count_tokens_all(text: &str) -> AllTokenCountsResult {
let counter = TokenCounter::new();
let counts = counter.count_primary_models(text);
AllTokenCountsResult {
gpt4o: counts.get(&ModelType::Gpt4o).map(|i| i.count).unwrap_or(0),
claude: counts
.get(&ModelType::ClaudeSonnet4)
.map(|i| i.count)
.unwrap_or(0),
gemini: counts
.get(&ModelType::Gemini3)
.map(|i| i.count)
.unwrap_or(0),
other: counts.get(&ModelType::Other).map(|i| i.count).unwrap_or(0),
}
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn dx_to_json(dx: &str) -> Result<String, JsValue> {
use crate::parser::parse;
let value =
parse(dx.as_bytes()).map_err(|e| JsValue::from_str(&format!("Parse error: {}", e)))?;
dx_value_to_json(&value).map_err(|e| JsValue::from_str(&e))
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn dx_to_yaml(dx: &str) -> Result<String, JsValue> {
use crate::parser::parse;
let value =
parse(dx.as_bytes()).map_err(|e| JsValue::from_str(&format!("Parse error: {}", e)))?;
dx_value_to_yaml(&value).map_err(|e| JsValue::from_str(&e))
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn dx_to_toml(dx: &str) -> Result<String, JsValue> {
use crate::parser::parse;
let value =
parse(dx.as_bytes()).map_err(|e| JsValue::from_str(&format!("Parse error: {}", e)))?;
dx_value_to_toml(&value).map_err(|e| JsValue::from_str(&e))
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn dx_to_toon_wasm(dx: &str) -> Result<String, JsValue> {
crate::converters::dx_to_toon(dx).map_err(|e| JsValue::from_str(&e))
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn llm_to_machine(llm: &str) -> Result<Vec<u8>, JsValue> {
use crate::llm::convert;
let machine = convert::llm_to_machine(llm)
.map_err(|e| JsValue::from_str(&format!("Failed to convert to machine format: {}", e)))?;
Ok(machine.data)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn human_to_machine(human: &str) -> Result<Vec<u8>, JsValue> {
use crate::llm::convert;
let machine = convert::human_to_machine(human)
.map_err(|e| JsValue::from_str(&format!("Failed to convert to machine format: {}", e)))?;
Ok(machine.data)
}
#[cfg(any(feature = "wasm", feature = "converters", test))]
use crate::types::DxValue;
#[allow(dead_code)] #[cfg(any(feature = "wasm", feature = "converters", test))]
fn dx_value_to_json(value: &DxValue) -> Result<String, String> {
let json_value = dx_value_to_serde_json(value)?;
serde_json::to_string_pretty(&json_value)
.map_err(|e| format!("JSON serialization error: {}", e))
}
#[allow(dead_code)] #[cfg(any(feature = "wasm", feature = "converters", test))]
fn dx_value_to_serde_json(value: &DxValue) -> Result<serde_json::Value, String> {
match value {
DxValue::Null => Ok(serde_json::Value::Null),
DxValue::Bool(b) => Ok(serde_json::Value::Bool(*b)),
DxValue::Int(i) => Ok(serde_json::Value::Number(serde_json::Number::from(*i))),
DxValue::Float(f) => serde_json::Number::from_f64(*f)
.map(serde_json::Value::Number)
.ok_or_else(|| "Invalid float value".to_string()),
DxValue::String(s) => Ok(serde_json::Value::String(s.clone())),
DxValue::Array(arr) => {
let items: Result<Vec<_>, _> = arr.values.iter().map(dx_value_to_serde_json).collect();
Ok(serde_json::Value::Array(items?))
}
DxValue::Object(obj) => {
let mut map = serde_json::Map::new();
for (k, v) in obj.iter() {
map.insert(k.clone(), dx_value_to_serde_json(v)?);
}
Ok(serde_json::Value::Object(map))
}
DxValue::Table(table) => {
let mut rows = Vec::new();
for row in &table.rows {
let mut obj = serde_json::Map::new();
for (i, col) in table.schema.columns.iter().enumerate() {
if let Some(val) = row.get(i) {
obj.insert(col.name.clone(), dx_value_to_serde_json(val)?);
}
}
rows.push(serde_json::Value::Object(obj));
}
Ok(serde_json::Value::Array(rows))
}
DxValue::Ref(id) => Ok(serde_json::Value::String(format!("@{}", id))),
}
}
#[cfg(any(feature = "wasm", test))]
fn dx_value_to_yaml(value: &DxValue) -> Result<String, String> {
let mut output = String::new();
dx_value_to_yaml_impl(value, &mut output, 0)?;
Ok(output)
}
#[cfg(any(feature = "wasm", test))]
fn dx_value_to_yaml_impl(
value: &DxValue,
output: &mut String,
indent: usize,
) -> Result<(), String> {
let indent_str = " ".repeat(indent);
match value {
DxValue::Null => output.push_str("null"),
DxValue::Bool(b) => output.push_str(if *b { "true" } else { "false" }),
DxValue::Int(i) => output.push_str(&i.to_string()),
DxValue::Float(f) => output.push_str(&f.to_string()),
DxValue::String(s) => {
if s.contains(':')
|| s.contains('#')
|| s.contains('\n')
|| s.starts_with(' ')
|| s.ends_with(' ')
{
output.push('"');
output.push_str(
&s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n"),
);
output.push('"');
} else {
output.push_str(s);
}
}
DxValue::Array(arr) => {
if arr.values.is_empty() {
output.push_str("[]");
} else {
for (i, item) in arr.values.iter().enumerate() {
if i > 0 {
output.push('\n');
output.push_str(&indent_str);
}
output.push_str("- ");
dx_value_to_yaml_impl(item, output, indent + 1)?;
}
}
}
DxValue::Object(obj) => {
for (i, (k, v)) in obj.iter().enumerate() {
if i > 0 {
output.push('\n');
output.push_str(&indent_str);
}
output.push_str(k);
output.push_str(": ");
if matches!(v, DxValue::Object(_) | DxValue::Array(_)) {
output.push('\n');
output.push_str(&" ".repeat(indent + 1));
}
dx_value_to_yaml_impl(v, output, indent + 1)?;
}
}
DxValue::Table(table) => {
for (i, row) in table.rows.iter().enumerate() {
if i > 0 {
output.push('\n');
output.push_str(&indent_str);
}
output.push_str("- ");
for (j, col) in table.schema.columns.iter().enumerate() {
if j > 0 {
output.push('\n');
output.push_str(&" ".repeat(indent + 1));
}
output.push_str(&col.name);
output.push_str(": ");
if let Some(val) = row.get(j) {
dx_value_to_yaml_impl(val, output, indent + 2)?;
}
}
}
}
DxValue::Ref(id) => {
output.push_str(&format!("\"@{}\"", id));
}
}
Ok(())
}
#[cfg(any(feature = "wasm", test))]
fn dx_value_to_toml(value: &DxValue) -> Result<String, String> {
let mut output = String::new();
match value {
DxValue::Object(obj) => {
for (k, v) in obj.iter() {
if !matches!(
v,
DxValue::Object(_) | DxValue::Array(_) | DxValue::Table(_)
) {
output.push_str(k);
output.push_str(" = ");
dx_value_to_toml_value(v, &mut output)?;
output.push('\n');
}
}
for (k, v) in obj.iter() {
if let DxValue::Object(nested) = v {
output.push('\n');
output.push('[');
output.push_str(k);
output.push_str("]\n");
for (nk, nv) in nested.iter() {
output.push_str(nk);
output.push_str(" = ");
dx_value_to_toml_value(nv, &mut output)?;
output.push('\n');
}
}
}
for (k, v) in obj.iter() {
if let DxValue::Array(arr) = v {
output.push_str(k);
output.push_str(" = [");
for (i, item) in arr.values.iter().enumerate() {
if i > 0 {
output.push_str(", ");
}
dx_value_to_toml_value(item, &mut output)?;
}
output.push_str("]\n");
}
}
}
_ => {
return Err("TOML root must be an object".to_string());
}
}
Ok(output)
}
#[cfg(any(feature = "wasm", test))]
fn dx_value_to_toml_value(value: &DxValue, output: &mut String) -> Result<(), String> {
match value {
DxValue::Null => output.push_str("\"\""),
DxValue::Bool(b) => output.push_str(if *b { "true" } else { "false" }),
DxValue::Int(i) => output.push_str(&i.to_string()),
DxValue::Float(f) => output.push_str(&f.to_string()),
DxValue::String(s) => {
output.push('"');
output.push_str(&s.replace('\\', "\\\\").replace('"', "\\\""));
output.push('"');
}
DxValue::Array(arr) => {
output.push('[');
for (i, item) in arr.values.iter().enumerate() {
if i > 0 {
output.push_str(", ");
}
dx_value_to_toml_value(item, output)?;
}
output.push(']');
}
DxValue::Object(_) => output.push_str("{}"),
DxValue::Table(_) => output.push_str("[[]]"),
DxValue::Ref(id) => {
output.push('"');
output.push('@');
output.push_str(&id.to_string());
output.push('"');
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transform_result() {
let ok = TransformResult::ok("content".to_string());
assert!(ok.success());
assert_eq!(ok.content(), "content");
assert!(ok.error().is_none());
let err = TransformResult::err("error".to_string());
assert!(!err.success());
assert!(err.content().is_empty());
assert_eq!(err.error(), Some("error".to_string()));
}
#[test]
fn test_validation_result() {
let valid = ValidationResult::valid();
assert!(valid.success());
assert!(valid.error().is_none());
let invalid = ValidationResult::invalid("error".to_string(), 1, 5, "hint".to_string());
assert!(!invalid.success());
assert_eq!(invalid.error(), Some("error".to_string()));
assert_eq!(invalid.line(), Some(1));
assert_eq!(invalid.column(), Some(5));
assert_eq!(invalid.hint(), Some("hint".to_string()));
}
#[test]
fn test_serializer_to_human() {
let serializer = DxSerializer::new();
let result = serializer.to_human("host|localhost\nport|5432");
assert!(result.success(), "to_human failed: {:?}", result.error());
assert!(result.content().contains("host") || result.content().contains("localhost"));
}
#[test]
fn test_serializer_to_dense() {
let serializer = DxSerializer::new();
let human = serializer.to_human("debug|+\nprod|-");
assert!(human.success(), "to_human failed: {:?}", human.error());
let dense = serializer.to_dense(&human.content());
assert!(dense.success(), "to_dense failed: {:?}", dense.error());
assert!(dense.content().contains("|") || dense.content().contains("debug"));
}
#[test]
fn test_validate_valid_content() {
let serializer = DxSerializer::new();
let result = serializer.validate("key: value\nother: data");
assert!(result.success());
}
#[test]
fn test_validate_unclosed_bracket() {
let serializer = DxSerializer::new();
let result = serializer.validate("data: {\n key: value");
assert!(!result.success());
assert!(result.error().unwrap().contains("Unclosed bracket"));
assert_eq!(result.line(), Some(1));
assert!(result.hint().is_some());
}
#[test]
fn test_validate_unclosed_string() {
let serializer = DxSerializer::new();
let result = serializer.validate("key: \"unclosed string");
assert!(!result.success());
assert!(result.error().unwrap().contains("Unclosed string"));
assert!(result.hint().is_some());
}
#[test]
fn test_validate_mismatched_brackets() {
let serializer = DxSerializer::new();
let result = serializer.validate("data: [value}");
assert!(!result.success());
assert!(result.error().unwrap().contains("Mismatched bracket"));
}
#[test]
fn test_is_saveable() {
let serializer = DxSerializer::new();
assert!(serializer.is_saveable("key: value"));
assert!(!serializer.is_saveable("key: {unclosed"));
assert!(!serializer.is_saveable("key: \"unclosed"));
}
#[test]
fn test_smart_quote_simple() {
assert_eq!(smart_quote("hello"), "hello");
assert_eq!(smart_quote("hello world"), "\"hello world\"");
}
#[test]
fn test_smart_quote_apostrophe() {
assert_eq!(smart_quote("don't"), "\"don't\"");
assert_eq!(smart_quote("it's working"), "\"it's working\"");
}
#[test]
fn test_smart_quote_double_quotes() {
assert_eq!(smart_quote("say \"hello\""), "'say \"hello\"'");
}
#[test]
fn test_smart_quote_both() {
assert_eq!(
smart_quote("don't say \"hello\""),
"\"don't say \\\"hello\\\"\""
);
}
#[test]
fn test_smart_quote_special_chars() {
assert_eq!(smart_quote("key:value"), "\"key:value\"");
assert_eq!(smart_quote("a|b|c"), "\"a|b|c\"");
assert_eq!(smart_quote("a#b"), "\"a#b\"");
}
#[test]
fn test_config() {
let mut config = SerializerConfig::new();
assert_eq!(config.indent_size, 2);
config.set_indent_size(4);
assert_eq!(config.indent_size, 4);
config.set_indent_size(3); assert_eq!(config.indent_size, 2);
}
#[test]
fn test_empty_input() {
let serializer = DxSerializer::new();
let human = serializer.to_human("");
assert!(human.success());
assert!(human.content().is_empty());
let dense = serializer.to_dense("");
assert!(dense.success());
assert!(dense.content().is_empty());
}
#[test]
fn test_dx_to_json() {
let dx = "name:test\nversion:100";
let result = dx_value_to_json(&crate::parser::parse(dx.as_bytes()).unwrap());
assert!(result.is_ok());
let json = result.unwrap();
assert!(json.contains("name"));
assert!(json.contains("test"));
assert!(json.contains("version"));
assert!(json.contains("100"));
}
#[test]
fn test_dx_to_yaml() {
let dx = "name:test\nversion:100";
let result = dx_value_to_yaml(&crate::parser::parse(dx.as_bytes()).unwrap());
assert!(result.is_ok());
let yaml = result.unwrap();
assert!(yaml.contains("name"));
assert!(yaml.contains("test"));
}
#[test]
fn test_dx_to_toml() {
let dx = "name:test\nversion:100";
let result = dx_value_to_toml(&crate::parser::parse(dx.as_bytes()).unwrap());
assert!(result.is_ok());
let toml = result.unwrap();
assert!(toml.contains("name"));
assert!(toml.contains("test"));
}
}
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
fn valid_abbrev_key() -> impl Strategy<Value = String> {
prop::string::string_regex("[a-z]{2,3}")
.unwrap()
.prop_filter("non-empty key", |s| !s.is_empty())
}
fn simple_value() -> impl Strategy<Value = String> {
prop_oneof![
prop::string::string_regex("[a-zA-Z][a-zA-Z0-9]{0,15}").unwrap(),
(1i32..10000).prop_map(|n| n.to_string()),
]
}
#[allow(dead_code)] fn _llm_bool() -> impl Strategy<Value = String> {
prop::bool::ANY.prop_map(|b| if b { "+".to_string() } else { "-".to_string() })
}
#[allow(dead_code)] fn _llm_context_section() -> impl Strategy<Value = String> {
prop::collection::vec((valid_abbrev_key(), simple_value()), 1..4).prop_map(|pairs| {
pairs
.into_iter()
.map(|(k, v)| format!("{}|{}", k, v))
.collect::<Vec<_>>()
.join("\n")
})
}
#[allow(dead_code)] fn _llm_data_section() -> impl Strategy<Value = String> {
(
prop::string::string_regex("[a-z]").unwrap(), prop::collection::vec(valid_abbrev_key(), 2..4), prop::collection::vec(simple_value(), 2..4), )
.prop_filter("schema and row same length", |(_, schema, row)| {
schema.len() == row.len()
})
.prop_map(|(id, schema, row)| {
let schema_str = schema.join("|");
let row_str = row.join("|");
format!("#{}({})\n{}", id, schema_str, row_str)
})
}
#[allow(dead_code)] fn _valid_llm_content() -> impl Strategy<Value = String> {
prop_oneof![_llm_context_section(), _llm_data_section(),]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_llm_round_trip_context(
pairs in prop::collection::vec(
(valid_abbrev_key(), simple_value()),
1..3,
)
) {
let keys: Vec<_> = pairs.iter().map(|(k, _)| k.clone()).collect();
let unique_keys: std::collections::HashSet<_> = keys.iter().collect();
prop_assume!(keys.len() == unique_keys.len());
let serializer = DxSerializer::new();
let llm: String = pairs
.iter()
.map(|(k, v)| format!("{}|{}", k, v))
.collect::<Vec<_>>()
.join("\n");
let human_result = serializer.to_human(&llm);
prop_assert!(human_result.success(), "to_human failed: {:?}", human_result.error());
let llm_result = serializer.to_dense(&human_result.content());
prop_assert!(llm_result.success(), "to_dense failed: {:?}", llm_result.error());
let result = llm_result.content();
for (_, value) in &pairs {
prop_assert!(
result.contains(value),
"Value '{}' not found in result: '{}'", value, result
);
}
}
#[test]
fn prop_llm_round_trip_booleans(
key1 in valid_abbrev_key(),
key2 in valid_abbrev_key(),
bool1 in prop::bool::ANY,
bool2 in prop::bool::ANY
) {
prop_assume!(key1 != key2);
let serializer = DxSerializer::new();
let b1 = if bool1 { "true" } else { "false" };
let b2 = if bool2 { "true" } else { "false" };
let llm = format!("{}={}\n{}={}", key1, b1, key2, b2);
let human_result = serializer.to_human(&llm);
prop_assert!(human_result.success(), "to_human failed: {:?}", human_result.error());
let human = human_result.content();
if bool1 {
prop_assert!(human.contains("true"),
"Boolean true not found in human format: '{}'", human);
} else {
prop_assert!(human.contains("false"),
"Boolean false not found in human format: '{}'", human);
}
let llm_result = serializer.to_dense(&human);
prop_assert!(llm_result.success(), "to_dense failed: {:?}", llm_result.error());
let result = llm_result.content();
prop_assert!(
result.contains("true") || result.contains("false"),
"Boolean values not found in LLM result: '{}'", result
);
}
#[test]
fn prop_empty_content_round_trip(content in "\\s*") {
let serializer = DxSerializer::new();
let human_result = serializer.to_human(&content);
prop_assert!(human_result.success());
let dense_result = serializer.to_dense(&human_result.content());
prop_assert!(dense_result.success());
}
}
}
#[cfg(test)]
mod string_preservation_tests {
use super::*;
use proptest::prelude::*;
fn url_string() -> impl Strategy<Value = String> {
(
prop::string::string_regex("https?://[a-z]+\\.[a-z]{2,4}").unwrap(),
prop::string::string_regex("/[a-z]+").unwrap(),
prop::collection::vec(
(
prop::string::string_regex("[a-z]+").unwrap(),
prop::string::string_regex("[a-zA-Z0-9]+").unwrap(),
),
0..3,
),
)
.prop_map(|(base, path, params)| {
if params.is_empty() {
format!("{}{}", base, path)
} else {
let query: String = params
.into_iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join("&");
format!("{}{}?{}", base, path, query)
}
})
}
fn apostrophe_string() -> impl Strategy<Value = String> {
prop_oneof![
Just("don't".to_string()),
Just("it's".to_string()),
Just("won't".to_string()),
Just("can't".to_string()),
Just("I'm".to_string()),
prop::string::string_regex("[A-Z][a-z]+'s [a-z]+").unwrap(),
]
}
fn double_quote_string() -> impl Strategy<Value = String> {
prop_oneof![
Just("say \"hello\"".to_string()),
Just("the \"best\" way".to_string()),
prop::string::string_regex("[a-z]+ \"[a-z]+\" [a-z]+").unwrap(),
]
}
fn mixed_quote_string() -> impl Strategy<Value = String> {
prop_oneof![
Just("don't say \"hello\"".to_string()),
Just("it's \"great\"".to_string()),
Just("can't \"stop\"".to_string()),
]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_url_preservation(url in url_string()) {
let quoted = smart_quote(&url);
let extracted = if (quoted.starts_with('"') && quoted.ends_with('"'))
|| (quoted.starts_with('\'') && quoted.ends_with('\''))
{
quoted[1..quoted.len()-1].to_string()
} else {
quoted.clone()
};
prop_assert_eq!(
url.clone(), extracted.clone(),
"URL not preserved: original='{}', quoted='{}', extracted='{}'",
url, quoted, extracted
);
}
#[test]
fn prop_apostrophe_uses_double_quotes(s in apostrophe_string()) {
let quoted = smart_quote(&s);
prop_assert!(
quoted.starts_with('"') && quoted.ends_with('"'),
"String with apostrophe should use double quotes: '{}' -> '{}'",
s, quoted
);
let extracted = "ed[1..quoted.len()-1];
prop_assert_eq!(
s.clone(), extracted.to_string(),
"Apostrophe string not preserved: original='{}', extracted='{}'",
s, extracted
);
}
#[test]
fn prop_double_quote_uses_single_quotes(s in double_quote_string()) {
let quoted = smart_quote(&s);
prop_assert!(
quoted.starts_with('\'') && quoted.ends_with('\''),
"String with double quotes should use single quotes: '{}' -> '{}'",
s, quoted
);
let extracted = "ed[1..quoted.len()-1];
prop_assert_eq!(
s.clone(), extracted.to_string(),
"Double quote string not preserved: original='{}', extracted='{}'",
s, extracted
);
}
#[test]
fn prop_mixed_quotes_escapes_double(s in mixed_quote_string()) {
let quoted = smart_quote(&s);
prop_assert!(
quoted.starts_with('"') && quoted.ends_with('"'),
"Mixed quote string should use double quotes: '{}' -> '{}'",
s, quoted
);
let extracted = quoted[1..quoted.len()-1].replace("\\\"", "\"");
prop_assert_eq!(
s.clone(), extracted.clone(),
"Mixed quote string not preserved: original='{}', extracted='{}'",
s, extracted
);
}
#[test]
fn prop_simple_string_no_quotes(
s in prop::string::string_regex("[a-zA-Z][a-zA-Z0-9]{0,15}").unwrap()
) {
let quoted = smart_quote(&s);
prop_assert_eq!(
s.clone(), quoted.clone(),
"Simple string should not be quoted: '{}' -> '{}'",
s, quoted
);
}
#[test]
fn prop_string_with_spaces_quoted(
word1 in prop::string::string_regex("[a-z]+").unwrap(),
word2 in prop::string::string_regex("[a-z]+").unwrap()
) {
let s = format!("{} {}", word1, word2);
let quoted = smart_quote(&s);
prop_assert!(
(quoted.starts_with('"') && quoted.ends_with('"')) ||
(quoted.starts_with('\'') && quoted.ends_with('\'')),
"String with spaces should be quoted: '{}' -> '{}'",
s, quoted
);
}
#[test]
fn prop_special_chars_quoted(
prefix in prop::string::string_regex("[a-z]+").unwrap(),
suffix in prop::string::string_regex("[a-z]+").unwrap(),
special in prop::sample::select(vec!['#', '|', '^', ':'])
) {
let s = format!("{}{}{}", prefix, special, suffix);
let quoted = smart_quote(&s);
prop_assert!(
(quoted.starts_with('"') && quoted.ends_with('"')) ||
(quoted.starts_with('\'') && quoted.ends_with('\'')),
"String with special char '{}' should be quoted: '{}' -> '{}'",
special, s, quoted
);
}
}
}