1#[cfg(feature = "wasm")]
65use wasm_bindgen::prelude::*;
66
67use crate::error::{MAX_INPUT_SIZE, MAX_RECURSION_DEPTH, MAX_TABLE_ROWS};
68use crate::llm::{human_to_llm, llm_to_human};
69
70#[cfg(feature = "wasm")]
71use crate::llm::tokens::{ModelType, TokenCounter};
72
73#[cfg_attr(feature = "wasm", wasm_bindgen)]
75#[derive(Debug, Clone)]
76pub struct TransformResult {
77 success: bool,
78 content: String,
79 error: Option<String>,
80}
81
82#[cfg_attr(feature = "wasm", wasm_bindgen)]
83impl TransformResult {
84 #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
86 pub fn success(&self) -> bool {
87 self.success
88 }
89
90 #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
92 pub fn content(&self) -> String {
93 self.content.clone()
94 }
95
96 #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
98 pub fn error(&self) -> Option<String> {
99 self.error.clone()
100 }
101}
102
103impl TransformResult {
104 pub fn ok(content: String) -> Self {
106 Self {
107 success: true,
108 content,
109 error: None,
110 }
111 }
112
113 pub fn err(error: String) -> Self {
115 Self {
116 success: false,
117 content: String::new(),
118 error: Some(error),
119 }
120 }
121}
122
123#[cfg_attr(feature = "wasm", wasm_bindgen)]
125#[derive(Debug, Clone)]
126pub struct ValidationResult {
127 success: bool,
128 error: Option<String>,
129 line: Option<u32>,
130 column: Option<u32>,
131 hint: Option<String>,
132}
133
134#[cfg_attr(feature = "wasm", wasm_bindgen)]
135impl ValidationResult {
136 #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
138 pub fn success(&self) -> bool {
139 self.success
140 }
141
142 #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
144 pub fn error(&self) -> Option<String> {
145 self.error.clone()
146 }
147
148 #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
150 pub fn line(&self) -> Option<u32> {
151 self.line
152 }
153
154 #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
156 pub fn column(&self) -> Option<u32> {
157 self.column
158 }
159
160 #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
162 pub fn hint(&self) -> Option<String> {
163 self.hint.clone()
164 }
165}
166
167impl ValidationResult {
168 pub fn valid() -> Self {
170 Self {
171 success: true,
172 error: None,
173 line: None,
174 column: None,
175 hint: None,
176 }
177 }
178
179 pub fn invalid(error: String, line: u32, column: u32, hint: String) -> Self {
181 Self {
182 success: false,
183 error: Some(error),
184 line: Some(line),
185 column: Some(column),
186 hint: Some(hint),
187 }
188 }
189}
190
191#[cfg_attr(feature = "wasm", wasm_bindgen)]
193#[derive(Debug, Clone)]
194pub struct SerializerConfig {
195 indent_size: usize,
197 preserve_comments: bool,
199 smart_quoting: bool,
201}
202
203#[cfg_attr(feature = "wasm", wasm_bindgen)]
204impl SerializerConfig {
205 #[cfg_attr(feature = "wasm", wasm_bindgen(constructor))]
207 pub fn new() -> Self {
208 Self {
209 indent_size: 2,
210 preserve_comments: true,
211 smart_quoting: true,
212 }
213 }
214
215 #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = setIndentSize))]
217 pub fn set_indent_size(&mut self, size: usize) {
218 self.indent_size = if size == 4 { 4 } else { 2 };
219 }
220
221 #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = setPreserveComments))]
223 pub fn set_preserve_comments(&mut self, preserve: bool) {
224 self.preserve_comments = preserve;
225 }
226
227 #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = setSmartQuoting))]
229 pub fn set_smart_quoting(&mut self, smart: bool) {
230 self.smart_quoting = smart;
231 }
232}
233
234impl Default for SerializerConfig {
235 fn default() -> Self {
236 Self::new()
237 }
238}
239
240#[cfg_attr(feature = "wasm", wasm_bindgen)]
245pub struct DxSerializer {
246 #[allow(dead_code)]
252 config: SerializerConfig,
254}
255
256#[cfg_attr(feature = "wasm", wasm_bindgen)]
257impl DxSerializer {
258 #[cfg_attr(feature = "wasm", wasm_bindgen(constructor))]
260 pub fn new() -> Self {
261 let config = SerializerConfig::new();
262 Self { config }
263 }
264
265 #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = withConfig))]
267 pub fn with_config(config: SerializerConfig) -> Self {
268 Self { config }
269 }
270
271 #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = toHuman))]
277 pub fn to_human(&self, llm_input: &str) -> TransformResult {
278 if llm_input.trim().is_empty() {
280 return TransformResult::ok(String::new());
281 }
282
283 match llm_to_human(llm_input) {
284 Ok(human) => TransformResult::ok(human),
285 Err(e) => TransformResult::err(format!("Parse error: {}", e)),
286 }
287 }
288
289 #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = toDense))]
294 pub fn to_dense(&self, human_input: &str) -> TransformResult {
295 if human_input.trim().is_empty() {
297 return TransformResult::ok(String::new());
298 }
299
300 match human_to_llm(human_input) {
301 Ok(llm) => TransformResult::ok(llm),
302 Err(e) => TransformResult::err(format!("Parse error: {}", e)),
303 }
304 }
305
306 #[cfg_attr(feature = "wasm", wasm_bindgen)]
310 pub fn validate(&self, content: &str) -> ValidationResult {
311 let mut bracket_stack: Vec<(char, u32, u32)> = Vec::new();
313 let mut in_string = false;
314 let mut string_char = '"';
315 let mut string_start: Option<(u32, u32)> = None;
316
317 for (line_idx, line) in content.lines().enumerate() {
318 let line_num = (line_idx + 1) as u32;
319 let mut col = 0u32;
320 let mut chars = line.chars().peekable();
321
322 while let Some(ch) = chars.next() {
323 col += 1;
324
325 if in_string && ch == '\\' {
327 chars.next(); col += 1;
329 continue;
330 }
331
332 if !in_string && (ch == '"' || ch == '\'') {
334 in_string = true;
335 string_char = ch;
336 string_start = Some((line_num, col));
337 continue;
338 }
339
340 if in_string && ch == string_char {
341 in_string = false;
342 string_start = None;
343 continue;
344 }
345
346 if in_string {
348 continue;
349 }
350
351 match ch {
353 '{' | '[' | '(' => {
354 bracket_stack.push((ch, line_num, col));
355 }
356 '}' | ']' | ')' => {
357 let expected = match ch {
358 '}' => '{',
359 ']' => '[',
360 ')' => '(',
361 _ => unreachable!(),
362 };
363
364 if let Some((open_char, open_line, open_col)) = bracket_stack.pop() {
365 if open_char != expected {
366 return ValidationResult::invalid(
367 format!(
368 "Mismatched bracket: expected '{}' but found '{}'",
369 matching_close(open_char),
370 ch
371 ),
372 line_num,
373 col,
374 format!(
375 "Opening '{}' at line {}, column {} expects '{}'",
376 open_char,
377 open_line,
378 open_col,
379 matching_close(open_char)
380 ),
381 );
382 }
383 } else {
384 return ValidationResult::invalid(
385 format!("Unexpected closing bracket '{}'", ch),
386 line_num,
387 col,
388 format!("No matching opening bracket for '{}'", ch),
389 );
390 }
391 }
392 _ => {}
393 }
394 }
395 }
396
397 if in_string {
399 if let Some((line, col)) = string_start {
400 return ValidationResult::invalid(
401 format!("Unclosed string starting with '{}'", string_char),
402 line,
403 col,
404 format!("Add a closing '{}' to complete the string", string_char),
405 );
406 }
407 }
408
409 if let Some((ch, line, col)) = bracket_stack.pop() {
411 return ValidationResult::invalid(
412 format!("Unclosed bracket '{}'", ch),
413 line,
414 col,
415 format!(
416 "Add a closing '{}' to match the opening '{}'",
417 matching_close(ch),
418 ch
419 ),
420 );
421 }
422
423 ValidationResult::valid()
424 }
425
426 #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = isSaveable))]
430 pub fn is_saveable(&self, content: &str) -> bool {
431 self.validate(content).success
432 }
433
434 #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = maxInputSize))]
438 pub fn max_input_size(&self) -> usize {
439 MAX_INPUT_SIZE
440 }
441
442 #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = maxRecursionDepth))]
446 pub fn max_recursion_depth(&self) -> usize {
447 MAX_RECURSION_DEPTH
448 }
449
450 #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = maxTableRows))]
454 pub fn max_table_rows(&self) -> usize {
455 MAX_TABLE_ROWS
456 }
457}
458
459impl Default for DxSerializer {
460 fn default() -> Self {
461 Self::new()
462 }
463}
464
465fn matching_close(open: char) -> char {
467 match open {
468 '{' => '}',
469 '[' => ']',
470 '(' => ')',
471 _ => open,
472 }
473}
474
475pub fn smart_quote(value: &str) -> String {
480 let has_single = value.contains('\'');
481 let has_double = value.contains('"');
482
483 if !has_single && !has_double {
484 if !value.contains(' ')
486 && !value.contains('#')
487 && !value.contains('|')
488 && !value.contains('^')
489 && !value.contains(':')
490 {
491 return value.to_string();
492 }
493 return format!("\"{}\"", value);
495 }
496
497 if has_single && !has_double {
498 return format!("\"{}\"", value);
500 }
501
502 if has_double && !has_single {
503 return format!("'{}'", value);
505 }
506
507 let escaped = value.replace('"', "\\\"");
509 format!("\"{}\"", escaped)
510}
511
512#[cfg(feature = "wasm")]
514#[wasm_bindgen(start)]
515pub fn init_wasm() {
516 #[cfg(feature = "console_error_panic_hook")]
517 console_error_panic_hook::set_once();
518}
519
520#[cfg(feature = "wasm")]
522#[wasm_bindgen(js_name = "serializerVersion")]
523pub fn serializer_version() -> String {
524 format!(
525 "dx-serializer v{} ({})",
526 env!("CARGO_PKG_VERSION"),
527 if cfg!(debug_assertions) {
528 "debug"
529 } else {
530 "release"
531 }
532 )
533}
534
535#[cfg_attr(feature = "wasm", wasm_bindgen)]
541#[derive(Debug, Clone)]
542pub struct TokenCountResult {
543 count: usize,
544 model: String,
545}
546
547#[cfg_attr(feature = "wasm", wasm_bindgen)]
548impl TokenCountResult {
549 #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
551 pub fn count(&self) -> usize {
552 self.count
553 }
554
555 #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
557 pub fn model(&self) -> String {
558 self.model.clone()
559 }
560}
561
562#[cfg_attr(feature = "wasm", wasm_bindgen)]
564#[derive(Debug, Clone)]
565pub struct AllTokenCountsResult {
566 gpt4o: usize,
567 claude: usize,
568 gemini: usize,
569 other: usize,
570}
571
572#[cfg_attr(feature = "wasm", wasm_bindgen)]
573impl AllTokenCountsResult {
574 #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
576 pub fn gpt4o(&self) -> usize {
577 self.gpt4o
578 }
579
580 #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
582 pub fn claude(&self) -> usize {
583 self.claude
584 }
585
586 #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
588 pub fn gemini(&self) -> usize {
589 self.gemini
590 }
591
592 #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
594 pub fn other(&self) -> usize {
595 self.other
596 }
597}
598
599#[cfg(feature = "wasm")]
605#[wasm_bindgen]
606pub fn count_tokens(text: &str, model: &str) -> TokenCountResult {
607 let counter = TokenCounter::new();
608 let model_type = match model.to_lowercase().as_str() {
609 "gpt4o" | "gpt-4o" | "openai" => ModelType::Gpt4o,
610 "claude" | "sonnet" | "claude-sonnet" => ModelType::ClaudeSonnet4,
611 "gemini" | "gemini3" | "gemini-3" => ModelType::Gemini3,
612 _ => ModelType::Other,
613 };
614
615 let info = counter.count(text, model_type);
616 TokenCountResult {
617 count: info.count,
618 model: format!("{}", model_type),
619 }
620}
621
622#[cfg(feature = "wasm")]
627#[wasm_bindgen]
628pub fn count_tokens_all(text: &str) -> AllTokenCountsResult {
629 let counter = TokenCounter::new();
630 let counts = counter.count_primary_models(text);
631
632 AllTokenCountsResult {
633 gpt4o: counts.get(&ModelType::Gpt4o).map(|i| i.count).unwrap_or(0),
634 claude: counts
635 .get(&ModelType::ClaudeSonnet4)
636 .map(|i| i.count)
637 .unwrap_or(0),
638 gemini: counts
639 .get(&ModelType::Gemini3)
640 .map(|i| i.count)
641 .unwrap_or(0),
642 other: counts.get(&ModelType::Other).map(|i| i.count).unwrap_or(0),
643 }
644}
645
646#[cfg(feature = "wasm")]
655#[wasm_bindgen]
656pub fn dx_to_json(dx: &str) -> Result<String, JsValue> {
657 use crate::parser::parse;
658
659 let value =
660 parse(dx.as_bytes()).map_err(|e| JsValue::from_str(&format!("Parse error: {}", e)))?;
661 dx_value_to_json(&value).map_err(|e| JsValue::from_str(&e))
662}
663
664#[cfg(feature = "wasm")]
669#[wasm_bindgen]
670pub fn dx_to_yaml(dx: &str) -> Result<String, JsValue> {
671 use crate::parser::parse;
672
673 let value =
674 parse(dx.as_bytes()).map_err(|e| JsValue::from_str(&format!("Parse error: {}", e)))?;
675 dx_value_to_yaml(&value).map_err(|e| JsValue::from_str(&e))
676}
677
678#[cfg(feature = "wasm")]
683#[wasm_bindgen]
684pub fn dx_to_toml(dx: &str) -> Result<String, JsValue> {
685 use crate::parser::parse;
686
687 let value =
688 parse(dx.as_bytes()).map_err(|e| JsValue::from_str(&format!("Parse error: {}", e)))?;
689 dx_value_to_toml(&value).map_err(|e| JsValue::from_str(&e))
690}
691
692#[cfg(feature = "wasm")]
697#[wasm_bindgen]
698pub fn dx_to_toon_wasm(dx: &str) -> Result<String, JsValue> {
699 crate::converters::dx_to_toon(dx).map_err(|e| JsValue::from_str(&e))
700}
701
702#[cfg(feature = "wasm")]
707#[wasm_bindgen]
708pub fn llm_to_machine(llm: &str) -> Result<Vec<u8>, JsValue> {
709 use crate::llm::convert;
710
711 let machine = convert::llm_to_machine(llm)
712 .map_err(|e| JsValue::from_str(&format!("Failed to convert to machine format: {}", e)))?;
713
714 Ok(machine.data)
715}
716
717#[cfg(feature = "wasm")]
722#[wasm_bindgen]
723pub fn human_to_machine(human: &str) -> Result<Vec<u8>, JsValue> {
724 use crate::llm::convert;
725
726 let machine = convert::human_to_machine(human)
727 .map_err(|e| JsValue::from_str(&format!("Failed to convert to machine format: {}", e)))?;
728
729 Ok(machine.data)
730}
731
732#[cfg(any(feature = "wasm", feature = "converters", test))]
736use crate::types::DxValue;
737
738#[allow(dead_code)] #[cfg(any(feature = "wasm", feature = "converters", test))]
741fn dx_value_to_json(value: &DxValue) -> Result<String, String> {
742 let json_value = dx_value_to_serde_json(value)?;
743 serde_json::to_string_pretty(&json_value)
744 .map_err(|e| format!("JSON serialization error: {}", e))
745}
746
747#[allow(dead_code)] #[cfg(any(feature = "wasm", feature = "converters", test))]
750fn dx_value_to_serde_json(value: &DxValue) -> Result<serde_json::Value, String> {
751 match value {
752 DxValue::Null => Ok(serde_json::Value::Null),
753 DxValue::Bool(b) => Ok(serde_json::Value::Bool(*b)),
754 DxValue::Int(i) => Ok(serde_json::Value::Number(serde_json::Number::from(*i))),
755 DxValue::Float(f) => serde_json::Number::from_f64(*f)
756 .map(serde_json::Value::Number)
757 .ok_or_else(|| "Invalid float value".to_string()),
758 DxValue::String(s) => Ok(serde_json::Value::String(s.clone())),
759 DxValue::Array(arr) => {
760 let items: Result<Vec<_>, _> = arr.values.iter().map(dx_value_to_serde_json).collect();
761 Ok(serde_json::Value::Array(items?))
762 }
763 DxValue::Object(obj) => {
764 let mut map = serde_json::Map::new();
765 for (k, v) in obj.iter() {
766 map.insert(k.clone(), dx_value_to_serde_json(v)?);
767 }
768 Ok(serde_json::Value::Object(map))
769 }
770 DxValue::Table(table) => {
771 let mut rows = Vec::new();
773 for row in &table.rows {
774 let mut obj = serde_json::Map::new();
775 for (i, col) in table.schema.columns.iter().enumerate() {
776 if let Some(val) = row.get(i) {
777 obj.insert(col.name.clone(), dx_value_to_serde_json(val)?);
778 }
779 }
780 rows.push(serde_json::Value::Object(obj));
781 }
782 Ok(serde_json::Value::Array(rows))
783 }
784 DxValue::Ref(id) => Ok(serde_json::Value::String(format!("@{}", id))),
785 }
786}
787
788#[cfg(any(feature = "wasm", test))]
790fn dx_value_to_yaml(value: &DxValue) -> Result<String, String> {
791 let mut output = String::new();
792 dx_value_to_yaml_impl(value, &mut output, 0)?;
793 Ok(output)
794}
795
796#[cfg(any(feature = "wasm", test))]
797fn dx_value_to_yaml_impl(
798 value: &DxValue,
799 output: &mut String,
800 indent: usize,
801) -> Result<(), String> {
802 let indent_str = " ".repeat(indent);
803
804 match value {
805 DxValue::Null => output.push_str("null"),
806 DxValue::Bool(b) => output.push_str(if *b { "true" } else { "false" }),
807 DxValue::Int(i) => output.push_str(&i.to_string()),
808 DxValue::Float(f) => output.push_str(&f.to_string()),
809 DxValue::String(s) => {
810 if s.contains(':')
812 || s.contains('#')
813 || s.contains('\n')
814 || s.starts_with(' ')
815 || s.ends_with(' ')
816 {
817 output.push('"');
818 output.push_str(
819 &s.replace('\\', "\\\\")
820 .replace('"', "\\\"")
821 .replace('\n', "\\n"),
822 );
823 output.push('"');
824 } else {
825 output.push_str(s);
826 }
827 }
828 DxValue::Array(arr) => {
829 if arr.values.is_empty() {
830 output.push_str("[]");
831 } else {
832 for (i, item) in arr.values.iter().enumerate() {
833 if i > 0 {
834 output.push('\n');
835 output.push_str(&indent_str);
836 }
837 output.push_str("- ");
838 dx_value_to_yaml_impl(item, output, indent + 1)?;
839 }
840 }
841 }
842 DxValue::Object(obj) => {
843 for (i, (k, v)) in obj.iter().enumerate() {
844 if i > 0 {
845 output.push('\n');
846 output.push_str(&indent_str);
847 }
848 output.push_str(k);
849 output.push_str(": ");
850 if matches!(v, DxValue::Object(_) | DxValue::Array(_)) {
851 output.push('\n');
852 output.push_str(&" ".repeat(indent + 1));
853 }
854 dx_value_to_yaml_impl(v, output, indent + 1)?;
855 }
856 }
857 DxValue::Table(table) => {
858 for (i, row) in table.rows.iter().enumerate() {
860 if i > 0 {
861 output.push('\n');
862 output.push_str(&indent_str);
863 }
864 output.push_str("- ");
865 for (j, col) in table.schema.columns.iter().enumerate() {
866 if j > 0 {
867 output.push('\n');
868 output.push_str(&" ".repeat(indent + 1));
869 }
870 output.push_str(&col.name);
871 output.push_str(": ");
872 if let Some(val) = row.get(j) {
873 dx_value_to_yaml_impl(val, output, indent + 2)?;
874 }
875 }
876 }
877 }
878 DxValue::Ref(id) => {
879 output.push_str(&format!("\"@{}\"", id));
880 }
881 }
882 Ok(())
883}
884
885#[cfg(any(feature = "wasm", test))]
887fn dx_value_to_toml(value: &DxValue) -> Result<String, String> {
888 let mut output = String::new();
889
890 match value {
891 DxValue::Object(obj) => {
892 for (k, v) in obj.iter() {
894 if !matches!(
895 v,
896 DxValue::Object(_) | DxValue::Array(_) | DxValue::Table(_)
897 ) {
898 output.push_str(k);
899 output.push_str(" = ");
900 dx_value_to_toml_value(v, &mut output)?;
901 output.push('\n');
902 }
903 }
904
905 for (k, v) in obj.iter() {
907 if let DxValue::Object(nested) = v {
908 output.push('\n');
909 output.push('[');
910 output.push_str(k);
911 output.push_str("]\n");
912 for (nk, nv) in nested.iter() {
913 output.push_str(nk);
914 output.push_str(" = ");
915 dx_value_to_toml_value(nv, &mut output)?;
916 output.push('\n');
917 }
918 }
919 }
920
921 for (k, v) in obj.iter() {
923 if let DxValue::Array(arr) = v {
924 output.push_str(k);
925 output.push_str(" = [");
926 for (i, item) in arr.values.iter().enumerate() {
927 if i > 0 {
928 output.push_str(", ");
929 }
930 dx_value_to_toml_value(item, &mut output)?;
931 }
932 output.push_str("]\n");
933 }
934 }
935 }
936 _ => {
937 return Err("TOML root must be an object".to_string());
938 }
939 }
940
941 Ok(output)
942}
943
944#[cfg(any(feature = "wasm", test))]
945fn dx_value_to_toml_value(value: &DxValue, output: &mut String) -> Result<(), String> {
946 match value {
947 DxValue::Null => output.push_str("\"\""),
948 DxValue::Bool(b) => output.push_str(if *b { "true" } else { "false" }),
949 DxValue::Int(i) => output.push_str(&i.to_string()),
950 DxValue::Float(f) => output.push_str(&f.to_string()),
951 DxValue::String(s) => {
952 output.push('"');
953 output.push_str(&s.replace('\\', "\\\\").replace('"', "\\\""));
954 output.push('"');
955 }
956 DxValue::Array(arr) => {
957 output.push('[');
958 for (i, item) in arr.values.iter().enumerate() {
959 if i > 0 {
960 output.push_str(", ");
961 }
962 dx_value_to_toml_value(item, output)?;
963 }
964 output.push(']');
965 }
966 DxValue::Object(_) => output.push_str("{}"),
967 DxValue::Table(_) => output.push_str("[[]]"),
968 DxValue::Ref(id) => {
969 output.push('"');
970 output.push('@');
971 output.push_str(&id.to_string());
972 output.push('"');
973 }
974 }
975 Ok(())
976}
977
978#[cfg(test)]
979mod tests {
980 use super::*;
981
982 #[test]
983 fn test_transform_result() {
984 let ok = TransformResult::ok("content".to_string());
985 assert!(ok.success());
986 assert_eq!(ok.content(), "content");
987 assert!(ok.error().is_none());
988
989 let err = TransformResult::err("error".to_string());
990 assert!(!err.success());
991 assert!(err.content().is_empty());
992 assert_eq!(err.error(), Some("error".to_string()));
993 }
994
995 #[test]
996 fn test_validation_result() {
997 let valid = ValidationResult::valid();
998 assert!(valid.success());
999 assert!(valid.error().is_none());
1000
1001 let invalid = ValidationResult::invalid("error".to_string(), 1, 5, "hint".to_string());
1002 assert!(!invalid.success());
1003 assert_eq!(invalid.error(), Some("error".to_string()));
1004 assert_eq!(invalid.line(), Some(1));
1005 assert_eq!(invalid.column(), Some(5));
1006 assert_eq!(invalid.hint(), Some("hint".to_string()));
1007 }
1008
1009 #[test]
1010 fn test_serializer_to_human() {
1011 let serializer = DxSerializer::new();
1012 let result = serializer.to_human("host|localhost\nport|5432");
1014 assert!(result.success(), "to_human failed: {:?}", result.error());
1015 assert!(result.content().contains("host") || result.content().contains("localhost"));
1016 }
1017
1018 #[test]
1019 fn test_serializer_to_dense() {
1020 let serializer = DxSerializer::new();
1021 let human = serializer.to_human("debug|+\nprod|-");
1023 assert!(human.success(), "to_human failed: {:?}", human.error());
1024
1025 let dense = serializer.to_dense(&human.content());
1027 assert!(dense.success(), "to_dense failed: {:?}", dense.error());
1028 assert!(dense.content().contains("|") || dense.content().contains("debug"));
1030 }
1031
1032 #[test]
1033 fn test_validate_valid_content() {
1034 let serializer = DxSerializer::new();
1035 let result = serializer.validate("key: value\nother: data");
1036 assert!(result.success());
1037 }
1038
1039 #[test]
1040 fn test_validate_unclosed_bracket() {
1041 let serializer = DxSerializer::new();
1042 let result = serializer.validate("data: {\n key: value");
1043 assert!(!result.success());
1044 assert!(result.error().unwrap().contains("Unclosed bracket"));
1045 assert_eq!(result.line(), Some(1));
1046 assert!(result.hint().is_some());
1047 }
1048
1049 #[test]
1050 fn test_validate_unclosed_string() {
1051 let serializer = DxSerializer::new();
1052 let result = serializer.validate("key: \"unclosed string");
1053 assert!(!result.success());
1054 assert!(result.error().unwrap().contains("Unclosed string"));
1055 assert!(result.hint().is_some());
1056 }
1057
1058 #[test]
1059 fn test_validate_mismatched_brackets() {
1060 let serializer = DxSerializer::new();
1061 let result = serializer.validate("data: [value}");
1062 assert!(!result.success());
1063 assert!(result.error().unwrap().contains("Mismatched bracket"));
1064 }
1065
1066 #[test]
1067 fn test_is_saveable() {
1068 let serializer = DxSerializer::new();
1069 assert!(serializer.is_saveable("key: value"));
1070 assert!(!serializer.is_saveable("key: {unclosed"));
1071 assert!(!serializer.is_saveable("key: \"unclosed"));
1072 }
1073
1074 #[test]
1075 fn test_smart_quote_simple() {
1076 assert_eq!(smart_quote("hello"), "hello");
1077 assert_eq!(smart_quote("hello world"), "\"hello world\"");
1078 }
1079
1080 #[test]
1081 fn test_smart_quote_apostrophe() {
1082 assert_eq!(smart_quote("don't"), "\"don't\"");
1084 assert_eq!(smart_quote("it's working"), "\"it's working\"");
1085 }
1086
1087 #[test]
1088 fn test_smart_quote_double_quotes() {
1089 assert_eq!(smart_quote("say \"hello\""), "'say \"hello\"'");
1091 }
1092
1093 #[test]
1094 fn test_smart_quote_both() {
1095 assert_eq!(
1097 smart_quote("don't say \"hello\""),
1098 "\"don't say \\\"hello\\\"\""
1099 );
1100 }
1101
1102 #[test]
1103 fn test_smart_quote_special_chars() {
1104 assert_eq!(smart_quote("key:value"), "\"key:value\"");
1105 assert_eq!(smart_quote("a|b|c"), "\"a|b|c\"");
1106 assert_eq!(smart_quote("a#b"), "\"a#b\"");
1107 }
1108
1109 #[test]
1110 fn test_config() {
1111 let mut config = SerializerConfig::new();
1112 assert_eq!(config.indent_size, 2);
1113
1114 config.set_indent_size(4);
1115 assert_eq!(config.indent_size, 4);
1116
1117 config.set_indent_size(3); assert_eq!(config.indent_size, 2);
1119 }
1120
1121 #[test]
1122 fn test_empty_input() {
1123 let serializer = DxSerializer::new();
1124
1125 let human = serializer.to_human("");
1126 assert!(human.success());
1127 assert!(human.content().is_empty());
1128
1129 let dense = serializer.to_dense("");
1130 assert!(dense.success());
1131 assert!(dense.content().is_empty());
1132 }
1133
1134 #[test]
1136 fn test_dx_to_json() {
1137 let dx = "name:test\nversion:100";
1138 let result = dx_value_to_json(&crate::parser::parse(dx.as_bytes()).unwrap());
1139 assert!(result.is_ok());
1140 let json = result.unwrap();
1141 assert!(json.contains("name"));
1142 assert!(json.contains("test"));
1143 assert!(json.contains("version"));
1144 assert!(json.contains("100"));
1145 }
1146
1147 #[test]
1148 fn test_dx_to_yaml() {
1149 let dx = "name:test\nversion:100";
1150 let result = dx_value_to_yaml(&crate::parser::parse(dx.as_bytes()).unwrap());
1151 assert!(result.is_ok());
1152 let yaml = result.unwrap();
1153 assert!(yaml.contains("name"));
1154 assert!(yaml.contains("test"));
1155 }
1156
1157 #[test]
1158 fn test_dx_to_toml() {
1159 let dx = "name:test\nversion:100";
1160 let result = dx_value_to_toml(&crate::parser::parse(dx.as_bytes()).unwrap());
1161 assert!(result.is_ok());
1162 let toml = result.unwrap();
1163 assert!(toml.contains("name"));
1164 assert!(toml.contains("test"));
1165 }
1166}
1167
1168#[cfg(test)]
1169mod property_tests {
1170 use super::*;
1171 use proptest::prelude::*;
1172
1173 fn valid_abbrev_key() -> impl Strategy<Value = String> {
1177 prop::string::string_regex("[a-z]{2,3}")
1178 .unwrap()
1179 .prop_filter("non-empty key", |s| !s.is_empty())
1180 }
1181
1182 fn simple_value() -> impl Strategy<Value = String> {
1184 prop_oneof![
1185 prop::string::string_regex("[a-zA-Z][a-zA-Z0-9]{0,15}").unwrap(),
1187 (1i32..10000).prop_map(|n| n.to_string()),
1189 ]
1190 }
1191
1192 #[allow(dead_code)] fn _llm_bool() -> impl Strategy<Value = String> {
1196 prop::bool::ANY.prop_map(|b| if b { "+".to_string() } else { "-".to_string() })
1197 }
1198
1199 #[allow(dead_code)] fn _llm_context_section() -> impl Strategy<Value = String> {
1203 prop::collection::vec((valid_abbrev_key(), simple_value()), 1..4).prop_map(|pairs| {
1204 pairs
1205 .into_iter()
1206 .map(|(k, v)| format!("{}|{}", k, v))
1207 .collect::<Vec<_>>()
1208 .join("\n")
1209 })
1210 }
1211
1212 #[allow(dead_code)] fn _llm_data_section() -> impl Strategy<Value = String> {
1216 (
1217 prop::string::string_regex("[a-z]").unwrap(), prop::collection::vec(valid_abbrev_key(), 2..4), prop::collection::vec(simple_value(), 2..4), )
1221 .prop_filter("schema and row same length", |(_, schema, row)| {
1222 schema.len() == row.len()
1223 })
1224 .prop_map(|(id, schema, row)| {
1225 let schema_str = schema.join("|");
1226 let row_str = row.join("|");
1227 format!("#{}({})\n{}", id, schema_str, row_str)
1228 })
1229 }
1230
1231 #[allow(dead_code)] fn _valid_llm_content() -> impl Strategy<Value = String> {
1235 prop_oneof![_llm_context_section(), _llm_data_section(),]
1236 }
1237
1238 proptest! {
1243 #![proptest_config(ProptestConfig::with_cases(100))]
1244
1245 #[test]
1246 fn prop_llm_round_trip_context(
1247 pairs in prop::collection::vec(
1248 (valid_abbrev_key(), simple_value()),
1249 1..3,
1250 )
1251 ) {
1252 let keys: Vec<_> = pairs.iter().map(|(k, _)| k.clone()).collect();
1254 let unique_keys: std::collections::HashSet<_> = keys.iter().collect();
1255 prop_assume!(keys.len() == unique_keys.len());
1256
1257 let serializer = DxSerializer::new();
1258 let llm: String = pairs
1260 .iter()
1261 .map(|(k, v)| format!("{}|{}", k, v))
1262 .collect::<Vec<_>>()
1263 .join("\n");
1264
1265 let human_result = serializer.to_human(&llm);
1267 prop_assert!(human_result.success(), "to_human failed: {:?}", human_result.error());
1268
1269 let llm_result = serializer.to_dense(&human_result.content());
1271 prop_assert!(llm_result.success(), "to_dense failed: {:?}", llm_result.error());
1272
1273 let result = llm_result.content();
1275 for (_, value) in &pairs {
1276 prop_assert!(
1277 result.contains(value),
1278 "Value '{}' not found in result: '{}'", value, result
1279 );
1280 }
1281 }
1282
1283 #[test]
1284 fn prop_llm_round_trip_booleans(
1285 key1 in valid_abbrev_key(),
1286 key2 in valid_abbrev_key(),
1287 bool1 in prop::bool::ANY,
1288 bool2 in prop::bool::ANY
1289 ) {
1290 prop_assume!(key1 != key2);
1291
1292 let serializer = DxSerializer::new();
1293 let b1 = if bool1 { "true" } else { "false" };
1294 let b2 = if bool2 { "true" } else { "false" };
1295 let llm = format!("{}={}\n{}={}", key1, b1, key2, b2);
1297
1298 let human_result = serializer.to_human(&llm);
1300 prop_assert!(human_result.success(), "to_human failed: {:?}", human_result.error());
1301
1302 let human = human_result.content();
1304 if bool1 {
1305 prop_assert!(human.contains("true"),
1306 "Boolean true not found in human format: '{}'", human);
1307 } else {
1308 prop_assert!(human.contains("false"),
1309 "Boolean false not found in human format: '{}'", human);
1310 }
1311
1312 let llm_result = serializer.to_dense(&human);
1314 prop_assert!(llm_result.success(), "to_dense failed: {:?}", llm_result.error());
1315
1316 let result = llm_result.content();
1318 prop_assert!(
1319 result.contains("true") || result.contains("false"),
1320 "Boolean values not found in LLM result: '{}'", result
1321 );
1322 }
1323
1324 #[test]
1325 fn prop_empty_content_round_trip(content in "\\s*") {
1326 let serializer = DxSerializer::new();
1327
1328 let human_result = serializer.to_human(&content);
1329 prop_assert!(human_result.success());
1330
1331 let dense_result = serializer.to_dense(&human_result.content());
1332 prop_assert!(dense_result.success());
1333 }
1334 }
1335}
1336
1337#[cfg(test)]
1338mod string_preservation_tests {
1339 use super::*;
1340 use proptest::prelude::*;
1341
1342 fn url_string() -> impl Strategy<Value = String> {
1350 (
1351 prop::string::string_regex("https?://[a-z]+\\.[a-z]{2,4}").unwrap(),
1352 prop::string::string_regex("/[a-z]+").unwrap(),
1353 prop::collection::vec(
1354 (
1355 prop::string::string_regex("[a-z]+").unwrap(),
1356 prop::string::string_regex("[a-zA-Z0-9]+").unwrap(),
1357 ),
1358 0..3,
1359 ),
1360 )
1361 .prop_map(|(base, path, params)| {
1362 if params.is_empty() {
1363 format!("{}{}", base, path)
1364 } else {
1365 let query: String = params
1366 .into_iter()
1367 .map(|(k, v)| format!("{}={}", k, v))
1368 .collect::<Vec<_>>()
1369 .join("&");
1370 format!("{}{}?{}", base, path, query)
1371 }
1372 })
1373 }
1374
1375 fn apostrophe_string() -> impl Strategy<Value = String> {
1377 prop_oneof![
1378 Just("don't".to_string()),
1379 Just("it's".to_string()),
1380 Just("won't".to_string()),
1381 Just("can't".to_string()),
1382 Just("I'm".to_string()),
1383 prop::string::string_regex("[A-Z][a-z]+'s [a-z]+").unwrap(),
1384 ]
1385 }
1386
1387 fn double_quote_string() -> impl Strategy<Value = String> {
1389 prop_oneof![
1390 Just("say \"hello\"".to_string()),
1391 Just("the \"best\" way".to_string()),
1392 prop::string::string_regex("[a-z]+ \"[a-z]+\" [a-z]+").unwrap(),
1393 ]
1394 }
1395
1396 fn mixed_quote_string() -> impl Strategy<Value = String> {
1398 prop_oneof![
1399 Just("don't say \"hello\"".to_string()),
1400 Just("it's \"great\"".to_string()),
1401 Just("can't \"stop\"".to_string()),
1402 ]
1403 }
1404
1405 proptest! {
1406 #![proptest_config(ProptestConfig::with_cases(100))]
1407
1408 #[test]
1409 fn prop_url_preservation(url in url_string()) {
1410 let quoted = smart_quote(&url);
1412
1413 let extracted = if (quoted.starts_with('"') && quoted.ends_with('"'))
1415 || (quoted.starts_with('\'') && quoted.ends_with('\''))
1416 {
1417 quoted[1..quoted.len()-1].to_string()
1418 } else {
1419 quoted.clone()
1420 };
1421
1422 prop_assert_eq!(
1423 url.clone(), extracted.clone(),
1424 "URL not preserved: original='{}', quoted='{}', extracted='{}'",
1425 url, quoted, extracted
1426 );
1427 }
1428
1429 #[test]
1430 fn prop_apostrophe_uses_double_quotes(s in apostrophe_string()) {
1431 let quoted = smart_quote(&s);
1432
1433 prop_assert!(
1435 quoted.starts_with('"') && quoted.ends_with('"'),
1436 "String with apostrophe should use double quotes: '{}' -> '{}'",
1437 s, quoted
1438 );
1439
1440 let extracted = "ed[1..quoted.len()-1];
1442 prop_assert_eq!(
1443 s.clone(), extracted.to_string(),
1444 "Apostrophe string not preserved: original='{}', extracted='{}'",
1445 s, extracted
1446 );
1447 }
1448
1449 #[test]
1450 fn prop_double_quote_uses_single_quotes(s in double_quote_string()) {
1451 let quoted = smart_quote(&s);
1452
1453 prop_assert!(
1455 quoted.starts_with('\'') && quoted.ends_with('\''),
1456 "String with double quotes should use single quotes: '{}' -> '{}'",
1457 s, quoted
1458 );
1459
1460 let extracted = "ed[1..quoted.len()-1];
1462 prop_assert_eq!(
1463 s.clone(), extracted.to_string(),
1464 "Double quote string not preserved: original='{}', extracted='{}'",
1465 s, extracted
1466 );
1467 }
1468
1469 #[test]
1470 fn prop_mixed_quotes_escapes_double(s in mixed_quote_string()) {
1471 let quoted = smart_quote(&s);
1472
1473 prop_assert!(
1475 quoted.starts_with('"') && quoted.ends_with('"'),
1476 "Mixed quote string should use double quotes: '{}' -> '{}'",
1477 s, quoted
1478 );
1479
1480 let extracted = quoted[1..quoted.len()-1].replace("\\\"", "\"");
1482 prop_assert_eq!(
1483 s.clone(), extracted.clone(),
1484 "Mixed quote string not preserved: original='{}', extracted='{}'",
1485 s, extracted
1486 );
1487 }
1488
1489 #[test]
1490 fn prop_simple_string_no_quotes(
1491 s in prop::string::string_regex("[a-zA-Z][a-zA-Z0-9]{0,15}").unwrap()
1492 ) {
1493 let quoted = smart_quote(&s);
1494
1495 prop_assert_eq!(
1497 s.clone(), quoted.clone(),
1498 "Simple string should not be quoted: '{}' -> '{}'",
1499 s, quoted
1500 );
1501 }
1502
1503 #[test]
1504 fn prop_string_with_spaces_quoted(
1505 word1 in prop::string::string_regex("[a-z]+").unwrap(),
1506 word2 in prop::string::string_regex("[a-z]+").unwrap()
1507 ) {
1508 let s = format!("{} {}", word1, word2);
1509 let quoted = smart_quote(&s);
1510
1511 prop_assert!(
1513 (quoted.starts_with('"') && quoted.ends_with('"')) ||
1514 (quoted.starts_with('\'') && quoted.ends_with('\'')),
1515 "String with spaces should be quoted: '{}' -> '{}'",
1516 s, quoted
1517 );
1518 }
1519
1520 #[test]
1521 fn prop_special_chars_quoted(
1522 prefix in prop::string::string_regex("[a-z]+").unwrap(),
1523 suffix in prop::string::string_regex("[a-z]+").unwrap(),
1524 special in prop::sample::select(vec!['#', '|', '^', ':'])
1525 ) {
1526 let s = format!("{}{}{}", prefix, special, suffix);
1527 let quoted = smart_quote(&s);
1528
1529 prop_assert!(
1531 (quoted.starts_with('"') && quoted.ends_with('"')) ||
1532 (quoted.starts_with('\'') && quoted.ends_with('\'')),
1533 "String with special char '{}' should be quoted: '{}' -> '{}'",
1534 special, s, quoted
1535 );
1536 }
1537 }
1538}