Skip to main content

serializer/
wasm.rs

1//! WASM Bindings for DX Serializer VS Code Extension
2//!
3//! Provides the DxSerializer interface for the VS Code extension with:
4//! - `to_human`: Transform LLM format to human-readable format
5//! - `to_dense`: Transform human-readable format to LLM format
6//! - `validate`: Validate content syntax with detailed error info
7//! - `is_saveable`: Check if content is complete enough to save
8//! - Security limits: max_input_size, max_recursion_depth, max_table_rows
9//! - Token counting: count_tokens, count_tokens_all
10//!
11//! ## LLM Format (on disk)
12//!
13//! ```dsr
14//! config[name=dx,version=0.0.1,title="Enhanced Developing Experience"]
15//! workspace[paths=@/www,@/backend]
16//! editors[items=neovim,zed,vscode,default=neovim]
17//! ```
18//!
19//! ## Human Format (in editor)
20//!
21//! ```dx
22//! name                = dx
23//! version             = 0.0.1
24//! title               = "Enhanced Developing Experience"
25//!
26//! [workspace]
27//! paths:
28//! - @/www
29//! - @/backend
30//!
31//! [editors]
32//! items:
33//! - neovim
34//! - zed
35//! - vscode
36//! default             = neovim
37//! ```
38//!
39//! ## Usage from JavaScript
40//!
41//! ```javascript
42//! import init, { DxSerializer } from 'dx_serializer';
43//!
44//! await init();
45//!
46//! const serializer = new DxSerializer();
47//!
48//! // Transform LLM to human (for editor display)
49//! const result = serializer.toHuman('config[name=dx,version=0.0.1]');
50//! if (result.success) {
51//!     console.log(result.content);
52//! }
53//!
54//! // Transform human to LLM (for disk storage)
55//! const llmResult = serializer.toDense(humanContent);
56//!
57//! // Validate content
58//! const validation = serializer.validate(content);
59//! if (!validation.success) {
60//!     console.log(`Error at line ${validation.line}: ${validation.error}`);
61//! }
62//! ```
63
64#[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/// Result of a transformation operation
74#[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    /// Whether the transformation succeeded
85    #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
86    pub fn success(&self) -> bool {
87        self.success
88    }
89
90    /// The transformed content (empty if failed)
91    #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
92    pub fn content(&self) -> String {
93        self.content.clone()
94    }
95
96    /// Error message if transformation failed
97    #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
98    pub fn error(&self) -> Option<String> {
99        self.error.clone()
100    }
101}
102
103impl TransformResult {
104    /// Create a successful result
105    pub fn ok(content: String) -> Self {
106        Self {
107            success: true,
108            content,
109            error: None,
110        }
111    }
112
113    /// Create a failed result
114    pub fn err(error: String) -> Self {
115        Self {
116            success: false,
117            content: String::new(),
118            error: Some(error),
119        }
120    }
121}
122
123/// Result of a validation operation
124#[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    /// Whether the content is valid
137    #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
138    pub fn success(&self) -> bool {
139        self.success
140    }
141
142    /// Error message if validation failed
143    #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
144    pub fn error(&self) -> Option<String> {
145        self.error.clone()
146    }
147
148    /// Line number where error occurred (1-indexed)
149    #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
150    pub fn line(&self) -> Option<u32> {
151        self.line
152    }
153
154    /// Column number where error occurred (1-indexed)
155    #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
156    pub fn column(&self) -> Option<u32> {
157        self.column
158    }
159
160    /// Actionable hint for fixing the error
161    #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
162    pub fn hint(&self) -> Option<String> {
163        self.hint.clone()
164    }
165}
166
167impl ValidationResult {
168    /// Create a successful validation result
169    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    /// Create a failed validation result
180    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/// Serializer configuration for the VS Code extension
192#[cfg_attr(feature = "wasm", wasm_bindgen)]
193#[derive(Debug, Clone)]
194pub struct SerializerConfig {
195    /// Indentation size (2 or 4 spaces)
196    indent_size: usize,
197    /// Whether to preserve comments
198    preserve_comments: bool,
199    /// Whether to use smart quoting for special characters
200    smart_quoting: bool,
201}
202
203#[cfg_attr(feature = "wasm", wasm_bindgen)]
204impl SerializerConfig {
205    /// Create a new configuration with defaults
206    #[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    /// Set the indent size (2 or 4)
216    #[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    /// Set whether to preserve comments
222    #[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    /// Set whether to use smart quoting
228    #[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/// DX Serializer for VS Code extension
241///
242/// Provides transformation between LLM (disk) and Human (editor) formats
243/// with validation support. Uses the llm module for format conversion.
244#[cfg_attr(feature = "wasm", wasm_bindgen)]
245pub struct DxSerializer {
246    /// Configuration for serialization behavior.
247    ///
248    /// Reserved for future use: will enable custom formatting rules,
249    /// validation strictness levels, and output style preferences.
250    /// Currently stored but not actively used pending feature implementation.
251    #[allow(dead_code)]
252    // Reserved for future configuration features (custom formatting, validation rules)
253    config: SerializerConfig,
254}
255
256#[cfg_attr(feature = "wasm", wasm_bindgen)]
257impl DxSerializer {
258    /// Create a new DxSerializer with default configuration
259    #[cfg_attr(feature = "wasm", wasm_bindgen(constructor))]
260    pub fn new() -> Self {
261        let config = SerializerConfig::new();
262        Self { config }
263    }
264
265    /// Create a DxSerializer with custom configuration
266    #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = withConfig))]
267    pub fn with_config(config: SerializerConfig) -> Self {
268        Self { config }
269    }
270
271    /// Transform LLM format to human-readable format
272    ///
273    /// This is called when opening a .dx file in the editor.
274    /// Converts sigil-based LLM format (`#c`, `#:`, `#<letter>`) to beautiful
275    /// human-readable format with Unicode tables and expanded keys.
276    #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = toHuman))]
277    pub fn to_human(&self, llm_input: &str) -> TransformResult {
278        // Handle empty input
279        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    /// Transform human-readable format to LLM format
290    ///
291    /// This is called when saving a .dx file in the editor.
292    /// Converts human-readable format back to token-optimized LLM format.
293    #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = toDense))]
294    pub fn to_dense(&self, human_input: &str) -> TransformResult {
295        // Handle empty input
296        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    /// Validate content syntax
307    ///
308    /// Returns detailed error information including line, column, and hints.
309    #[cfg_attr(feature = "wasm", wasm_bindgen)]
310    pub fn validate(&self, content: &str) -> ValidationResult {
311        // Track bracket/quote state for validation
312        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                // Handle escape sequences in strings
326                if in_string && ch == '\\' {
327                    chars.next(); // Skip escaped character
328                    col += 1;
329                    continue;
330                }
331
332                // Handle string boundaries
333                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                // Skip bracket checking inside strings
347                if in_string {
348                    continue;
349                }
350
351                // Track brackets
352                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        // Check for unclosed strings
398        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        // Check for unclosed brackets
410        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    /// Check if content is complete enough to save
427    ///
428    /// Returns true if the content has no unclosed brackets or strings.
429    #[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    /// Get the maximum input size limit (100 MB)
435    ///
436    /// Files larger than this will be rejected to prevent memory exhaustion.
437    #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = maxInputSize))]
438    pub fn max_input_size(&self) -> usize {
439        MAX_INPUT_SIZE
440    }
441
442    /// Get the maximum recursion depth limit (1000 levels)
443    ///
444    /// Structures nested deeper than this will be rejected to prevent stack overflow.
445    #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = maxRecursionDepth))]
446    pub fn max_recursion_depth(&self) -> usize {
447        MAX_RECURSION_DEPTH
448    }
449
450    /// Get the maximum table rows limit (10 million rows)
451    ///
452    /// Tables with more rows than this will be rejected to prevent memory exhaustion.
453    #[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
465/// Get the matching closing bracket for an opening bracket
466fn matching_close(open: char) -> char {
467    match open {
468        '{' => '}',
469        '[' => ']',
470        '(' => ')',
471        _ => open,
472    }
473}
474
475/// Apply smart quoting to a string value
476///
477/// - If string contains apostrophe ('), wrap in double quotes
478/// - If string contains both ' and ", use double quotes with escaped "
479pub 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        // No quotes needed for simple strings without spaces/special chars
485        if !value.contains(' ')
486            && !value.contains('#')
487            && !value.contains('|')
488            && !value.contains('^')
489            && !value.contains(':')
490        {
491            return value.to_string();
492        }
493        // Default to double quotes
494        return format!("\"{}\"", value);
495    }
496
497    if has_single && !has_double {
498        // Contains apostrophe - use double quotes
499        return format!("\"{}\"", value);
500    }
501
502    if has_double && !has_single {
503        // Contains double quotes - use single quotes
504        return format!("'{}'", value);
505    }
506
507    // Contains both - use double quotes with escaped double quotes
508    let escaped = value.replace('"', "\\\"");
509    format!("\"{}\"", escaped)
510}
511
512/// Initialize WASM module
513#[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/// Get version information
521#[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// ============================================================================
536// TOKEN COUNTING WASM EXPORTS
537// ============================================================================
538
539/// Result of token counting for a single model
540#[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    /// Get the token count
550    #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
551    pub fn count(&self) -> usize {
552        self.count
553    }
554
555    /// Get the model name
556    #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
557    pub fn model(&self) -> String {
558        self.model.clone()
559    }
560}
561
562/// Result of token counting for all primary models
563#[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    /// Get GPT-4o token count
575    #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
576    pub fn gpt4o(&self) -> usize {
577        self.gpt4o
578    }
579
580    /// Get Claude Sonnet 4 token count
581    #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
582    pub fn claude(&self) -> usize {
583        self.claude
584    }
585
586    /// Get Gemini 3 token count
587    #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
588    pub fn gemini(&self) -> usize {
589        self.gemini
590    }
591
592    /// Get Other model token count
593    #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
594    pub fn other(&self) -> usize {
595        self.other
596    }
597}
598
599/// Count tokens for a specific model
600///
601/// @param text - The text to tokenize
602/// @param model - The model name: "gpt4o", "claude", "gemini", or "other"
603/// @returns TokenCountResult with count and model name
604#[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/// Count tokens for all primary models (GPT-4o, Claude, Gemini, Other)
623///
624/// @param text - The text to tokenize
625/// @returns AllTokenCountsResult with counts for all 4 models
626#[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// ============================================================================
647// FORMAT CONVERSION WASM EXPORTS
648// ============================================================================
649
650/// Convert DX format to JSON
651///
652/// @param dx - DX format string
653/// @returns JSON string or error
654#[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/// Convert DX format to YAML
665///
666/// @param dx - DX format string
667/// @returns YAML string or error
668#[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/// Convert DX format to TOML
679///
680/// @param dx - DX format string
681/// @returns TOML string or error
682#[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/// Convert DX format to TOON
693///
694/// @param dx - DX format string
695/// @returns TOON string or error
696#[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/// Convert LLM format to Machine format (binary)
703///
704/// @param llm - LLM format string
705/// @returns Uint8Array with binary machine format
706#[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/// Convert Human format to Machine format (binary)
718///
719/// @param human - Human format string
720/// @returns Uint8Array with binary machine format
721#[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// Helper functions for format conversion (non-WASM)
733// Reserved for future public API to convert DxValue to various formats
734
735#[cfg(any(feature = "wasm", feature = "converters", test))]
736use crate::types::DxValue;
737
738/// Convert DxValue to JSON string
739#[allow(dead_code)] // Used by WASM and converter features
740#[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/// Convert DxValue to serde_json::Value
748#[allow(dead_code)] // Used by dx_value_to_json
749#[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            // Convert table to array of objects
772            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/// Convert DxValue to YAML string
789#[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            // Quote strings that need it
811            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            // Convert table to array of objects in YAML
859            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/// Convert DxValue to TOML string
886#[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            // First pass: simple key-value pairs
893            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            // Second pass: nested objects as sections
906            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            // Third pass: arrays
922            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        // Use LLM format: root-level key|value pairs
1013        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        // Use LLM format for input
1022        let human = serializer.to_human("debug|+\nprod|-");
1023        assert!(human.success(), "to_human failed: {:?}", human.error());
1024
1025        // Then deflate back
1026        let dense = serializer.to_dense(&human.content());
1027        assert!(dense.success(), "to_dense failed: {:?}", dense.error());
1028        // The result should contain the LLM format (key|value pairs)
1029        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        // Strings with apostrophes should use double quotes
1083        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        // Strings with double quotes should use single quotes
1090        assert_eq!(smart_quote("say \"hello\""), "'say \"hello\"'");
1091    }
1092
1093    #[test]
1094    fn test_smart_quote_both() {
1095        // Strings with both should escape double quotes
1096        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); // Invalid, should default to 2
1118        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    // Token counting tests
1135    #[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    // Generators for valid LLM format content
1174
1175    /// Generate a valid abbreviated key (2-3 lowercase letters)
1176    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    /// Generate a simple value for LLM format
1183    fn simple_value() -> impl Strategy<Value = String> {
1184        prop_oneof![
1185            // Simple strings (alphanumeric)
1186            prop::string::string_regex("[a-zA-Z][a-zA-Z0-9]{0,15}").unwrap(),
1187            // Numbers
1188            (1i32..10000).prop_map(|n| n.to_string()),
1189        ]
1190    }
1191
1192    /// Generate a boolean value in LLM format (+ or -)
1193    /// Reserved for future property tests covering boolean round-trips
1194    #[allow(dead_code)] // Test utility for future property tests
1195    fn _llm_bool() -> impl Strategy<Value = String> {
1196        prop::bool::ANY.prop_map(|b| if b { "+".to_string() } else { "-".to_string() })
1197    }
1198
1199    /// Generate a context section in LLM format: key|val\nkey|val
1200    /// Reserved for future property tests covering context section round-trips
1201    #[allow(dead_code)] // Test utility for future property tests
1202    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    /// Generate a data section in LLM format: #d(schema)\nrow\nrow
1213    /// Reserved for future property tests covering data section round-trips
1214    #[allow(dead_code)] // Test utility for future property tests
1215    fn _llm_data_section() -> impl Strategy<Value = String> {
1216        (
1217            prop::string::string_regex("[a-z]").unwrap(), // section id (single letter)
1218            prop::collection::vec(valid_abbrev_key(), 2..4), // schema columns
1219            prop::collection::vec(simple_value(), 2..4),  // row values
1220        )
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    /// Generate valid LLM format content
1232    /// Reserved for future property tests covering full document round-trips
1233    #[allow(dead_code)] // Test utility for future property tests
1234    fn _valid_llm_content() -> impl Strategy<Value = String> {
1235        prop_oneof![_llm_context_section(), _llm_data_section(),]
1236    }
1237
1238    // Feature: dx-serializer-extension-fix, Property 1: LLM to Human to LLM Round-Trip
1239    // For any valid LLM format string, converting to human format and back to LLM format
1240    // SHALL produce a document with equivalent data.
1241    // **Validates: Requirements 1.1-1.9, 2.1-2.6, 3.1-3.5, 3.6**
1242    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            // Skip if keys are duplicated
1253            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            // Use new format: root-level key|value pairs (one per line)
1259            let llm: String = pairs
1260                .iter()
1261                .map(|(k, v)| format!("{}|{}", k, v))
1262                .collect::<Vec<_>>()
1263                .join("\n");
1264
1265            // Transform to human
1266            let human_result = serializer.to_human(&llm);
1267            prop_assert!(human_result.success(), "to_human failed: {:?}", human_result.error());
1268
1269            // Transform back to LLM
1270            let llm_result = serializer.to_dense(&human_result.content());
1271            prop_assert!(llm_result.success(), "to_dense failed: {:?}", llm_result.error());
1272
1273            // Verify values are preserved
1274            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            // Use new format: root-level key=value pairs
1296            let llm = format!("{}={}\n{}={}", key1, b1, key2, b2);
1297
1298            // Transform to human
1299            let human_result = serializer.to_human(&llm);
1300            prop_assert!(human_result.success(), "to_human failed: {:?}", human_result.error());
1301
1302            // Human format should show true/false
1303            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            // Transform back to LLM
1313            let llm_result = serializer.to_dense(&human);
1314            prop_assert!(llm_result.success(), "to_dense failed: {:?}", llm_result.error());
1315
1316            // LLM format should have true or false
1317            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    // Feature: dx-serializer-extension, Property 3: String value preservation
1343    // For any string value (including URLs with query parameters, strings with
1344    // apostrophes, strings with both quote types, and strings with escape sequences),
1345    // transforming through the serializer SHALL preserve the exact string content.
1346    // **Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.5**
1347
1348    /// Generate URL-like strings with query parameters
1349    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    /// Generate strings with apostrophes
1376    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    /// Generate strings with double quotes
1388    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    /// Generate strings with both quote types
1397    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            // Test that URLs are preserved through smart_quote
1411            let quoted = smart_quote(&url);
1412
1413            // Extract the content (remove quotes if present)
1414            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            // Strings with apostrophes should use double quotes
1434            prop_assert!(
1435                quoted.starts_with('"') && quoted.ends_with('"'),
1436                "String with apostrophe should use double quotes: '{}' -> '{}'",
1437                s, quoted
1438            );
1439
1440            // Content should be preserved
1441            let extracted = &quoted[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            // Strings with double quotes should use single quotes
1454            prop_assert!(
1455                quoted.starts_with('\'') && quoted.ends_with('\''),
1456                "String with double quotes should use single quotes: '{}' -> '{}'",
1457                s, quoted
1458            );
1459
1460            // Content should be preserved
1461            let extracted = &quoted[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            // Should use double quotes with escaped internal double quotes
1474            prop_assert!(
1475                quoted.starts_with('"') && quoted.ends_with('"'),
1476                "Mixed quote string should use double quotes: '{}' -> '{}'",
1477                s, quoted
1478            );
1479
1480            // Content should be preserved (after unescaping)
1481            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            // Simple strings without special chars should not be quoted
1496            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            // Strings with spaces should be quoted
1512            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            // Strings with special DX chars should be quoted
1530            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}