Skip to main content

ison_rs/
lib.rs

1//! # ISON Parser for Rust
2//!
3//! A Rust implementation of the ISON (Interchange Simple Object Notation) parser.
4//! ISON is a minimal, LLM-friendly data serialization format optimized for AI/ML workflows.
5//!
6//! ## Quick Start
7//!
8//! ```rust
9//! use ison_rs::{parse, dumps, Value};
10//!
11//! let ison_text = r#"
12//! table.users
13//! id name email
14//! 1 Alice alice@example.com
15//! 2 Bob bob@example.com
16//! "#;
17//!
18//! let doc = parse(ison_text).unwrap();
19//! let users = doc.get("users").unwrap();
20//!
21//! for row in &users.rows {
22//!     println!("{}: {}", row.get("id").unwrap(), row.get("name").unwrap());
23//! }
24//!
25//! // Serialize back
26//! let output = dumps(&doc, true);
27//! ```
28
29use std::collections::HashMap;
30use std::fmt;
31
32// Plugins module (feature-gated)
33pub mod plugins;
34
35#[cfg(feature = "serde")]
36use serde::{Deserialize, Serialize};
37
38pub const VERSION: &str = "1.0.2";
39
40// =============================================================================
41// Error Types
42// =============================================================================
43
44/// Errors that can occur during ISON parsing
45#[derive(Debug, Clone)]
46pub struct ISONError {
47    pub message: String,
48    pub line: Option<usize>,
49}
50
51impl fmt::Display for ISONError {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        match self.line {
54            Some(line) => write!(f, "Line {}: {}", line, self.message),
55            None => write!(f, "{}", self.message),
56        }
57    }
58}
59
60impl std::error::Error for ISONError {}
61
62pub type Result<T> = std::result::Result<T, ISONError>;
63
64// =============================================================================
65// Types
66// =============================================================================
67
68/// Reference to another record in the document
69#[derive(Debug, Clone, PartialEq)]
70#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
71pub struct Reference {
72    pub id: String,
73    pub ref_type: Option<String>,
74}
75
76impl Reference {
77    /// Create a new simple reference
78    pub fn new(id: impl Into<String>) -> Self {
79        Self {
80            id: id.into(),
81            ref_type: None,
82        }
83    }
84
85    /// Create a new typed reference
86    pub fn with_type(id: impl Into<String>, ref_type: impl Into<String>) -> Self {
87        Self {
88            id: id.into(),
89            ref_type: Some(ref_type.into()),
90        }
91    }
92
93    /// Check if this is a relationship reference (UPPERCASE type)
94    pub fn is_relationship(&self) -> bool {
95        match &self.ref_type {
96            Some(t) => t.chars().all(|c| c.is_uppercase() || c == '_'),
97            None => false,
98        }
99    }
100
101    /// Get namespace (for non-relationship references)
102    pub fn get_namespace(&self) -> Option<&str> {
103        if self.is_relationship() {
104            None
105        } else {
106            self.ref_type.as_deref()
107        }
108    }
109
110    /// Get relationship type (for relationship references)
111    pub fn relationship_type(&self) -> Option<&str> {
112        if self.is_relationship() {
113            self.ref_type.as_deref()
114        } else {
115            None
116        }
117    }
118
119    /// Convert to ISON string representation
120    pub fn to_ison(&self) -> String {
121        match &self.ref_type {
122            Some(t) => format!(":{}:{}", t, self.id),
123            None => format!(":{}", self.id),
124        }
125    }
126}
127
128impl fmt::Display for Reference {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        write!(f, "{}", self.to_ison())
131    }
132}
133
134/// Value types in ISON
135#[derive(Debug, Clone, PartialEq)]
136#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
137#[cfg_attr(feature = "serde", serde(untagged))]
138pub enum Value {
139    Null,
140    Bool(bool),
141    Int(i64),
142    Float(f64),
143    String(String),
144    Reference(Reference),
145}
146
147impl Value {
148    pub fn is_null(&self) -> bool {
149        matches!(self, Value::Null)
150    }
151
152    pub fn is_bool(&self) -> bool {
153        matches!(self, Value::Bool(_))
154    }
155
156    pub fn is_int(&self) -> bool {
157        matches!(self, Value::Int(_))
158    }
159
160    pub fn is_float(&self) -> bool {
161        matches!(self, Value::Float(_))
162    }
163
164    pub fn is_string(&self) -> bool {
165        matches!(self, Value::String(_))
166    }
167
168    pub fn is_reference(&self) -> bool {
169        matches!(self, Value::Reference(_))
170    }
171
172    pub fn as_bool(&self) -> Option<bool> {
173        match self {
174            Value::Bool(b) => Some(*b),
175            _ => None,
176        }
177    }
178
179    pub fn as_int(&self) -> Option<i64> {
180        match self {
181            Value::Int(i) => Some(*i),
182            _ => None,
183        }
184    }
185
186    pub fn as_float(&self) -> Option<f64> {
187        match self {
188            Value::Float(f) => Some(*f),
189            Value::Int(i) => Some(*i as f64),
190            _ => None,
191        }
192    }
193
194    pub fn as_str(&self) -> Option<&str> {
195        match self {
196            Value::String(s) => Some(s),
197            _ => None,
198        }
199    }
200
201    pub fn as_reference(&self) -> Option<&Reference> {
202        match self {
203            Value::Reference(r) => Some(r),
204            _ => None,
205        }
206    }
207}
208
209impl fmt::Display for Value {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        match self {
212            Value::Null => write!(f, "null"),
213            Value::Bool(b) => write!(f, "{}", b),
214            Value::Int(i) => write!(f, "{}", i),
215            Value::Float(fl) => write!(f, "{}", fl),
216            Value::String(s) => write!(f, "{}", s),
217            Value::Reference(r) => write!(f, "{}", r),
218        }
219    }
220}
221
222/// A row of data (field name -> value mapping)
223pub type Row = HashMap<String, Value>;
224
225/// Field information including optional type annotation
226#[derive(Debug, Clone)]
227#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
228pub struct FieldInfo {
229    pub name: String,
230    pub field_type: Option<String>,
231    pub is_computed: bool,
232}
233
234impl FieldInfo {
235    pub fn new(name: impl Into<String>) -> Self {
236        Self {
237            name: name.into(),
238            field_type: None,
239            is_computed: false,
240        }
241    }
242
243    pub fn with_type(name: impl Into<String>, field_type: impl Into<String>) -> Self {
244        let ft: String = field_type.into();
245        let is_computed = ft == "computed";
246        Self {
247            name: name.into(),
248            field_type: Some(ft),
249            is_computed,
250        }
251    }
252}
253
254/// A block of structured data
255#[derive(Debug, Clone)]
256#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
257pub struct Block {
258    pub kind: String,
259    pub name: String,
260    pub fields: Vec<String>,
261    pub field_info: Vec<FieldInfo>,
262    pub rows: Vec<Row>,
263    pub summary_rows: Vec<Row>,
264}
265
266impl Block {
267    pub fn new(kind: impl Into<String>, name: impl Into<String>) -> Self {
268        Self {
269            kind: kind.into(),
270            name: name.into(),
271            fields: Vec::new(),
272            field_info: Vec::new(),
273            rows: Vec::new(),
274            summary_rows: Vec::new(),
275        }
276    }
277
278    /// Number of data rows
279    pub fn len(&self) -> usize {
280        self.rows.len()
281    }
282
283    /// Check if block has no rows
284    pub fn is_empty(&self) -> bool {
285        self.rows.is_empty()
286    }
287
288    /// Get row by index
289    pub fn get_row(&self, index: usize) -> Option<&Row> {
290        self.rows.get(index)
291    }
292
293    /// Get field type annotation
294    pub fn get_field_type(&self, field_name: &str) -> Option<&str> {
295        self.field_info
296            .iter()
297            .find(|fi| fi.name == field_name)
298            .and_then(|fi| fi.field_type.as_deref())
299    }
300
301    /// Get list of computed fields
302    pub fn get_computed_fields(&self) -> Vec<&str> {
303        self.field_info
304            .iter()
305            .filter(|fi| fi.is_computed)
306            .map(|fi| fi.name.as_str())
307            .collect()
308    }
309}
310
311impl std::ops::Index<usize> for Block {
312    type Output = Row;
313
314    fn index(&self, index: usize) -> &Self::Output {
315        &self.rows[index]
316    }
317}
318
319/// A complete ISON document
320#[derive(Debug, Clone, Default)]
321#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
322pub struct Document {
323    pub blocks: Vec<Block>,
324}
325
326impl Document {
327    pub fn new() -> Self {
328        Self { blocks: Vec::new() }
329    }
330
331    /// Get block by name
332    pub fn get(&self, name: &str) -> Option<&Block> {
333        self.blocks.iter().find(|b| b.name == name)
334    }
335
336    /// Get mutable block by name
337    pub fn get_mut(&mut self, name: &str) -> Option<&mut Block> {
338        self.blocks.iter_mut().find(|b| b.name == name)
339    }
340
341    /// Check if block exists
342    pub fn has(&self, name: &str) -> bool {
343        self.blocks.iter().any(|b| b.name == name)
344    }
345
346    /// Number of blocks
347    pub fn len(&self) -> usize {
348        self.blocks.len()
349    }
350
351    /// Check if document is empty
352    pub fn is_empty(&self) -> bool {
353        self.blocks.is_empty()
354    }
355
356    /// Convert to JSON string (requires serde feature)
357    #[cfg(feature = "serde")]
358    pub fn to_json(&self, pretty: bool) -> String {
359        let map: HashMap<&str, Vec<&Row>> = self
360            .blocks
361            .iter()
362            .map(|b| (b.name.as_str(), b.rows.iter().collect()))
363            .collect();
364
365        if pretty {
366            serde_json::to_string_pretty(&map).unwrap_or_default()
367        } else {
368            serde_json::to_string(&map).unwrap_or_default()
369        }
370    }
371}
372
373impl std::ops::Index<&str> for Document {
374    type Output = Block;
375
376    fn index(&self, name: &str) -> &Self::Output {
377        self.get(name).expect("Block not found")
378    }
379}
380
381// =============================================================================
382// Parser
383// =============================================================================
384
385struct Parser<'a> {
386    text: &'a str,
387    pos: usize,
388    line: usize,
389}
390
391impl<'a> Parser<'a> {
392    fn new(text: &'a str) -> Self {
393        Self {
394            text,
395            pos: 0,
396            line: 1,
397        }
398    }
399
400    fn parse(&mut self) -> Result<Document> {
401        let mut doc = Document::new();
402
403        self.skip_whitespace_and_comments();
404
405        while self.pos < self.text.len() {
406            if let Some(block) = self.parse_block()? {
407                doc.blocks.push(block);
408            }
409            self.skip_whitespace_and_comments();
410        }
411
412        Ok(doc)
413    }
414
415    fn parse_block(&mut self) -> Result<Option<Block>> {
416        let header_line = match self.read_line() {
417            Some(line) => line,
418            None => return Ok(None),
419        };
420
421        if header_line.starts_with('#') || header_line.is_empty() {
422            return Ok(None);
423        }
424
425        let dot_index = header_line.find('.').ok_or_else(|| ISONError {
426            message: format!("Invalid block header: {}", header_line),
427            line: Some(self.line),
428        })?;
429
430        let kind = header_line[..dot_index].trim().to_string();
431        let name = header_line[dot_index + 1..].trim().to_string();
432
433        if kind.is_empty() || name.is_empty() {
434            return Err(ISONError {
435                message: format!("Invalid block header: {}", header_line),
436                line: Some(self.line),
437            });
438        }
439
440        let mut block = Block::new(kind, name);
441
442        // Parse field definitions
443        self.skip_empty_lines();
444        let fields_line = match self.read_line() {
445            Some(line) => line,
446            None => return Ok(Some(block)),
447        };
448
449        let field_tokens = self.tokenize_line(&fields_line);
450        for (token, _) in field_tokens {
451            if let Some(colon_idx) = token.find(':') {
452                let field_name = token[..colon_idx].to_string();
453                let field_type = token[colon_idx + 1..].to_string();
454                block.fields.push(field_name.clone());
455                block.field_info.push(FieldInfo::with_type(field_name, field_type));
456            } else {
457                block.fields.push(token.clone());
458                block.field_info.push(FieldInfo::new(token));
459            }
460        }
461
462        // Parse data rows
463        let mut in_summary = false;
464        while self.pos < self.text.len() {
465            let line = match self.peek_line() {
466                Some(line) => line,
467                None => break,
468            };
469
470            // Empty line or new block = end of current block
471            if line.is_empty() || Self::looks_like_header(&line) {
472                break;
473            }
474
475            self.read_line(); // consume the line
476
477            // Skip comments
478            if line.starts_with('#') {
479                continue;
480            }
481
482            // Summary separator
483            if line.trim() == "---" {
484                in_summary = true;
485                continue;
486            }
487
488            let mut values = self.tokenize_line(&line);
489
490            // An unquoted token starting with '#' begins an inline comment
491            let keep = strip_inline_comment(&values);
492            values.truncate(keep);
493            if values.is_empty() {
494                continue;
495            }
496
497            // More values than fields is an error, not a silent truncation
498            check_extra_tokens(&values, block.fields.len(), Some(self.line.saturating_sub(1)))?;
499
500            let mut row = Row::new();
501            for (i, field) in block.fields.iter().enumerate() {
502                let value = match values.get(i) {
503                    // Quoted tokens keep their string type
504                    Some((token, true)) => Value::String(token.clone()),
505                    Some((token, false)) => self.parse_value(token)?,
506                    // Missing trailing values pad with null
507                    None => Value::Null,
508                };
509                row.insert(field.clone(), value);
510            }
511
512            if in_summary {
513                block.summary_rows.push(row);
514            } else {
515                block.rows.push(row);
516            }
517        }
518
519        Ok(Some(block))
520    }
521
522    /// Tokenize a line into `(token, was_quoted)` pairs.
523    ///
524    /// Inline comments are deliberately NOT stripped here at the string
525    /// level (the old approach corrupted quoted values containing `#`).
526    /// Instead, `strip_inline_comment` applies the token-level rule: an
527    /// unquoted token starting with `#` begins an inline comment.
528    fn tokenize_line(&self, line: &str) -> Vec<(String, bool)> {
529        let mut tokens = Vec::new();
530        let chars: Vec<char> = line.chars().collect();
531        let mut i = 0;
532
533        while i < chars.len() {
534            // Skip whitespace
535            while i < chars.len() && (chars[i] == ' ' || chars[i] == '\t') {
536                i += 1;
537            }
538
539            if i >= chars.len() {
540                break;
541            }
542
543            // Quoted string
544            if chars[i] == '"' {
545                let (token, new_pos) = self.parse_quoted_string(&chars, i);
546                tokens.push((token, true));
547                i = new_pos;
548            } else {
549                // Unquoted token
550                let start = i;
551                while i < chars.len() && chars[i] != ' ' && chars[i] != '\t' {
552                    i += 1;
553                }
554                tokens.push((chars[start..i].iter().collect(), false));
555            }
556        }
557
558        tokens
559    }
560
561    /// Check if a line looks like a block header: a single whitespace-free
562    /// token of the form `ident.ident` (mirrors the Python parser's
563    /// `_looks_like_header`). Serialized data rows can never match this
564    /// because the serializer quotes strings containing `.`.
565    fn looks_like_header(line: &str) -> bool {
566        fn is_identifier(s: &str) -> bool {
567            let mut chars = s.chars();
568            match chars.next() {
569                Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
570                _ => return false,
571            }
572            chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
573        }
574
575        if line.split_whitespace().count() != 1 {
576            return false;
577        }
578        let parts: Vec<&str> = line.split('.').collect();
579        parts.len() == 2 && is_identifier(parts[0]) && is_identifier(parts[1])
580    }
581
582    fn parse_quoted_string(&self, chars: &[char], start: usize) -> (String, usize) {
583        let mut result = String::new();
584        let mut i = start + 1; // skip opening quote
585
586        while i < chars.len() {
587            if chars[i] == '\\' {
588                if i + 1 < chars.len() {
589                    let next = chars[i + 1];
590                    match next {
591                        'n' => result.push('\n'),
592                        't' => result.push('\t'),
593                        'r' => result.push('\r'),
594                        '\\' => result.push('\\'),
595                        '"' => result.push('"'),
596                        '|' => result.push('|'),
597                        _ => result.push(next),
598                    }
599                    i += 2;
600                } else {
601                    result.push('\\');
602                    i += 1;
603                }
604            } else if chars[i] == '"' {
605                return (result, i + 1);
606            } else {
607                result.push(chars[i]);
608                i += 1;
609            }
610        }
611
612        (result, i)
613    }
614
615    fn parse_value(&self, token: &str) -> Result<Value> {
616        // Null
617        if token == "null" || token == "~" {
618            return Ok(Value::Null);
619        }
620
621        // Boolean
622        if token == "true" {
623            return Ok(Value::Bool(true));
624        }
625        if token == "false" {
626            return Ok(Value::Bool(false));
627        }
628
629        // Reference
630        if token.starts_with(':') {
631            return self.parse_reference(token);
632        }
633
634        // Integer
635        if let Ok(i) = token.parse::<i64>() {
636            return Ok(Value::Int(i));
637        }
638
639        // Float
640        if let Ok(f) = token.parse::<f64>() {
641            return Ok(Value::Float(f));
642        }
643
644        // String
645        Ok(Value::String(token.to_string()))
646    }
647
648    fn parse_reference(&self, token: &str) -> Result<Value> {
649        let content = &token[1..]; // skip ':'
650        let parts: Vec<&str> = content.split(':').collect();
651
652        match parts.len() {
653            1 => Ok(Value::Reference(Reference::new(parts[0]))),
654            2 => Ok(Value::Reference(Reference::with_type(parts[1], parts[0]))),
655            _ => Err(ISONError {
656                message: format!("Invalid reference: {}", token),
657                line: Some(self.line),
658            }),
659        }
660    }
661
662    fn read_line(&mut self) -> Option<String> {
663        if self.pos >= self.text.len() {
664            return None;
665        }
666
667        let start = self.pos;
668        while self.pos < self.text.len() && self.text.as_bytes()[self.pos] != b'\n' {
669            self.pos += 1;
670        }
671
672        let line = self.text[start..self.pos].trim().to_string();
673
674        if self.pos < self.text.len() {
675            self.pos += 1; // skip newline
676        }
677        self.line += 1;
678
679        Some(line)
680    }
681
682    fn peek_line(&self) -> Option<String> {
683        if self.pos >= self.text.len() {
684            return None;
685        }
686
687        let mut end = self.pos;
688        while end < self.text.len() && self.text.as_bytes()[end] != b'\n' {
689            end += 1;
690        }
691
692        Some(self.text[self.pos..end].trim().to_string())
693    }
694
695    fn skip_whitespace_and_comments(&mut self) {
696        while self.pos < self.text.len() {
697            let ch = self.text.as_bytes()[self.pos];
698            match ch {
699                b' ' | b'\t' | b'\r' => self.pos += 1,
700                b'\n' => {
701                    self.pos += 1;
702                    self.line += 1;
703                }
704                b'#' => {
705                    while self.pos < self.text.len() && self.text.as_bytes()[self.pos] != b'\n' {
706                        self.pos += 1;
707                    }
708                }
709                _ => break,
710            }
711        }
712    }
713
714    fn skip_empty_lines(&mut self) {
715        while self.pos < self.text.len() {
716            let ch = self.text.as_bytes()[self.pos];
717            match ch {
718                b' ' | b'\t' | b'\r' => self.pos += 1,
719                b'\n' => {
720                    self.pos += 1;
721                    self.line += 1;
722                }
723                b'#' => {
724                    while self.pos < self.text.len() && self.text.as_bytes()[self.pos] != b'\n' {
725                        self.pos += 1;
726                    }
727                }
728                _ => break,
729            }
730        }
731    }
732}
733
734// =============================================================================
735// Row integrity helpers (shared by the regular and ISONL parsers)
736// =============================================================================
737
738/// Return the number of leading tokens that are data: an unquoted token
739/// whose first character is `#` begins an inline comment, discarding it and
740/// every token after it. Quoted tokens are always data.
741fn strip_inline_comment(tokens: &[(String, bool)]) -> usize {
742    for (i, (token, was_quoted)) in tokens.iter().enumerate() {
743        if !was_quoted && token.starts_with('#') {
744            return i;
745        }
746    }
747    tokens.len()
748}
749
750/// Reject rows with more values than fields instead of silently truncating
751/// them. Missing trailing values are still allowed (they pad with null).
752fn check_extra_tokens(
753    tokens: &[(String, bool)],
754    field_count: usize,
755    line: Option<usize>,
756) -> Result<()> {
757    if tokens.len() <= field_count {
758        return Ok(());
759    }
760    Err(ISONError {
761        message: format!(
762            "Row has {} values but only {} fields (extra value: {:?})",
763            tokens.len(),
764            field_count,
765            tokens[field_count].0
766        ),
767        line,
768    })
769}
770
771// =============================================================================
772// Serializer
773// =============================================================================
774
775/// One half of a `kind.name` block header: an identifier starting with a
776/// letter or underscore, followed by letters, digits, underscores or hyphens.
777fn is_header_part(s: &str) -> bool {
778    let mut chars = s.chars();
779    match chars.next() {
780        Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
781        _ => return false,
782    }
783    chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
784}
785
786/// Whether a value would be mistaken for a `kind.name` block header if emitted
787/// unquoted as the only token on a line.
788///
789/// Deliberately narrower than "contains a dot": `alice@example.com`, `a.b.c`
790/// and `v1.2` are all safe unquoted, and quoting them would waste tokens and
791/// diverge from the other implementations.
792fn looks_like_block_header(s: &str) -> bool {
793    let parts: Vec<&str> = s.split('.').collect();
794    parts.len() == 2 && parts.iter().all(|p| is_header_part(p))
795}
796
797struct Serializer {
798    align_columns: bool,
799    delimiter: String,
800}
801
802impl Serializer {
803    fn new(align_columns: bool) -> Self {
804        Self { align_columns, delimiter: " ".to_string() }
805    }
806
807    fn with_delimiter(align_columns: bool, delimiter: &str) -> Self {
808        Self { align_columns, delimiter: delimiter.to_string() }
809    }
810
811    fn serialize(&self, doc: &Document) -> Result<String> {
812        let mut parts = Vec::with_capacity(doc.blocks.len());
813        for b in &doc.blocks {
814            validate_block_names(b)?;
815            validate_row_references(b, &REFERENCE_FORBIDDEN_ISON)?;
816            parts.push(self.serialize_block(b));
817        }
818        Ok(parts.join("\n\n"))
819    }
820
821    fn serialize_block(&self, block: &Block) -> String {
822        let mut lines = Vec::new();
823
824        // Header
825        lines.push(format!("{}.{}", block.kind, block.name));
826
827        // Fields with types
828        let field_defs: Vec<String> = block
829            .field_info
830            .iter()
831            .map(|fi| {
832                if let Some(ref ft) = fi.field_type {
833                    format!("{}:{}", fi.name, ft)
834                } else {
835                    fi.name.clone()
836                }
837            })
838            .collect();
839        lines.push(field_defs.join(&self.delimiter));
840
841        // Calculate column widths for alignment
842        let widths = if self.align_columns {
843            self.calculate_widths(block)
844        } else {
845            vec![]
846        };
847
848        // Data rows
849        for row in &block.rows {
850            lines.push(self.serialize_row(row, &block.fields, &widths));
851        }
852
853        // Summary separator and rows
854        if !block.summary_rows.is_empty() {
855            lines.push("---".to_string());
856            for row in &block.summary_rows {
857                lines.push(self.serialize_row(row, &block.fields, &widths));
858            }
859        }
860
861        lines.join("\n")
862    }
863
864    fn calculate_widths(&self, block: &Block) -> Vec<usize> {
865        let mut widths: Vec<usize> = block.fields.iter().map(|f| f.len()).collect();
866
867        for row in block.rows.iter().chain(block.summary_rows.iter()) {
868            for (i, field) in block.fields.iter().enumerate() {
869                if let Some(value) = row.get(field) {
870                    let str_val = self.serialize_value(value);
871                    if i < widths.len() {
872                        widths[i] = widths[i].max(str_val.len());
873                    }
874                }
875            }
876        }
877
878        widths
879    }
880
881    fn serialize_row(&self, row: &Row, fields: &[String], widths: &[usize]) -> String {
882        let mut values = Vec::new();
883
884        for (i, field) in fields.iter().enumerate() {
885            let value = row.get(field).cloned().unwrap_or(Value::Null);
886            let mut str_val = self.serialize_value(&value);
887
888            if self.align_columns && !widths.is_empty() && i < fields.len() - 1 {
889                while str_val.len() < widths[i] {
890                    str_val.push(' ');
891                }
892            }
893            values.push(str_val);
894        }
895
896        values.join(&self.delimiter)
897    }
898
899    fn serialize_value(&self, value: &Value) -> String {
900        match value {
901            Value::Null => "null".to_string(),
902            Value::Bool(b) => if *b { "true" } else { "false" }.to_string(),
903            Value::Int(i) => i.to_string(),
904            Value::Float(f) => f.to_string(),
905            Value::Reference(r) => r.to_ison(),
906            Value::String(s) => self.serialize_string(s),
907        }
908    }
909
910    fn serialize_string(&self, s: &str) -> String {
911        if s.is_empty() {
912            return "\"\"".to_string();
913        }
914
915        // '\r' and '\\' would be emitted raw and corrupt on re-parse; a
916        // leading '#' would turn the value into an inline comment (or the
917        // line into a whole-line comment) and silently lose data.
918        let needs_quotes = s.contains(' ')
919            || s.contains('\t')
920            || s.contains('\n')
921            || s.contains('\r')
922            || s.contains('"')
923            || s.contains('\\')
924            // Only 'ident.ident' shapes need quoting: alone on a line they
925            // would be re-parsed as a block header. Quoting every value
926            // containing a '.' would also quote emails, domains and version
927            // strings, which costs tokens and diverges from the other ports.
928            || looks_like_block_header(s)
929            || s == "true"
930            || s == "false"
931            || s == "null"
932        || s == "~"
933            || s == "~"
934            || s.starts_with('#')
935            || s.starts_with(':')
936            || s.parse::<f64>().is_ok();
937
938        if !needs_quotes {
939            return s.to_string();
940        }
941
942        let escaped = s
943            .replace('\\', "\\\\")
944            .replace('"', "\\\"")
945            .replace('\n', "\\n")
946            .replace('\t', "\\t")
947            .replace('\r', "\\r");
948
949        format!("\"{}\"", escaped)
950    }
951}
952
953// =============================================================================
954// Canonical Serializer
955// =============================================================================
956
957struct CanonicalSerializer;
958
959impl CanonicalSerializer {
960    fn new() -> Self {
961        Self
962    }
963
964    fn serialize(&self, doc: &Document) -> Result<String> {
965        for b in &doc.blocks {
966            validate_block_names(b)?;
967            validate_row_references(b, &REFERENCE_FORBIDDEN_ISON)?;
968        }
969
970        // Sort blocks ordinal-string by kind.name
971        let mut sorted_blocks = doc.blocks.clone();
972        sorted_blocks.sort_by(|a, b| {
973            let key_a = format!("{}.{}", a.kind, a.name);
974            let key_b = format!("{}.{}", b.kind, b.name);
975            key_a.cmp(&key_b)
976        });
977
978        let parts: Vec<String> = sorted_blocks
979            .iter()
980            .map(|b| self.serialize_block_canonical(b))
981            .collect();
982        Ok(parts.join("\n\n"))
983    }
984
985    fn sort_fields_canonical(&self, fields: &[String]) -> Vec<String> {
986        // Sort fields for canonical form: id first, then alphabetically by UTF-8 bytes.
987        // Rationale:
988        // - Canonical form must be order-independent across implementations
989        // - Python dict insertion-order preservation masks unordered iteration in
990        //   Rust HashMap and Go map, causing cross-language byte-identity to fail
991        // - Sorting fields explicitly ensures byte-identical output regardless of
992        //   how the parser discovered them
993        // - 'id' hoisted first (anchor for :type:id references); remaining fields
994        //   sorted by UTF-8 byte comparison (ordinal, not Unicode code point)
995
996        // Partition fields: id vs others
997        let id_fields: Vec<String> = fields.iter().filter(|f| *f == "id").cloned().collect();
998        let mut other_fields: Vec<String> = fields.iter().filter(|f| *f != "id").cloned().collect();
999
1000        // Sort other fields by UTF-8 bytes (not by Unicode code points)
1001        // Using bytes comparison ensures the same rule across all implementations
1002        other_fields.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
1003
1004        // Return: id first (if present), then sorted others
1005        [id_fields, other_fields].concat()
1006    }
1007
1008    /// Order rows on the FULL canonical field tuple.
1009    ///
1010    /// Keying on the first column alone left ties resolved by input order, so
1011    /// the same logical data serialized to different bytes depending on how the
1012    /// rows were built -- which defeats content addressing and prefix
1013    /// stability.
1014    ///
1015    /// Values compare as UTF-8 bytes via `as_bytes()`, matching
1016    /// `sort_fields_canonical`. Rust `String` ordering is already by UTF-8
1017    /// bytes, so this agrees with `cmp` here, but stating it explicitly keeps
1018    /// both sorts expressed in the same terms as every other implementation.
1019    fn sort_rows_by_key_canonical(&self, block: &Block, sorted_fields: &[String]) -> Vec<Row> {
1020        if block.rows.is_empty() || sorted_fields.is_empty() {
1021            return block.rows.clone();
1022        }
1023
1024        let mut sorted_rows = block.rows.clone();
1025
1026        sorted_rows.sort_by(|a, b| {
1027            for field in sorted_fields {
1028                let val_a = a.get(field);
1029                let val_b = b.get(field);
1030
1031                // Nulls sort last at EVERY position, not only the key column.
1032                let is_null_a = val_a.is_none() || matches!(val_a, Some(Value::Null));
1033                let is_null_b = val_b.is_none() || matches!(val_b, Some(Value::Null));
1034
1035                let ord = match (is_null_a, is_null_b) {
1036                    (true, true) => continue,
1037                    (true, false) => std::cmp::Ordering::Greater,
1038                    (false, true) => std::cmp::Ordering::Less,
1039                    (false, false) => {
1040                        let str_a = self.value_to_string(val_a.unwrap());
1041                        let str_b = self.value_to_string(val_b.unwrap());
1042                        str_a.as_bytes().cmp(str_b.as_bytes())
1043                    }
1044                };
1045
1046                if ord != std::cmp::Ordering::Equal {
1047                    return ord;
1048                }
1049            }
1050            std::cmp::Ordering::Equal
1051        });
1052
1053        sorted_rows
1054    }
1055
1056    fn serialize_isonl(&self, doc: &Document) -> Result<String> {
1057        let mut lines = Vec::new();
1058
1059        // Sort blocks ordinal-string by kind.name
1060        let mut sorted_blocks = doc.blocks.clone();
1061        sorted_blocks.sort_by(|a, b| {
1062            let key_a = format!("{}.{}", a.kind, a.name);
1063            let key_b = format!("{}.{}", b.kind, b.name);
1064            key_a.cmp(&key_b)
1065        });
1066
1067        for block in &sorted_blocks {
1068            validate_isonl_envelope(block)?;
1069            let header = format!("{}.{}", block.kind, block.name);
1070
1071            // Sort fields: id first (if present), then alphabetically by UTF-8 bytes
1072            let sorted_fields = self.sort_fields_canonical(&block.fields);
1073
1074            // Serialize field definitions - use field_info if available, otherwise use fields directly
1075            let fields: Vec<String> = if !block.field_info.is_empty() {
1076                sorted_fields
1077                    .iter()
1078                    .map(|field_name| {
1079                        // Find field_info for this field
1080                        if let Some(fi) = block.field_info.iter().find(|fi| &fi.name == field_name) {
1081                            if let Some(ref ft) = fi.field_type {
1082                                format!("{}:{}", fi.name, ft)
1083                            } else {
1084                                fi.name.clone()
1085                            }
1086                        } else {
1087                            field_name.clone()
1088                        }
1089                    })
1090                    .collect()
1091            } else {
1092                sorted_fields.clone()
1093            };
1094            let fields_str = fields.join(" ");
1095
1096            // Sort rows ordinal-string by first column value (key), using canonical field order
1097            let sorted_rows = self.sort_rows_by_key_canonical(block, &sorted_fields);
1098
1099            // Serialize each row. ISONL values use their own quoting rules:
1100            // the pipe is significant, but a value can never be misread as a
1101            // block header because every line carries its own envelope.
1102            for row in &sorted_rows {
1103                let values: Vec<String> = sorted_fields
1104                    .iter()
1105                    .map(|f| {
1106                        row.get(f)
1107                            .map(|v| self.serialize_value_canonical_isonl(v))
1108                            .unwrap_or_else(|| "null".to_string())
1109                    })
1110                    .collect();
1111                lines.push(format!("{}|{}|{}", header, fields_str, values.join(" ")));
1112            }
1113        }
1114
1115        Ok(lines.join("\n"))
1116    }
1117
1118    fn serialize_block_canonical(&self, block: &Block) -> String {
1119        let mut lines = Vec::new();
1120
1121        // Header
1122        lines.push(format!("{}.{}", block.kind, block.name));
1123
1124        // Sort fields: id first (if present), then alphabetically by UTF-8 bytes
1125        let sorted_fields = self.sort_fields_canonical(&block.fields);
1126
1127        // Fields with types - use field_info if available, otherwise use fields directly
1128        let field_defs: Vec<String> = if !block.field_info.is_empty() {
1129            sorted_fields
1130                .iter()
1131                .map(|field_name| {
1132                    // Find field_info for this field
1133                    if let Some(fi) = block.field_info.iter().find(|fi| &fi.name == field_name) {
1134                        if let Some(ref ft) = fi.field_type {
1135                            format!("{}:{}", fi.name, ft)
1136                        } else {
1137                            fi.name.clone()
1138                        }
1139                    } else {
1140                        field_name.clone()
1141                    }
1142                })
1143                .collect()
1144        } else {
1145            sorted_fields.clone()
1146        };
1147        lines.push(field_defs.join(" "));
1148
1149        // Sort rows ordinal-string by first column value (key), using canonical field order
1150        let sorted_rows = self.sort_rows_by_key_canonical(block, &sorted_fields);
1151
1152        // Data rows (no alignment, single-space delimiter)
1153        for row in &sorted_rows {
1154            let values: Vec<String> = sorted_fields
1155                .iter()
1156                .map(|f| {
1157                    row.get(f)
1158                        .map(|v| self.serialize_value_canonical(v))
1159                        .unwrap_or_else(|| "null".to_string())
1160                })
1161                .collect();
1162            lines.push(values.join(" "));
1163        }
1164
1165        // Summary separator and rows (if present)
1166        if !block.summary_rows.is_empty() {
1167            lines.push("---".to_string());
1168            for row in &block.summary_rows {
1169                let values: Vec<String> = sorted_fields
1170                    .iter()
1171                    .map(|f| {
1172                        row.get(f)
1173                            .map(|v| self.serialize_value_canonical(v))
1174                            .unwrap_or_else(|| "null".to_string())
1175                    })
1176                    .collect();
1177                lines.push(values.join(" "));
1178            }
1179        }
1180
1181        lines.join("\n")
1182    }
1183
1184    fn serialize_value_canonical(&self, value: &Value) -> String {
1185        match value {
1186            Value::Null => "null".to_string(),
1187            Value::Bool(b) => if *b { "true" } else { "false" }.to_string(),
1188            Value::Int(i) => i.to_string(),
1189            Value::Float(f) => f.to_string(),
1190            Value::Reference(r) => r.to_ison(),
1191            Value::String(s) => self.serialize_string_canonical(s),
1192        }
1193    }
1194
1195    fn serialize_value_canonical_isonl(&self, value: &Value) -> String {
1196        match value {
1197            Value::String(s) => self.serialize_string_canonical_isonl(s),
1198            other => self.serialize_value_canonical(other),
1199        }
1200    }
1201
1202    /// Quoting for canonical ISONL values.
1203    ///
1204    /// Differs from the ISON rules in two ways: the pipe separates sections so
1205    /// it must be escaped, and a value can never be mistaken for a block
1206    /// header (every ISONL line carries its own `kind.name` envelope), so the
1207    /// header-shape rule does not apply and would only waste tokens.
1208    fn serialize_string_canonical_isonl(&self, s: &str) -> String {
1209        if s.is_empty() {
1210            return "\"\"".to_string();
1211        }
1212
1213        let needs_quotes = s.contains(' ')
1214            || s.contains('\t')
1215            || s.contains('\n')
1216            || s.contains('\r')
1217            || s.contains('"')
1218            || s.contains('\\')
1219            || s.contains('|')
1220            || s == "true"
1221            || s == "false"
1222            || s == "null"
1223        || s == "~"
1224            || s == "~"
1225            || s.starts_with('#')
1226            || s.starts_with(':')
1227            || s.parse::<f64>().is_ok();
1228
1229        if !needs_quotes {
1230            return s.to_string();
1231        }
1232
1233        format!(
1234            "\"{}\"",
1235            s.replace('\\', "\\\\")
1236                .replace('"', "\\\"")
1237                .replace('\n', "\\n")
1238                .replace('\t', "\\t")
1239                .replace('\r', "\\r")
1240                .replace('|', "\\|")
1241        )
1242    }
1243
1244    fn serialize_string_canonical(&self, s: &str) -> String {
1245        if s.is_empty() {
1246            return "\"\"".to_string();
1247        }
1248
1249        // Same quoting rules as regular Serializer
1250        let needs_quotes = s.contains(' ')
1251            || s.contains('\t')
1252            || s.contains('\n')
1253            || s.contains('\r')
1254            || s.contains('"')
1255            || s.contains('\\')
1256            || looks_like_block_header(s)
1257            || s == "true"
1258            || s == "false"
1259            || s == "null"
1260        || s == "~"
1261            || s == "~"
1262            || s.starts_with('#')
1263            || s.starts_with(':')
1264            || s.parse::<f64>().is_ok();
1265
1266        if !needs_quotes {
1267            return s.to_string();
1268        }
1269
1270        let escaped = s
1271            .replace('\\', "\\\\")
1272            .replace('"', "\\\"")
1273            .replace('\n', "\\n")
1274            .replace('\t', "\\t")
1275            .replace('\r', "\\r");
1276
1277        format!("\"{}\"", escaped)
1278    }
1279
1280    /// Convert a value to string for ordinal comparison
1281    fn value_to_string(&self, value: &Value) -> String {
1282        match value {
1283            Value::Null => String::new(), // Should not be used due to null check
1284            Value::Bool(b) => if *b { "true" } else { "false" }.to_string(),
1285            Value::Int(i) => i.to_string(),
1286            Value::Float(f) => f.to_string(),
1287            Value::Reference(r) => r.to_ison(),
1288            Value::String(s) => s.clone(),
1289        }
1290    }
1291}
1292
1293// =============================================================================
1294// ISONL Parser/Serializer
1295// =============================================================================
1296
1297/// Characters that would corrupt the line structure if they appeared raw in
1298/// the envelope (kind, name, or field names)
1299const ISONL_ENVELOPE_FORBIDDEN: &[char] = &['|', '"', '\\', ' ', '\t', '\n', '\r'];
1300
1301/// Split an ISONL line by unquoted pipe characters.
1302///
1303/// The scanner is both quote-aware and escape-aware: while inside quotes, a
1304/// backslash consumes the escape pair (both chars are pushed and the cursor
1305/// advances by two) so a value ending in an escaped backslash (`"x \\"`)
1306/// cannot desync the quote tracking and let a later `|` split wrongly.
1307fn split_isonl_sections(line: &str) -> Vec<String> {
1308    let chars: Vec<char> = line.chars().collect();
1309    let mut sections = Vec::new();
1310    let mut current = String::new();
1311    let mut in_quotes = false;
1312    let mut i = 0;
1313
1314    while i < chars.len() {
1315        let ch = chars[i];
1316
1317        if in_quotes && ch == '\\' && i + 1 < chars.len() {
1318            // Consume the escape pair so an escaped backslash before a
1319            // closing quote ("foo\\") can't desync the quote tracking
1320            current.push(ch);
1321            current.push(chars[i + 1]);
1322            i += 2;
1323            continue;
1324        }
1325
1326        if ch == '"' {
1327            in_quotes = !in_quotes;
1328            current.push(ch);
1329        } else if ch == '|' && !in_quotes {
1330            sections.push(current.trim().to_string());
1331            current = String::new();
1332        } else {
1333            current.push(ch);
1334        }
1335
1336        i += 1;
1337    }
1338
1339    sections.push(current.trim().to_string());
1340    sections
1341}
1342
1343/// Tokenize the values section of an ISONL line.
1344///
1345/// Returns `(token, was_quoted)` pairs. `#` is never stripped at the string
1346/// level here; inline comments are handled token-level by
1347/// `strip_inline_comment` (an unquoted token starting with `#`), so quoted
1348/// values containing `#` are never corrupted. Quoted tokens keep their
1349/// string type during parsing.
1350fn tokenize_isonl_values(line: &str) -> Vec<(String, bool)> {
1351    let chars: Vec<char> = line.chars().collect();
1352    let mut tokens = Vec::new();
1353    let mut i = 0;
1354
1355    while i < chars.len() {
1356        // Skip whitespace
1357        while i < chars.len() && (chars[i] == ' ' || chars[i] == '\t') {
1358            i += 1;
1359        }
1360        if i >= chars.len() {
1361            break;
1362        }
1363
1364        if chars[i] == '"' {
1365            // Quoted string with escape handling
1366            let mut result = String::new();
1367            i += 1; // skip opening quote
1368            while i < chars.len() {
1369                let ch = chars[i];
1370                if ch == '"' {
1371                    i += 1; // skip closing quote
1372                    break;
1373                }
1374                if ch == '\\' {
1375                    if i + 1 < chars.len() {
1376                        let next = chars[i + 1];
1377                        match next {
1378                            'n' => result.push('\n'),
1379                            't' => result.push('\t'),
1380                            'r' => result.push('\r'),
1381                            '\\' => result.push('\\'),
1382                            '"' => result.push('"'),
1383                            '|' => result.push('|'),
1384                            _ => result.push(next),
1385                        }
1386                        i += 2;
1387                    } else {
1388                        result.push('\\');
1389                        i += 1;
1390                    }
1391                } else {
1392                    result.push(ch);
1393                    i += 1;
1394                }
1395            }
1396            tokens.push((result, true));
1397        } else {
1398            // Unquoted token
1399            let start = i;
1400            while i < chars.len() && chars[i] != ' ' && chars[i] != '\t' {
1401                i += 1;
1402            }
1403            tokens.push((chars[start..i].iter().collect(), false));
1404        }
1405    }
1406
1407    tokens
1408}
1409
1410/// Quote and escape a string for the ISONL values section if needed
1411fn isonl_quote_if_needed(s: &str) -> String {
1412    if s.is_empty() {
1413        return "\"\"".to_string();
1414    }
1415
1416    let needs_quote = s.contains(' ')
1417        || s.contains('\t')
1418        || s.contains('"')
1419        || s.contains('\n')
1420        || s.contains('\r')
1421        || s.contains('\\')
1422        || s.contains('|')
1423        || s == "true"
1424        || s == "false"
1425        || s == "null"
1426        || s == "~"
1427        || s.starts_with('#')
1428        || s.starts_with(':')
1429        || s.parse::<f64>().is_ok();
1430
1431    if !needs_quote {
1432        return s.to_string();
1433    }
1434
1435    let escaped = s
1436        .replace('\\', "\\\\")
1437        .replace('"', "\\\"")
1438        .replace('\n', "\\n")
1439        .replace('\t', "\\t")
1440        .replace('\r', "\\r")
1441        .replace('|', "\\|");
1442
1443    format!("\"{}\"", escaped)
1444}
1445
1446/// Serialize a value for the ISONL values section
1447fn isonl_serialize_value(value: &Value) -> String {
1448    match value {
1449        Value::Null => "null".to_string(),
1450        Value::Bool(b) => if *b { "true" } else { "false" }.to_string(),
1451        Value::Int(i) => i.to_string(),
1452        Value::Float(f) => f.to_string(),
1453        Value::Reference(r) => r.to_ison(),
1454        Value::String(s) => isonl_quote_if_needed(s),
1455    }
1456}
1457
1458/// Characters a block kind or name may not contain, plus the two more that a
1459/// field name may not.
1460///
1461/// Names from `loads` are safe by construction - the parser could not have
1462/// produced them otherwise. These rules exist for the other path: a Document
1463/// built in code whose names never had to survive a parse.
1464///
1465/// Each forbidden character is one the reader gives a meaning to:
1466///
1467/// - space, tab: the field header is whitespace-separated, so `first name`
1468///   reads back as two fields
1469/// - newline, CR: ends the header line
1470/// - `:`: separates a field name from its type (`id:int`)
1471/// - `|`: the ISONL field delimiter
1472/// - `#`: a comment, but only line-initial - `a#b` is unambiguous and stays
1473///   legal, so that is a prefix rule below rather than a character listed here
1474///
1475/// `.` is deliberately absent for field names: dotted keys address nested
1476/// values and flat keys containing dots round-trip correctly.
1477const NAME_FORBIDDEN: [char; 4] = [' ', '\t', '\n', '\r'];
1478const FIELD_FORBIDDEN: [char; 6] = [' ', '\t', '\n', '\r', ':', '|'];
1479
1480/// Name a character for an error message.
1481fn describe_char(c: char) -> String {
1482    match c {
1483        ' ' => "a space".to_string(),
1484        '\t' => "a tab".to_string(),
1485        '\n' => "a newline".to_string(),
1486        '\r' => "a carriage return".to_string(),
1487        other => format!("'{}'", other),
1488    }
1489}
1490
1491/// Reject a field name that cannot be written and read back unchanged.
1492fn validate_field_name(name: &str) -> Result<()> {
1493    if let Some(c) = name.chars().find(|c| FIELD_FORBIDDEN.contains(c)) {
1494        return Err(ISONError {
1495            message: format!(
1496                "field name '{}' contains {}, which has no unambiguous ISON encoding",
1497                name,
1498                describe_char(c)
1499            ),
1500            line: None,
1501        });
1502    }
1503    if name.starts_with('#') {
1504        return Err(ISONError {
1505            message: format!(
1506                "field name '{}' starts with '#', which begins a comment; '#' elsewhere in a name is fine",
1507                name
1508            ),
1509            line: None,
1510        });
1511    }
1512    if name.is_empty() {
1513        return Err(ISONError {
1514            message: "field name is empty".to_string(),
1515            line: None,
1516        });
1517    }
1518    Ok(())
1519}
1520
1521/// Reject a block header that cannot be written and read back unchanged.
1522fn validate_block_name(kind: &str, name: &str) -> Result<()> {
1523    for (label, value) in [("kind", kind), ("name", name)] {
1524        if let Some(c) = value.chars().find(|c| NAME_FORBIDDEN.contains(c)) {
1525            return Err(ISONError {
1526                message: format!(
1527                    "block {} '{}' contains {}, which has no unambiguous ISON encoding",
1528                    label,
1529                    value,
1530                    describe_char(c)
1531                ),
1532                line: None,
1533            });
1534        }
1535        if value.is_empty() {
1536            return Err(ISONError {
1537                message: format!("block {} is empty", label),
1538                line: None,
1539            });
1540        }
1541    }
1542    // The header splits on the first '.', so a dot in the kind would move the
1543    // boundary and rename the block. A dot in the name survives.
1544    if kind.contains('.') {
1545        return Err(ISONError {
1546            message: format!(
1547                "block kind '{}' contains '.', which separates kind from name",
1548                kind
1549            ),
1550            line: None,
1551        });
1552    }
1553    Ok(())
1554}
1555
1556/// Validate every name a block will emit.
1557fn validate_block_names(block: &Block) -> Result<()> {
1558    validate_block_name(&block.kind, &block.name)?;
1559    for field in &block.fields {
1560        validate_field_name(field)?;
1561    }
1562    Ok(())
1563}
1564
1565/// Characters a reference may not carry, per output form.
1566///
1567/// A reference emits as `:type:id` with no quoting. Every other value type
1568/// passes through the quoting rules, so a string holding a space is quoted and
1569/// survives; a reference has no such escape and the raw characters land in the
1570/// row. Whitespace therefore splits the row into extra columns, and a newline
1571/// ends it early - which truncates the reference silently.
1572///
1573/// Each form rejects exactly what it cannot parse, and nothing more. That is
1574/// what keeps the invariant that anything obtained by parsing can be written
1575/// back: a reference the reader could produce is always one the writer accepts.
1576///
1577/// `|` is deliberately absent from the ISON set: `:p:a|b` parses there and
1578/// reads back correctly, so refusing to write it would make a valid file
1579/// readable but not writable. ISONL cannot parse one, so it rejects it.
1580const REFERENCE_FORBIDDEN_ISON: [char; 4] = [' ', '\t', '\n', '\r'];
1581const REFERENCE_FORBIDDEN_ISONL: [char; 5] = [' ', '\t', '\n', '\r', '|'];
1582
1583/// Reject a reference that cannot be written and read back unchanged.
1584fn validate_reference(r: &Reference, forbidden: &[char]) -> Result<()> {
1585    let parts: [(&str, Option<&String>); 2] =
1586        [("id", Some(&r.id)), ("type", r.ref_type.as_ref())];
1587    for (label, value) in parts {
1588        let Some(value) = value else { continue };
1589        if let Some(c) = value.chars().find(|c| forbidden.contains(c)) {
1590            return Err(ISONError {
1591                message: format!(
1592                    "reference {} '{}' contains {}; a reference is written as ':type:id' with no quoting, so it has no unambiguous ISON encoding",
1593                    label,
1594                    value,
1595                    describe_char(c)
1596                ),
1597                line: None,
1598            });
1599        }
1600    }
1601    if r.id.is_empty() {
1602        return Err(ISONError {
1603            message: "reference id is empty".to_string(),
1604            line: None,
1605        });
1606    }
1607    Ok(())
1608}
1609
1610/// Check every reference a block will emit.
1611fn validate_row_references(block: &Block, forbidden: &[char]) -> Result<()> {
1612    for row in block.rows.iter().chain(block.summary_rows.iter()) {
1613        for value in row.values() {
1614            if let Value::Reference(r) = value {
1615                validate_reference(r, forbidden)?;
1616            }
1617        }
1618    }
1619    Ok(())
1620}
1621
1622/// Reject kind/name/fields that cannot survive an ISONL round-trip
1623fn validate_isonl_envelope(block: &Block) -> Result<()> {
1624    // The shared ISON name rules apply here too - a name unwritable in ISON is
1625    // unwritable in ISONL. ISONL then adds its own: the quote and backslash
1626    // that its value escaping gives meaning to.
1627    validate_block_names(block)?;
1628    validate_row_references(block, &REFERENCE_FORBIDDEN_ISONL)?;
1629
1630    for (label, value) in [("kind", &block.kind), ("name", &block.name)] {
1631        if value.is_empty() {
1632            return Err(ISONError {
1633                message: format!("ISONL block {} must be non-empty", label),
1634                line: None,
1635            });
1636        }
1637        if value.contains(ISONL_ENVELOPE_FORBIDDEN) {
1638            return Err(ISONError {
1639                message: format!(
1640                    "ISONL block {} '{}' contains characters that cannot be serialized (pipe, quote, backslash, or whitespace)",
1641                    label, value
1642                ),
1643                line: None,
1644            });
1645        }
1646    }
1647    if block.kind.contains('.') {
1648        return Err(ISONError {
1649            message: format!("ISONL block kind '{}' must not contain '.'", block.kind),
1650            line: None,
1651        });
1652    }
1653    if block.kind.starts_with('#') {
1654        return Err(ISONError {
1655            message: format!("ISONL block kind '{}' must not start with '#'", block.kind),
1656            line: None,
1657        });
1658    }
1659    for field in &block.fields {
1660        if field.is_empty() {
1661            return Err(ISONError {
1662                message: "ISONL field names must be non-empty".to_string(),
1663                line: None,
1664            });
1665        }
1666        if field.contains(ISONL_ENVELOPE_FORBIDDEN) {
1667            return Err(ISONError {
1668                message: format!(
1669                    "ISONL field name '{}' contains characters that cannot be serialized (pipe, quote, backslash, or whitespace)",
1670                    field
1671                ),
1672                line: None,
1673            });
1674        }
1675    }
1676    Ok(())
1677}
1678
1679/// Parse ISONL format
1680pub fn parse_isonl(text: &str) -> Result<Document> {
1681    let mut doc = Document::new();
1682    let mut block_map: HashMap<String, usize> = HashMap::new();
1683
1684    for (line_num, line) in text.lines().enumerate() {
1685        let line = line.trim();
1686        if line.is_empty() || line.starts_with('#') {
1687            continue;
1688        }
1689
1690        let sections = split_isonl_sections(line);
1691        if sections.len() != 3 {
1692            return Err(ISONError {
1693                message: format!("Invalid ISONL line: {}", line),
1694                line: Some(line_num + 1),
1695            });
1696        }
1697
1698        let header = sections[0].as_str();
1699        let fields_part = sections[1].as_str();
1700        let values_part = sections[2].as_str();
1701
1702        let dot_index = header.find('.').ok_or_else(|| ISONError {
1703            message: format!("Invalid ISONL header: {}", header),
1704            line: Some(line_num + 1),
1705        })?;
1706
1707        let kind = &header[..dot_index];
1708        let name = &header[dot_index + 1..];
1709        let key = format!("{}.{}", kind, name);
1710
1711        let block_idx = if let Some(&idx) = block_map.get(&key) {
1712            idx
1713        } else {
1714            let mut block = Block::new(kind, name);
1715
1716            // Parse fields
1717            for f in fields_part.split_whitespace() {
1718                if let Some(colon_idx) = f.find(':') {
1719                    let field_name = f[..colon_idx].to_string();
1720                    let field_type = f[colon_idx + 1..].to_string();
1721                    block.fields.push(field_name.clone());
1722                    block.field_info.push(FieldInfo::with_type(field_name, field_type));
1723                } else {
1724                    block.fields.push(f.to_string());
1725                    block.field_info.push(FieldInfo::new(f));
1726                }
1727            }
1728
1729            let idx = doc.blocks.len();
1730            block_map.insert(key, idx);
1731            doc.blocks.push(block);
1732            idx
1733        };
1734
1735        // Parse values (quoted tokens keep their string type)
1736        let parser = Parser::new("");
1737        let mut values = tokenize_isonl_values(values_part);
1738
1739        // An unquoted token starting with '#' begins an inline comment
1740        let keep = strip_inline_comment(&values);
1741        values.truncate(keep);
1742
1743        let block = &doc.blocks[block_idx];
1744
1745        // More values than fields is an error, not a silent truncation
1746        check_extra_tokens(&values, block.fields.len(), Some(line_num + 1))?;
1747
1748        let mut row = Row::new();
1749        for (i, field) in block.fields.iter().enumerate() {
1750            let value = match values.get(i) {
1751                Some((token, true)) => Value::String(token.clone()),
1752                Some((token, false)) => parser.parse_value(token)?,
1753                // Missing trailing values pad with null
1754                None => Value::Null,
1755            };
1756            row.insert(field.clone(), value);
1757        }
1758
1759        doc.blocks[block_idx].rows.push(row);
1760    }
1761
1762    Ok(doc)
1763}
1764
1765/// Serialize to ISONL format
1766///
1767/// Returns an error if any block's kind, name, or field names contain
1768/// characters that cannot survive an ISONL round-trip (pipe, quote,
1769/// backslash, or whitespace; additionally `.` or a leading `#` in the kind).
1770pub fn dumps_isonl(doc: &Document) -> Result<String> {
1771    let mut lines = Vec::new();
1772
1773    for block in &doc.blocks {
1774        validate_isonl_envelope(block)?;
1775        let header = format!("{}.{}", block.kind, block.name);
1776        let fields: Vec<String> = block
1777            .field_info
1778            .iter()
1779            .map(|fi| {
1780                if let Some(ref ft) = fi.field_type {
1781                    format!("{}:{}", fi.name, ft)
1782                } else {
1783                    fi.name.clone()
1784                }
1785            })
1786            .collect();
1787        let fields_str = fields.join(" ");
1788
1789        for row in &block.rows {
1790            let values: Vec<String> = block
1791                .fields
1792                .iter()
1793                .map(|f| {
1794                    row.get(f)
1795                        .map(isonl_serialize_value)
1796                        .unwrap_or_else(|| "null".to_string())
1797                })
1798                .collect();
1799            lines.push(format!("{}|{}|{}", header, fields_str, values.join(" ")));
1800        }
1801    }
1802
1803    Ok(lines.join("\n"))
1804}
1805
1806// =============================================================================
1807// Public API
1808// =============================================================================
1809
1810/// Parse an ISON string into a Document
1811pub fn parse(text: &str) -> Result<Document> {
1812    Parser::new(text).parse()
1813}
1814
1815/// Parse an ISON string into a Document (alias for parse)
1816pub fn loads(text: &str) -> Result<Document> {
1817    parse(text)
1818}
1819
1820/// Serialize a Document to an ISON string
1821///
1822/// # Arguments
1823/// * `doc` - The document to serialize
1824/// * `align_columns` - Whether to align columns with padding (default: false for token efficiency)
1825pub fn dumps(doc: &Document, align_columns: bool) -> Result<String> {
1826    Serializer::new(align_columns).serialize(doc)
1827}
1828
1829/// Serialize a Document to an ISON string with custom delimiter
1830///
1831/// # Arguments
1832/// * `doc` - The document to serialize
1833/// * `align_columns` - Whether to align columns with padding
1834/// * `delimiter` - Column separator (default: " ", alternatives: ",")
1835pub fn dumps_with_delimiter(
1836    doc: &Document,
1837    align_columns: bool,
1838    delimiter: &str,
1839) -> Result<String> {
1840    Serializer::with_delimiter(align_columns, delimiter).serialize(doc)
1841}
1842
1843/// Serialize a Document to canonical ISON string.
1844///
1845/// Canonical form produces byte-identical output across all implementations
1846/// for the same logical data. Blocks are sorted ordinal-string (lexicographically)
1847/// by key (kind.name), rows within each block are sorted ordinal-string by the
1848/// first column value (conventionally 'id'), using single-space delimiter
1849/// and no alignment.
1850///
1851/// # Arguments
1852/// * `doc` - The document to serialize
1853///
1854/// # Returns
1855/// Canonical ISON formatted string (deterministic, sorted)
1856pub fn dumps_canonical(doc: &Document) -> Result<String> {
1857    CanonicalSerializer::new().serialize(doc)
1858}
1859
1860/// Serialize a Document to canonical ISONL string.
1861///
1862/// Canonical form produces byte-identical output across all implementations
1863/// for the same logical data. Blocks are sorted ordinal-string by key
1864/// (kind.name), rows within each block are sorted ordinal-string by the
1865/// first column value (conventionally 'id').
1866///
1867/// # Arguments
1868/// * `doc` - The document to serialize
1869///
1870/// # Returns
1871/// Result containing canonical ISONL formatted string, or error if envelope validation fails
1872pub fn dumps_canonical_isonl(doc: &Document) -> Result<String> {
1873    CanonicalSerializer::new().serialize_isonl(doc)
1874}
1875
1876/// Parse ISONL string (alias for parse_isonl)
1877pub fn loads_isonl(text: &str) -> Result<Document> {
1878    parse_isonl(text)
1879}
1880
1881/// Convert ISON text to ISONL text
1882pub fn ison_to_isonl(ison_text: &str) -> Result<String> {
1883    let doc = parse(ison_text)?;
1884    dumps_isonl(&doc)
1885}
1886
1887/// Convert ISONL text to ISON text
1888pub fn isonl_to_ison(isonl_text: &str) -> Result<String> {
1889    let doc = parse_isonl(isonl_text)?;
1890    dumps(&doc, false)
1891}
1892
1893/// Options for json_to_ison conversion
1894#[cfg(feature = "serde")]
1895#[derive(Debug, Clone)]
1896pub struct JsonToIsonOptions {
1897    /// Whether to flatten nested objects into separate tables (default: true)
1898    pub flatten: bool,
1899    /// Whether to align columns in output (default: false)
1900    pub align_columns: bool,
1901}
1902
1903#[cfg(feature = "serde")]
1904impl Default for JsonToIsonOptions {
1905    fn default() -> Self {
1906        Self {
1907            flatten: true,
1908            align_columns: false,
1909        }
1910    }
1911}
1912
1913/// Convert JSON to ISON format (requires serde feature)
1914///
1915/// Converts a JSON object where keys are block names and values are arrays of objects
1916/// into ISON format. Nested objects and arrays are flattened into separate tables
1917/// with references.
1918#[cfg(feature = "serde")]
1919pub fn json_to_ison(json_text: &str) -> Result<String> {
1920    json_to_ison_with_options(json_text, JsonToIsonOptions::default())
1921}
1922
1923/// Convert JSON to ISON format with options (requires serde feature)
1924#[cfg(feature = "serde")]
1925pub fn json_to_ison_with_options(json_text: &str, opts: JsonToIsonOptions) -> Result<String> {
1926    let json_value: serde_json::Value = serde_json::from_str(json_text)
1927        .map_err(|e| ISONError { message: format!("JSON parse error: {}", e), line: None })?;
1928
1929    let obj = json_value.as_object()
1930        .ok_or_else(|| ISONError { message: "JSON must be an object".to_string(), line: None })?;
1931
1932    let mut doc = Document::new();
1933    let mut extra_blocks: Vec<Block> = Vec::new();
1934    let mut ref_counter: i64 = 1;
1935
1936    // Helper to check if value is a nested object
1937    fn is_nested_object(val: &serde_json::Value) -> bool {
1938        val.is_object()
1939    }
1940
1941    // Helper to check if value is an array of objects
1942    fn is_array_of_objects(val: &serde_json::Value) -> bool {
1943        if let Some(arr) = val.as_array() {
1944            !arr.is_empty() && arr[0].is_object()
1945        } else {
1946            false
1947        }
1948    }
1949
1950    // Helper to check if value is an array of primitives
1951    fn is_array_of_primitives(val: &serde_json::Value) -> bool {
1952        if let Some(arr) = val.as_array() {
1953            arr.is_empty() || (!arr[0].is_object() && !arr[0].is_array())
1954        } else {
1955            false
1956        }
1957    }
1958
1959    // Helper to check if value is an array of arrays
1960    fn is_array_of_arrays(val: &serde_json::Value) -> bool {
1961        if let Some(arr) = val.as_array() {
1962            !arr.is_empty() && arr[0].is_array()
1963        } else {
1964            false
1965        }
1966    }
1967
1968    // Helper to convert JSON value to ISON Value
1969    fn json_to_value(val: &serde_json::Value) -> Value {
1970        match val {
1971            serde_json::Value::Null => Value::Null,
1972            serde_json::Value::Bool(b) => Value::Bool(*b),
1973            serde_json::Value::Number(n) => {
1974                if let Some(i) = n.as_i64() {
1975                    Value::Int(i)
1976                } else if let Some(f) = n.as_f64() {
1977                    Value::Float(f)
1978                } else {
1979                    Value::String(n.to_string())
1980                }
1981            }
1982            serde_json::Value::String(s) => {
1983                if s.starts_with(':') {
1984                    let parts: Vec<&str> = s[1..].splitn(2, ':').collect();
1985                    if parts.len() == 2 {
1986                        Value::Reference(Reference::with_type(parts[1], parts[0]))
1987                    } else {
1988                        Value::Reference(Reference::new(parts[0]))
1989                    }
1990                } else {
1991                    Value::String(s.clone())
1992                }
1993            }
1994            _ => Value::String(val.to_string()),
1995        }
1996    }
1997
1998    for (block_name, block_value) in obj {
1999        if let Some(arr) = block_value.as_array() {
2000            // Handle array of arrays
2001            if is_array_of_arrays(block_value) {
2002                let max_cols = arr.iter()
2003                    .filter_map(|r| r.as_array())
2004                    .map(|a| a.len())
2005                    .max()
2006                    .unwrap_or(0);
2007
2008                let fields: Vec<String> = (1..=max_cols).map(|i| format!("col{}", i)).collect();
2009                let field_info: Vec<FieldInfo> = fields.iter()
2010                    .map(|f| FieldInfo::new(f))
2011                    .collect();
2012
2013                let mut rows = Vec::new();
2014                for item in arr {
2015                    if let Some(inner_arr) = item.as_array() {
2016                        let mut row = Row::new();
2017                        for (i, field) in fields.iter().enumerate() {
2018                            if i < inner_arr.len() {
2019                                row.insert(field.clone(), json_to_value(&inner_arr[i]));
2020                            } else {
2021                                row.insert(field.clone(), Value::Null);
2022                            }
2023                        }
2024                        rows.push(row);
2025                    }
2026                }
2027
2028                doc.blocks.push(Block {
2029                    kind: "table".to_string(),
2030                    name: block_name.clone(),
2031                    fields,
2032                    field_info,
2033                    rows,
2034                    summary_rows: vec![],
2035                });
2036                continue;
2037            }
2038
2039            // Handle array of objects
2040            if arr.is_empty() {
2041                continue;
2042            }
2043
2044            if !arr[0].is_object() {
2045                // Array of primitives at top level
2046                let fields = vec!["value".to_string()];
2047                let field_info = vec![FieldInfo::new("value")];
2048                let rows: Vec<Row> = arr.iter()
2049                    .map(|v| {
2050                        let mut row = Row::new();
2051                        row.insert("value".to_string(), json_to_value(v));
2052                        row
2053                    })
2054                    .collect();
2055
2056                doc.blocks.push(Block {
2057                    kind: "table".to_string(),
2058                    name: block_name.clone(),
2059                    fields,
2060                    field_info,
2061                    rows,
2062                    summary_rows: vec![],
2063                });
2064                continue;
2065            }
2066
2067            // Collect all fields from all objects
2068            let mut field_set: Vec<String> = Vec::new();
2069            let mut rows = Vec::new();
2070
2071            for item in arr {
2072                if let Some(item_obj) = item.as_object() {
2073                    // Determine row ID
2074                    let row_id: i64 = if let Some(id_val) = item_obj.get("id") {
2075                        id_val.as_i64().unwrap_or_else(|| {
2076                            let id = ref_counter;
2077                            ref_counter += 1;
2078                            id
2079                        })
2080                    } else {
2081                        let id = ref_counter;
2082                        ref_counter += 1;
2083                        id
2084                    };
2085
2086                    let parent_ref = Reference::new(row_id.to_string());
2087
2088                    let mut row = Row::new();
2089                    for (key, val) in item_obj {
2090                        if opts.flatten && (is_nested_object(val) || is_array_of_objects(val) || is_array_of_primitives(val)) {
2091                            let nested_name = format!("{}_{}", block_name, key);
2092
2093                            if is_nested_object(val) {
2094                                // Nested object - create separate table
2095                                if let Some(nested_obj) = val.as_object() {
2096                                    let parent_id_field = format!("{}_id", block_name);
2097                                    let mut nested_fields = vec![parent_id_field.clone()];
2098                                    let mut nested_row = Row::new();
2099                                    nested_row.insert(parent_id_field.clone(), Value::Reference(parent_ref.clone()));
2100
2101                                    for (nk, nv) in nested_obj {
2102                                        if !is_nested_object(nv) && !is_array_of_objects(nv) && !is_array_of_primitives(nv) {
2103                                            nested_row.insert(nk.clone(), json_to_value(nv));
2104                                            if !nested_fields.contains(nk) {
2105                                                nested_fields.push(nk.clone());
2106                                            }
2107                                        }
2108                                    }
2109
2110                                    // Add to extra blocks
2111                                    if nested_row.len() > 1 {
2112                                        if let Some(existing) = extra_blocks.iter_mut().find(|b| b.name == nested_name) {
2113                                            for f in &nested_fields {
2114                                                if !existing.fields.contains(f) {
2115                                                    existing.fields.push(f.clone());
2116                                                    existing.field_info.push(FieldInfo::new(f));
2117                                                }
2118                                            }
2119                                            existing.rows.push(nested_row);
2120                                        } else {
2121                                            let field_info = nested_fields.iter().map(|f| FieldInfo::new(f)).collect();
2122                                            extra_blocks.push(Block {
2123                                                kind: "table".to_string(),
2124                                                name: nested_name.clone(),
2125                                                fields: nested_fields,
2126                                                field_info,
2127                                                rows: vec![nested_row],
2128                                                summary_rows: vec![],
2129                                            });
2130                                        }
2131                                    }
2132                                }
2133                            } else if is_array_of_objects(val) {
2134                                // Array of objects - create separate table
2135                                if let Some(arr) = val.as_array() {
2136                                    for arr_item in arr {
2137                                        if let Some(nested_obj) = arr_item.as_object() {
2138                                            let parent_id_field = format!("{}_id", block_name);
2139                                            let mut nested_fields = vec![parent_id_field.clone()];
2140                                            let mut nested_row = Row::new();
2141                                            nested_row.insert(parent_id_field.clone(), Value::Reference(parent_ref.clone()));
2142
2143                                            for (nk, nv) in nested_obj {
2144                                                if !is_nested_object(nv) && !is_array_of_objects(nv) && !is_array_of_primitives(nv) {
2145                                                    nested_row.insert(nk.clone(), json_to_value(nv));
2146                                                    if !nested_fields.contains(nk) {
2147                                                        nested_fields.push(nk.clone());
2148                                                    }
2149                                                }
2150                                            }
2151
2152                                            if nested_row.len() > 1 {
2153                                                if let Some(existing) = extra_blocks.iter_mut().find(|b| b.name == nested_name) {
2154                                                    for f in &nested_fields {
2155                                                        if !existing.fields.contains(f) {
2156                                                            existing.fields.push(f.clone());
2157                                                            existing.field_info.push(FieldInfo::new(f));
2158                                                        }
2159                                                    }
2160                                                    existing.rows.push(nested_row);
2161                                                } else {
2162                                                    let field_info = nested_fields.iter().map(|f| FieldInfo::new(f)).collect();
2163                                                    extra_blocks.push(Block {
2164                                                        kind: "table".to_string(),
2165                                                        name: nested_name.clone(),
2166                                                        fields: nested_fields,
2167                                                        field_info,
2168                                                        rows: vec![nested_row],
2169                                                        summary_rows: vec![],
2170                                                    });
2171                                                }
2172                                            }
2173                                        }
2174                                    }
2175                                }
2176                            } else if is_array_of_primitives(val) {
2177                                // Array of primitives - create separate table with value column
2178                                if let Some(arr) = val.as_array() {
2179                                    let parent_id_field = format!("{}_id", block_name);
2180                                    for prim in arr {
2181                                        let mut nested_row = Row::new();
2182                                        nested_row.insert(parent_id_field.clone(), Value::Reference(parent_ref.clone()));
2183                                        nested_row.insert("value".to_string(), json_to_value(prim));
2184
2185                                        if let Some(existing) = extra_blocks.iter_mut().find(|b| b.name == nested_name) {
2186                                            existing.rows.push(nested_row);
2187                                        } else {
2188                                            let nested_fields = vec![parent_id_field.clone(), "value".to_string()];
2189                                            let field_info = nested_fields.iter().map(|f| FieldInfo::new(f)).collect();
2190                                            extra_blocks.push(Block {
2191                                                kind: "table".to_string(),
2192                                                name: nested_name.clone(),
2193                                                fields: nested_fields,
2194                                                field_info,
2195                                                rows: vec![nested_row],
2196                                                summary_rows: vec![],
2197                                            });
2198                                        }
2199                                    }
2200                                }
2201                            }
2202                            // Don't add this field to the main row
2203                        } else {
2204                            row.insert(key.clone(), json_to_value(val));
2205                            if !field_set.contains(key) {
2206                                field_set.push(key.clone());
2207                            }
2208                        }
2209                    }
2210                    rows.push(row);
2211                }
2212            }
2213
2214            let field_info: Vec<FieldInfo> = field_set.iter()
2215                .map(|f| FieldInfo::new(f))
2216                .collect();
2217
2218            doc.blocks.push(Block {
2219                kind: "table".to_string(),
2220                name: block_name.clone(),
2221                fields: field_set,
2222                field_info,
2223                rows,
2224                summary_rows: vec![],
2225            });
2226        } else if let Some(obj_value) = block_value.as_object() {
2227            // Single object
2228            let row_id = obj_value.get("id")
2229                .and_then(|v| v.as_i64())
2230                .map(|i| i.to_string())
2231                .unwrap_or_else(|| block_name.clone());
2232            let parent_ref = Reference::new(row_id);
2233
2234            let mut fields: Vec<String> = Vec::new();
2235            let mut row = Row::new();
2236
2237            for (key, val) in obj_value {
2238                if opts.flatten && (is_nested_object(val) || is_array_of_objects(val) || is_array_of_primitives(val)) {
2239                    // Handle nested structures similar to above
2240                    let nested_name = format!("{}_{}", block_name, key);
2241                    let parent_id_field = format!("{}_id", block_name);
2242
2243                    if is_nested_object(val) {
2244                        if let Some(nested_obj) = val.as_object() {
2245                            let mut nested_fields = vec![parent_id_field.clone()];
2246                            let mut nested_row = Row::new();
2247                            nested_row.insert(parent_id_field.clone(), Value::Reference(parent_ref.clone()));
2248
2249                            for (nk, nv) in nested_obj {
2250                                if !is_nested_object(nv) && !is_array_of_objects(nv) && !is_array_of_primitives(nv) {
2251                                    nested_row.insert(nk.clone(), json_to_value(nv));
2252                                    nested_fields.push(nk.clone());
2253                                }
2254                            }
2255
2256                            if nested_row.len() > 1 {
2257                                let field_info = nested_fields.iter().map(|f| FieldInfo::new(f)).collect();
2258                                extra_blocks.push(Block {
2259                                    kind: "table".to_string(),
2260                                    name: nested_name,
2261                                    fields: nested_fields,
2262                                    field_info,
2263                                    rows: vec![nested_row],
2264                                    summary_rows: vec![],
2265                                });
2266                            }
2267                        }
2268                    } else if is_array_of_primitives(val) {
2269                        if let Some(arr) = val.as_array() {
2270                            let nested_fields = vec![parent_id_field.clone(), "value".to_string()];
2271                            let field_info = nested_fields.iter().map(|f| FieldInfo::new(f)).collect();
2272                            let nested_rows: Vec<Row> = arr.iter().map(|prim| {
2273                                let mut r = Row::new();
2274                                r.insert(parent_id_field.clone(), Value::Reference(parent_ref.clone()));
2275                                r.insert("value".to_string(), json_to_value(prim));
2276                                r
2277                            }).collect();
2278
2279                            extra_blocks.push(Block {
2280                                kind: "table".to_string(),
2281                                name: nested_name,
2282                                fields: nested_fields,
2283                                field_info,
2284                                rows: nested_rows,
2285                                summary_rows: vec![],
2286                            });
2287                        }
2288                    }
2289                } else {
2290                    row.insert(key.clone(), json_to_value(val));
2291                    fields.push(key.clone());
2292                }
2293            }
2294
2295            let field_info: Vec<FieldInfo> = fields.iter()
2296                .map(|f| FieldInfo::new(f))
2297                .collect();
2298
2299            doc.blocks.push(Block {
2300                kind: "object".to_string(),
2301                name: block_name.clone(),
2302                fields,
2303                field_info,
2304                rows: vec![row],
2305                summary_rows: vec![],
2306            });
2307        }
2308    }
2309
2310    // Add extra blocks from flattened structures
2311    for block in extra_blocks {
2312        doc.blocks.push(block);
2313    }
2314
2315    dumps(&doc, opts.align_columns)
2316}
2317
2318/// Build a Document from JSON text (requires serde feature).
2319///
2320/// The other six implementations all expose a from_dict / FromDict / fromDict
2321/// entry point; Rust only had converters that went straight to a string. That
2322/// left the one construction path the parser cannot reach - a Document whose
2323/// names never had to survive a parse - untestable from outside the crate.
2324#[cfg(feature = "serde")]
2325pub fn json_to_document(json_text: &str) -> Result<Document> {
2326    // Use default options for JSON to ISON conversion
2327    let json_value: serde_json::Value = serde_json::from_str(json_text)
2328        .map_err(|e| ISONError { message: format!("JSON parse error: {}", e), line: None })?;
2329
2330    let obj = json_value.as_object()
2331        .ok_or_else(|| ISONError { message: "JSON must be an object".to_string(), line: None })?;
2332
2333    let mut doc = Document::new();
2334    let mut extra_blocks: Vec<Block> = Vec::new();
2335
2336
2337    // Helper to check if value is an array of arrays
2338    fn is_array_of_arrays(val: &serde_json::Value) -> bool {
2339        if let Some(arr) = val.as_array() {
2340            !arr.is_empty() && arr[0].is_array()
2341        } else {
2342            false
2343        }
2344    }
2345
2346    // Helper to convert JSON value to ISON Value
2347    fn json_to_value(val: &serde_json::Value) -> Value {
2348        match val {
2349            serde_json::Value::Null => Value::Null,
2350            serde_json::Value::Bool(b) => Value::Bool(*b),
2351            serde_json::Value::Number(n) => {
2352                if let Some(i) = n.as_i64() {
2353                    Value::Int(i)
2354                } else if let Some(f) = n.as_f64() {
2355                    Value::Float(f)
2356                } else {
2357                    Value::String(n.to_string())
2358                }
2359            }
2360            serde_json::Value::String(s) => {
2361                if s.starts_with(':') {
2362                    let parts: Vec<&str> = s[1..].splitn(2, ':').collect();
2363                    if parts.len() == 2 {
2364                        Value::Reference(Reference::with_type(parts[1], parts[0]))
2365                    } else {
2366                        Value::Reference(Reference::new(parts[0]))
2367                    }
2368                } else {
2369                    Value::String(s.clone())
2370                }
2371            }
2372            _ => Value::String(val.to_string()),
2373        }
2374    }
2375
2376    for (block_name, block_value) in obj {
2377        if let Some(arr) = block_value.as_array() {
2378            // Handle array of arrays
2379            if is_array_of_arrays(block_value) {
2380                let max_cols = arr.iter()
2381                    .filter_map(|r| r.as_array())
2382                    .map(|a| a.len())
2383                    .max()
2384                    .unwrap_or(0);
2385
2386                let fields: Vec<String> = (1..=max_cols).map(|i| format!("col{}", i)).collect();
2387                let field_info: Vec<FieldInfo> = fields.iter()
2388                    .map(|f| FieldInfo::new(f))
2389                    .collect();
2390
2391                let mut rows = Vec::new();
2392                for item in arr {
2393                    if let Some(inner_arr) = item.as_array() {
2394                        let mut row = Row::new();
2395                        for (i, field) in fields.iter().enumerate() {
2396                            if i < inner_arr.len() {
2397                                row.insert(field.clone(), json_to_value(&inner_arr[i]));
2398                            } else {
2399                                row.insert(field.clone(), Value::Null);
2400                            }
2401                        }
2402                        rows.push(row);
2403                    }
2404                }
2405
2406                doc.blocks.push(Block {
2407                    kind: "table".to_string(),
2408                    name: block_name.clone(),
2409                    fields,
2410                    field_info,
2411                    rows,
2412                    summary_rows: vec![],
2413                });
2414                continue;
2415            }
2416
2417            // Handle array of objects
2418            if arr.is_empty() {
2419                continue;
2420            }
2421
2422            if !arr[0].is_object() {
2423                // Array of primitives at top level
2424                let fields = vec!["value".to_string()];
2425                let field_info = vec![FieldInfo::new("value")];
2426                let rows: Vec<Row> = arr.iter()
2427                    .map(|v| {
2428                        let mut row = Row::new();
2429                        row.insert("value".to_string(), json_to_value(v));
2430                        row
2431                    })
2432                    .collect();
2433
2434                doc.blocks.push(Block {
2435                    kind: "table".to_string(),
2436                    name: block_name.clone(),
2437                    fields,
2438                    field_info,
2439                    rows,
2440                    summary_rows: vec![],
2441                });
2442                continue;
2443            }
2444
2445            // Collect all fields from all objects
2446            let mut field_set: Vec<String> = Vec::new();
2447            let mut rows = Vec::new();
2448
2449            for item in arr {
2450                if let Some(item_obj) = item.as_object() {
2451                    let mut row = Row::new();
2452                    for (key, val) in item_obj {
2453                        row.insert(key.clone(), json_to_value(val));
2454                        if !field_set.contains(key) {
2455                            field_set.push(key.clone());
2456                        }
2457                    }
2458                    rows.push(row);
2459                }
2460            }
2461
2462            let field_info: Vec<FieldInfo> = field_set.iter()
2463                .map(|f| FieldInfo::new(f))
2464                .collect();
2465
2466            doc.blocks.push(Block {
2467                kind: "table".to_string(),
2468                name: block_name.clone(),
2469                fields: field_set,
2470                field_info,
2471                rows,
2472                summary_rows: vec![],
2473            });
2474        } else if let Some(obj_value) = block_value.as_object() {
2475            // Single object
2476            let mut fields: Vec<String> = Vec::new();
2477            let mut row = Row::new();
2478
2479            for (key, val) in obj_value {
2480                row.insert(key.clone(), json_to_value(val));
2481                fields.push(key.clone());
2482            }
2483
2484            let field_info = fields.iter().map(|f| FieldInfo::new(f)).collect();
2485
2486            doc.blocks.push(Block {
2487                kind: "table".to_string(),
2488                name: block_name.clone(),
2489                fields,
2490                field_info,
2491                rows: vec![row],
2492                summary_rows: vec![],
2493            });
2494        }
2495    }
2496
2497    // Add extra blocks from flattened structures
2498    for block in extra_blocks {
2499        doc.blocks.push(block);
2500    }
2501
2502    Ok(doc)
2503}
2504
2505/// Convert JSON to canonical ISON format (requires serde feature)
2506#[cfg(feature = "serde")]
2507pub fn json_to_ison_canonical(json_text: &str) -> Result<String> {
2508    // Canonical ISON (field-sorted, row-sorted output)
2509    dumps_canonical(&json_to_document(json_text)?)
2510}
2511
2512/// Convert ISON to JSON format (requires serde feature)
2513#[cfg(feature = "serde")]
2514pub fn ison_to_json(ison_text: &str, pretty: bool) -> Result<String> {
2515    let doc = parse(ison_text)?;
2516    Ok(doc.to_json(pretty))
2517}
2518
2519#[cfg(test)]
2520mod tests {
2521    use super::*;
2522
2523    #[test]
2524    fn test_parse_simple_table() {
2525        let ison = r#"table.users
2526id name email
25271 Alice alice@example.com
25282 Bob bob@example.com"#;
2529
2530        let doc = parse(ison).unwrap();
2531        let users = doc.get("users").unwrap();
2532
2533        assert_eq!(users.kind, "table");
2534        assert_eq!(users.name, "users");
2535        assert_eq!(users.len(), 2);
2536        assert_eq!(users.fields, vec!["id", "name", "email"]);
2537
2538        assert_eq!(users[0].get("id").unwrap().as_int(), Some(1));
2539        assert_eq!(users[0].get("name").unwrap().as_str(), Some("Alice"));
2540    }
2541
2542    #[test]
2543    fn test_parse_references() {
2544        let ison = r#"table.orders
2545id user_id
25461 :42
25472 :user:101
25483 :MEMBER_OF:10"#;
2549
2550        let doc = parse(ison).unwrap();
2551        let orders = doc.get("orders").unwrap();
2552
2553        let ref1 = orders[0].get("user_id").unwrap().as_reference().unwrap();
2554        assert_eq!(ref1.id, "42");
2555        assert!(ref1.ref_type.is_none());
2556
2557        let ref2 = orders[1].get("user_id").unwrap().as_reference().unwrap();
2558        assert_eq!(ref2.id, "101");
2559        assert_eq!(ref2.ref_type, Some("user".to_string()));
2560        assert!(!ref2.is_relationship());
2561
2562        let ref3 = orders[2].get("user_id").unwrap().as_reference().unwrap();
2563        assert_eq!(ref3.id, "10");
2564        assert!(ref3.is_relationship());
2565    }
2566
2567    #[test]
2568    fn test_type_inference() {
2569        let ison = r#"table.test
2570int_val float_val bool_val null_val str_val
257142 3.14 true null hello"#;
2572
2573        let doc = parse(ison).unwrap();
2574        let test = doc.get("test").unwrap();
2575
2576        assert!(test[0].get("int_val").unwrap().is_int());
2577        assert!(test[0].get("float_val").unwrap().is_float());
2578        assert!(test[0].get("bool_val").unwrap().is_bool());
2579        assert!(test[0].get("null_val").unwrap().is_null());
2580        assert!(test[0].get("str_val").unwrap().is_string());
2581    }
2582
2583    #[test]
2584    fn test_roundtrip() {
2585        let original = r#"table.users
2586id name email
25871 Alice alice@example.com
25882 Bob bob@example.com"#;
2589
2590        let doc = parse(original).unwrap();
2591        let serialized = dumps(&doc, true).unwrap();
2592        let doc2 = parse(&serialized).unwrap();
2593
2594        assert_eq!(doc2.get("users").unwrap().len(), 2);
2595    }
2596
2597    #[test]
2598    fn test_isonl() {
2599        let isonl = "table.users|id name|1 Alice\ntable.users|id name|2 Bob";
2600
2601        let doc = parse_isonl(isonl).unwrap();
2602        let users = doc.get("users").unwrap();
2603
2604        assert_eq!(users.len(), 2);
2605        assert_eq!(users[0].get("name").unwrap().as_str(), Some("Alice"));
2606    }
2607
2608    #[test]
2609    fn test_dumps_with_delimiter() {
2610        let ison = r#"table.users
2611id name email
26121 Alice "alice@example.com"
26132 Bob "bob@example.com""#;
2614
2615        let doc = parse(ison).unwrap();
2616
2617        // Emails are emitted bare: only 'ident.ident' shapes could be misread
2618        // as a block header, so quoting every dotted value would waste tokens
2619        // and diverge from the other implementations.
2620        let comma_output = dumps_with_delimiter(&doc, false, ",").unwrap();
2621        assert!(comma_output.contains("id,name,email"));
2622        assert!(comma_output.contains("1,Alice,alice@example.com"));
2623
2624        // Test with default space delimiter
2625        let space_output = dumps_with_delimiter(&doc, false, " ").unwrap();
2626        assert!(space_output.contains("id name email"));
2627        assert!(space_output.contains("1 Alice alice@example.com"));
2628    }
2629
2630    #[test]
2631    fn test_version() {
2632        assert_eq!(VERSION, "1.0.2");
2633    }
2634
2635    #[test]
2636    fn test_json_to_ison() {
2637        let json = r#"{
2638            "users": [
2639                {"id": 1, "name": "Alice", "email": "alice@example.com"},
2640                {"id": 2, "name": "Bob", "email": "bob@example.com"}
2641            ]
2642        }"#;
2643
2644        let ison = json_to_ison(json).unwrap();
2645        assert!(ison.contains("table.users"));
2646
2647        // Parse it back to verify
2648        let doc = parse(&ison).unwrap();
2649        let users = doc.get("users").unwrap();
2650        assert_eq!(users.len(), 2);
2651    }
2652
2653    #[test]
2654    fn test_ison_to_json() {
2655        let ison = r#"table.users
2656id name email
26571 Alice alice@example.com
26582 Bob bob@example.com"#;
2659
2660        let json = ison_to_json(ison, false).unwrap();
2661        assert!(json.contains("Alice"));
2662        assert!(json.contains("Bob"));
2663
2664        // Verify it's valid JSON
2665        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
2666        assert!(parsed.get("users").is_some());
2667    }
2668
2669    fn make_string_block(kind: &str, name: &str, fields: &[&str], rows: Vec<Row>) -> Document {
2670        let mut block = Block::new(kind, name);
2671        for f in fields {
2672            block.fields.push(f.to_string());
2673            block.field_info.push(FieldInfo::new(*f));
2674        }
2675        block.rows = rows;
2676        let mut doc = Document::new();
2677        doc.blocks.push(block);
2678        doc
2679    }
2680
2681    #[test]
2682    fn test_isonl_escaping_integrity() {
2683        // Regression: delimiter/escape chars in values must survive a round-trip
2684        let adversarial: Vec<&str> = vec![
2685            "C:\\path\\", // trailing backslash used to desync quote tracking
2686            "\\",
2687            "a\\",
2688            "ends with backslash \\",
2689            "pipe|inside",
2690            "quote \" inside",
2691            "mix \\\" of both",
2692            "line1\nline2",
2693            "tab\there",
2694            "cr\rhere",
2695            "crlf\r\nend",
2696            "123",
2697            "true",
2698            ":ref",
2699            "",
2700            "\\|",
2701            " leading and trailing ",
2702        ];
2703
2704        let rows: Vec<Row> = adversarial
2705            .iter()
2706            .map(|s| {
2707                let mut row = Row::new();
2708                row.insert("v".to_string(), Value::String(s.to_string()));
2709                row
2710            })
2711            .collect();
2712        let doc = make_string_block("table", "adversarial", &["v"], rows);
2713
2714        let out = dumps_isonl(&doc).unwrap();
2715        let parsed = parse_isonl(&out).unwrap();
2716        let got: Vec<String> = parsed.blocks[0]
2717            .rows
2718            .iter()
2719            .map(|r| r.get("v").unwrap().as_str().unwrap().to_string())
2720            .collect();
2721        assert_eq!(got, adversarial, "round-trip corrupted values");
2722
2723        // Compound case: a quoted value ending in an escaped backslash followed
2724        // by a pipe-bearing value on the same line — the exact shape that
2725        // desynced quote tracking and corrupted section splitting
2726        let compound_rows: Vec<Row> = [("x \\", "y|z"), ("x\\", "y|z")]
2727            .iter()
2728            .map(|(a, b)| {
2729                let mut row = Row::new();
2730                row.insert("a".to_string(), Value::String(a.to_string()));
2731                row.insert("b".to_string(), Value::String(b.to_string()));
2732                row
2733            })
2734            .collect();
2735        let doc = make_string_block("table", "compound", &["a", "b"], compound_rows.clone());
2736
2737        let out = dumps_isonl(&doc).unwrap();
2738        let parsed = parse_isonl(&out).unwrap();
2739        assert_eq!(parsed.blocks[0].rows, compound_rows);
2740    }
2741
2742    #[test]
2743    fn test_isonl_roundtrip_property() {
2744        // Property test: random strings over a hostile alphabet must round-trip.
2745        // Deterministic LCG so no rand crate is needed.
2746        struct Lcg {
2747            state: u64,
2748        }
2749        impl Lcg {
2750            fn next_int(&mut self, lo: u64, hi: u64) -> u64 {
2751                self.state = (self.state * 1103515245 + 12345) % 2147483648;
2752                lo + (self.state % (hi - lo + 1))
2753            }
2754        }
2755
2756        let alphabet: [&str; 16] = [
2757            "a", "b", " ", "|", "\"", "\\", "\n", "\r", "\t", ".", ":", "#", "0", "1", "true",
2758            "null",
2759        ];
2760        let mut rng = Lcg { state: 20260713 };
2761
2762        for trial in 0..300 {
2763            let num_fields = rng.next_int(1, 4) as usize;
2764            let fields: Vec<String> = (0..num_fields).map(|i| format!("f{}", i)).collect();
2765            let field_refs: Vec<&str> = fields.iter().map(|f| f.as_str()).collect();
2766
2767            let num_rows = rng.next_int(1, 3) as usize;
2768            let mut rows: Vec<Row> = Vec::new();
2769            for _ in 0..num_rows {
2770                let mut row = Row::new();
2771                for f in &fields {
2772                    let len = rng.next_int(0, 12) as usize;
2773                    let mut s = String::new();
2774                    for _ in 0..len {
2775                        s.push_str(alphabet[rng.next_int(0, alphabet.len() as u64 - 1) as usize]);
2776                    }
2777                    row.insert(f.clone(), Value::String(s));
2778                }
2779                rows.push(row);
2780            }
2781
2782            let doc = make_string_block("table", "t", &field_refs, rows.clone());
2783            let out = dumps_isonl(&doc).unwrap();
2784            let parsed = parse_isonl(&out)
2785                .unwrap_or_else(|e| panic!("trial {}: parse failed ({}) for {:?}", trial, e, out));
2786            assert_eq!(
2787                parsed.blocks[0].rows, rows,
2788                "trial {}: {:?} -> {:?}",
2789                trial, rows, out
2790            );
2791        }
2792    }
2793
2794    #[test]
2795    fn test_isonl_envelope_validation() {
2796        // Envelope values that can't be serialized must be rejected, not corrupted
2797        fn make_doc(kind: &str, name: &str, fields: &[&str]) -> Document {
2798            let mut row = Row::new();
2799            for f in fields {
2800                row.insert(f.to_string(), Value::Int(1));
2801            }
2802            make_string_block(kind, name, fields, vec![row])
2803        }
2804
2805        let bad_cases: Vec<Document> = vec![
2806            make_doc("ta|ble", "t", &["id"]),
2807            make_doc("ta ble", "t", &["id"]),
2808            make_doc("t.able", "t", &["id"]),
2809            make_doc("#table", "t", &["id"]),
2810            make_doc("", "t", &["id"]),
2811            make_doc("table", "na|me", &["id"]),
2812            make_doc("table", "na\nme", &["id"]),
2813            make_doc("table", "na me", &["id"]),
2814            make_doc("table", "", &["id"]),
2815            make_doc("table", "t", &["bad field"]),
2816            make_doc("table", "t", &["bad|field"]),
2817            make_doc("table", "t", &[""]),
2818        ];
2819        for doc in &bad_cases {
2820            assert!(
2821                dumps_isonl(doc).is_err(),
2822                "should have rejected envelope kind={:?} name={:?} fields={:?}",
2823                doc.blocks[0].kind,
2824                doc.blocks[0].name,
2825                doc.blocks[0].fields
2826            );
2827        }
2828
2829        // Dots in the block NAME are legal — the parser splits on the first dot
2830        let doc = make_doc("table", "v1.2", &["id"]);
2831        let parsed = parse_isonl(&dumps_isonl(&doc).unwrap()).unwrap();
2832        assert_eq!(parsed.blocks[0].kind, "table");
2833        assert_eq!(parsed.blocks[0].name, "v1.2");
2834    }
2835
2836    #[test]
2837    fn test_extra_values_rejected() {
2838        // Regression: rows with more values than fields must error, not truncate
2839        let err = parse("table.t\na b\n1 2 3").unwrap_err();
2840        assert!(
2841            err.to_string().contains("3 values"),
2842            "unexpected error message: {}",
2843            err
2844        );
2845
2846        // A quoted token is data, never a comment — still an extra value
2847        assert!(parse("table.t\na b\n1 2 \"#not-a-comment\"").is_err());
2848
2849        // ISONL
2850        let err = parse_isonl("table.t|a b|1 2 3").unwrap_err();
2851        assert!(
2852            err.to_string().contains("3 values"),
2853            "unexpected error message: {}",
2854            err
2855        );
2856    }
2857
2858    #[test]
2859    fn test_inline_trailing_comment() {
2860        // An unquoted token starting with '#' begins an inline comment
2861        let doc = parse("table.t\na b\n1 2 # note ignored").unwrap();
2862        let row = &doc.blocks[0].rows[0];
2863        assert_eq!(row.get("a"), Some(&Value::Int(1)));
2864        assert_eq!(row.get("b"), Some(&Value::Int(2)));
2865        assert_eq!(row.len(), 2);
2866
2867        let doc = parse_isonl("table.t|a b|1 2 # note ignored").unwrap();
2868        let row = &doc.blocks[0].rows[0];
2869        assert_eq!(row.get("a"), Some(&Value::Int(1)));
2870        assert_eq!(row.get("b"), Some(&Value::Int(2)));
2871        assert_eq!(row.len(), 2);
2872
2873        // Comment mid-row: remaining fields are missing (null), not data
2874        let doc = parse("table.t\na b\n1 #tag").unwrap();
2875        let row = &doc.blocks[0].rows[0];
2876        assert_eq!(row.get("a"), Some(&Value::Int(1)));
2877        assert_eq!(row.get("b"), Some(&Value::Null));
2878
2879        // Quoted tokens are always data, never comments
2880        let doc = parse("table.t\na b\n1 \"#tag\"").unwrap();
2881        assert_eq!(
2882            doc.blocks[0].rows[0].get("b"),
2883            Some(&Value::String("#tag".to_string()))
2884        );
2885
2886        // Serializer quotes leading-'#' strings so they round-trip as data
2887        let mut row = Row::new();
2888        row.insert("a".to_string(), Value::String("#tag".to_string()));
2889        let doc = make_string_block("table", "t", &["a"], vec![row.clone()]);
2890        let parsed = parse(&dumps(&doc, false).unwrap()).unwrap();
2891        assert_eq!(parsed.blocks[0].rows, vec![row]);
2892
2893        // A quoted value containing '#' mid-string is data, not a comment
2894        let doc = parse("table.t\na b\n1 \"a#b\"").unwrap();
2895        assert_eq!(
2896            doc.blocks[0].rows[0].get("b"),
2897            Some(&Value::String("a#b".to_string()))
2898        );
2899
2900        // Regression pin: the old string-level '#' strip desynced on an
2901        // escaped backslash before a closing quote and then truncated a
2902        // quoted value containing '#'. Must survive a full round-trip now.
2903        let mut row = Row::new();
2904        row.insert("a".to_string(), Value::String("x\\".to_string()));
2905        row.insert("b".to_string(), Value::String("a #b".to_string()));
2906        let doc = make_string_block("table", "t", &["a", "b"], vec![row.clone()]);
2907        let out = dumps(&doc, false).unwrap();
2908        let parsed = parse(&out)
2909            .unwrap_or_else(|e| panic!("round-trip parse failed ({}) for {:?}", e, out));
2910        assert_eq!(parsed.blocks[0].rows, vec![row], "corrupted by {:?}", out);
2911    }
2912
2913    #[test]
2914    fn test_ison_roundtrip_property() {
2915        // Header-shaped string values ('ident.ident', e.g. "a.true" or
2916        // "object.config") must be quoted by the serializer, otherwise a
2917        // single-field row line is re-parsed as a NEW block header and the
2918        // round-trip splits the block.
2919        let header_shaped: Vec<Row> = ["a.true", "object.config"]
2920            .iter()
2921            .map(|s| {
2922                let mut row = Row::new();
2923                row.insert("v".to_string(), Value::String(s.to_string()));
2924                row
2925            })
2926            .collect();
2927        let doc = make_string_block("table", "t", &["v"], header_shaped.clone());
2928        let out = dumps(&doc, false).unwrap();
2929        let parsed = parse(&out)
2930            .unwrap_or_else(|e| panic!("parse failed ({}) for {:?}", e, out));
2931        assert_eq!(
2932            parsed.blocks.len(),
2933            1,
2934            "header-shaped value split the block: {:?}",
2935            out
2936        );
2937        assert_eq!(parsed.blocks[0].rows, header_shaped, "corrupted by {:?}", out);
2938
2939        // Regular-format twin of test_isonl_roundtrip_property: random
2940        // strings over a hostile alphabet must round-trip through
2941        // dumps/parse. Deterministic LCG so no rand crate is needed.
2942        struct Lcg {
2943            state: u64,
2944        }
2945        impl Lcg {
2946            fn next_int(&mut self, lo: u64, hi: u64) -> u64 {
2947                self.state = (self.state * 1103515245 + 12345) % 2147483648;
2948                lo + (self.state % (hi - lo + 1))
2949            }
2950        }
2951
2952        let alphabet: [&str; 16] = [
2953            "a", "b", " ", "|", "\"", "\\", "\n", "\r", "\t", ".", ":", "#", "0", "1", "true",
2954            "null",
2955        ];
2956        let mut rng = Lcg { state: 20260713 };
2957
2958        for trial in 0..300 {
2959            let num_fields = rng.next_int(1, 4) as usize;
2960            let fields: Vec<String> = (0..num_fields).map(|i| format!("f{}", i)).collect();
2961            let field_refs: Vec<&str> = fields.iter().map(|f| f.as_str()).collect();
2962
2963            let num_rows = rng.next_int(1, 3) as usize;
2964            let mut rows: Vec<Row> = Vec::new();
2965            for _ in 0..num_rows {
2966                let mut row = Row::new();
2967                for f in &fields {
2968                    let len = rng.next_int(0, 12) as usize;
2969                    let mut s = String::new();
2970                    for _ in 0..len {
2971                        s.push_str(alphabet[rng.next_int(0, alphabet.len() as u64 - 1) as usize]);
2972                    }
2973                    row.insert(f.clone(), Value::String(s));
2974                }
2975                rows.push(row);
2976            }
2977
2978            let doc = make_string_block("table", "t", &field_refs, rows.clone());
2979            let out = dumps(&doc, false).unwrap();
2980            let parsed = parse(&out)
2981                .unwrap_or_else(|e| panic!("trial {}: parse failed ({}) for {:?}", trial, e, out));
2982            assert_eq!(
2983                parsed.blocks[0].rows, rows,
2984                "trial {}: {:?} -> {:?}",
2985                trial, rows, out
2986            );
2987        }
2988    }
2989
2990    // ==========================================================================
2991    // Canonical Serialization Tests (ISONCS)
2992    // ==========================================================================
2993
2994    #[test]
2995    fn test_canonical_blocks_sorted() {
2996        // Blocks should be sorted ordinal-string by kind.name
2997        let mut doc = Document::new();
2998
2999        let mut users_block = Block::new("table", "users");
3000        users_block.fields = vec!["id".to_string(), "name".to_string()];
3001        let mut users_row = Row::new();
3002        users_row.insert("id".to_string(), Value::String("2".to_string()));
3003        users_row.insert("name".to_string(), Value::String("Bob".to_string()));
3004        users_block.rows.push(users_row);
3005        doc.blocks.push(users_block);
3006
3007        let mut active_block = Block::new("table", "active_users");
3008        active_block.fields = vec!["id".to_string(), "name".to_string()];
3009        let mut active_row = Row::new();
3010        active_row.insert("id".to_string(), Value::String("1".to_string()));
3011        active_row.insert("name".to_string(), Value::String("Alice".to_string()));
3012        active_block.rows.push(active_row);
3013        doc.blocks.push(active_block);
3014
3015        let mut zulu_block = Block::new("table", "zulu");
3016        zulu_block.fields = vec!["id".to_string(), "name".to_string()];
3017        let mut zulu_row = Row::new();
3018        zulu_row.insert("id".to_string(), Value::String("3".to_string()));
3019        zulu_row.insert("name".to_string(), Value::String("Charlie".to_string()));
3020        zulu_block.rows.push(zulu_row);
3021        doc.blocks.push(zulu_block);
3022
3023        let canonical = dumps_canonical(&doc).unwrap();
3024
3025        // Blocks should be in ordinal order: table.active_users < table.users < table.zulu
3026        assert!(canonical.find("table.active_users").unwrap() < canonical.find("table.users").unwrap());
3027        assert!(canonical.find("table.users").unwrap() < canonical.find("table.zulu").unwrap());
3028    }
3029
3030    #[test]
3031    fn test_canonical_rows_sorted_by_key() {
3032        // Rows should be sorted ordinal-string by first column value
3033        let mut doc = Document::new();
3034        let mut block = Block::new("table", "items");
3035        block.fields = vec!["id".to_string(), "name".to_string()];
3036
3037        let mut row1 = Row::new();
3038        row1.insert("id".to_string(), Value::String("10".to_string()));
3039        row1.insert("name".to_string(), Value::String("ten".to_string()));
3040        block.rows.push(row1);
3041
3042        let mut row2 = Row::new();
3043        row2.insert("id".to_string(), Value::String("2".to_string()));
3044        row2.insert("name".to_string(), Value::String("two".to_string()));
3045        block.rows.push(row2);
3046
3047        let mut row3 = Row::new();
3048        row3.insert("id".to_string(), Value::String("1".to_string()));
3049        row3.insert("name".to_string(), Value::String("one".to_string()));
3050        block.rows.push(row3);
3051
3052        doc.blocks.push(block);
3053
3054        let canonical = dumps_canonical(&doc).unwrap();
3055        let lines: Vec<&str> = canonical.split('\n').collect();
3056
3057        // Find data lines (skip header and field line)
3058        let data_lines: Vec<&str> = lines.iter()
3059            .filter(|l| !l.contains("table.") && *l != &"id name" && !l.is_empty())
3060            .copied()
3061            .collect();
3062
3063        // Ordinal sort: "1" < "10" < "2"
3064        assert_eq!(data_lines[0], "\"1\" one");
3065        assert_eq!(data_lines[1], "\"10\" ten");
3066        assert_eq!(data_lines[2], "\"2\" two");
3067    }
3068
3069    #[test]
3070    fn test_canonical_null_keys_sort_last() {
3071        // Rows with null in the key column should sort to the end
3072        let mut doc = Document::new();
3073        let mut block = Block::new("table", "items");
3074        block.fields = vec!["id".to_string(), "name".to_string()];
3075
3076        let mut row1 = Row::new();
3077        row1.insert("id".to_string(), Value::String("2".to_string()));
3078        row1.insert("name".to_string(), Value::String("two".to_string()));
3079        block.rows.push(row1);
3080
3081        let mut row2 = Row::new();
3082        row2.insert("id".to_string(), Value::Null);
3083        row2.insert("name".to_string(), Value::String("orphan".to_string()));
3084        block.rows.push(row2);
3085
3086        let mut row3 = Row::new();
3087        row3.insert("id".to_string(), Value::String("1".to_string()));
3088        row3.insert("name".to_string(), Value::String("one".to_string()));
3089        block.rows.push(row3);
3090
3091        doc.blocks.push(block);
3092
3093        let canonical = dumps_canonical(&doc).unwrap();
3094        let lines: Vec<&str> = canonical.split('\n').collect();
3095
3096        // Find data lines
3097        let data_lines: Vec<&str> = lines.iter()
3098            .filter(|l| !l.contains("table.") && *l != &"id name" && !l.is_empty())
3099            .copied()
3100            .collect();
3101
3102        // Rows with values come first, null keys last
3103        assert_eq!(data_lines[0], "\"1\" one");
3104        assert_eq!(data_lines[1], "\"2\" two");
3105        assert_eq!(data_lines[2], "null orphan");
3106    }
3107
3108    #[test]
3109    fn test_canonical_idempotent() {
3110        // Canonical serialization is idempotent
3111        let mut doc = Document::new();
3112        let mut block = Block::new("table", "users");
3113        block.fields = vec!["id".to_string(), "name".to_string()];
3114
3115        let mut row1 = Row::new();
3116        row1.insert("id".to_string(), Value::String("2".to_string()));
3117        row1.insert("name".to_string(), Value::String("Bob".to_string()));
3118        block.rows.push(row1);
3119
3120        let mut row2 = Row::new();
3121        row2.insert("id".to_string(), Value::String("1".to_string()));
3122        row2.insert("name".to_string(), Value::String("Alice".to_string()));
3123        block.rows.push(row2);
3124
3125        doc.blocks.push(block);
3126
3127        let canonical1 = dumps_canonical(&doc).unwrap();
3128        let parsed = parse(&canonical1).unwrap();
3129        let canonical2 = dumps_canonical(&parsed).unwrap();
3130
3131        assert_eq!(canonical1, canonical2);
3132    }
3133
3134    #[test]
3135    fn test_canonical_no_alignment() {
3136        // Canonical output should use single space, no alignment padding
3137        let mut doc = Document::new();
3138        let mut block = Block::new("table", "data");
3139        block.fields = vec!["short".to_string(), "very_long_name".to_string()];
3140
3141        let mut row = Row::new();
3142        row.insert("short".to_string(), Value::String("a".to_string()));
3143        row.insert("very_long_name".to_string(), Value::String("b".to_string()));
3144        block.rows.push(row);
3145
3146        doc.blocks.push(block);
3147
3148        let canonical = dumps_canonical(&doc).unwrap();
3149
3150        // Should be single space between columns, not padded
3151        assert!(canonical.contains("short very_long_name"));
3152        assert!(canonical.contains("a b"));
3153        // No padding after 'a' (would have extra spaces for alignment)
3154        assert!(!canonical.contains("a  "));
3155    }
3156
3157    #[test]
3158    fn test_canonical_with_references() {
3159        // Canonical serialization should preserve references and sort rows
3160        let mut doc = Document::new();
3161        let mut block = Block::new("table", "edges");
3162        block.fields = vec!["source".to_string(), "target".to_string()];
3163
3164        let mut row1 = Row::new();
3165        row1.insert("source".to_string(), Value::Reference(Reference::new("2")));
3166        row1.insert("target".to_string(), Value::Reference(Reference::new("b")));
3167        block.rows.push(row1);
3168
3169        let mut row2 = Row::new();
3170        row2.insert("source".to_string(), Value::Reference(Reference::new("1")));
3171        row2.insert("target".to_string(), Value::Reference(Reference::new("a")));
3172        block.rows.push(row2);
3173
3174        doc.blocks.push(block);
3175
3176        let canonical = dumps_canonical(&doc).unwrap();
3177        let lines: Vec<&str> = canonical.split('\n').collect();
3178
3179        // Find data lines that start with ':'
3180        let data_lines: Vec<&str> = lines.iter()
3181            .filter(|l| l.starts_with(':'))
3182            .copied()
3183            .collect();
3184
3185        // Rows should be sorted by first column: :1 < :2
3186        assert_eq!(data_lines[0], ":1 :a");
3187        assert_eq!(data_lines[1], ":2 :b");
3188    }
3189
3190    #[test]
3191    fn test_canonical_golden_fixture() {
3192        // Golden fixture: a standard document serialized to canonical form.
3193        // This fixture is used for cross-implementation byte-identity verification.
3194
3195        let mut doc = Document::new();
3196
3197        // Edges block (added first, but should sort before users alphabetically)
3198        let mut edges = Block::new("table", "edges");
3199        edges.fields = vec!["source".to_string(), "target".to_string()];
3200
3201        let mut edge1 = Row::new();
3202        edge1.insert("source".to_string(), Value::Reference(Reference::new("2")));
3203        edge1.insert("target".to_string(), Value::Reference(Reference::new("1")));
3204        edges.rows.push(edge1);
3205
3206        let mut edge2 = Row::new();
3207        edge2.insert("source".to_string(), Value::Reference(Reference::new("1")));
3208        edge2.insert("target".to_string(), Value::Reference(Reference::new("3")));
3209        edges.rows.push(edge2);
3210
3211        doc.blocks.push(edges);
3212
3213        // Users block
3214        let mut users = Block::new("table", "users");
3215        users.fields = vec!["id".to_string(), "name".to_string(), "active".to_string()];
3216
3217        let mut user1 = Row::new();
3218        user1.insert("id".to_string(), Value::String("2".to_string()));
3219        user1.insert("name".to_string(), Value::String("Bob".to_string()));
3220        user1.insert("active".to_string(), Value::Bool(true));
3221        users.rows.push(user1);
3222
3223        let mut user2 = Row::new();
3224        user2.insert("id".to_string(), Value::String("1".to_string()));
3225        user2.insert("name".to_string(), Value::String("Alice".to_string()));
3226        user2.insert("active".to_string(), Value::Bool(true));
3227        users.rows.push(user2);
3228
3229        let mut user3 = Row::new();
3230        user3.insert("id".to_string(), Value::String("3".to_string()));
3231        user3.insert("name".to_string(), Value::String("Charlie".to_string()));
3232        user3.insert("active".to_string(), Value::Bool(false));
3233        users.rows.push(user3);
3234
3235        doc.blocks.push(users);
3236
3237        let canonical = dumps_canonical(&doc).unwrap();
3238
3239        // Expected order: blocks sorted (edges < users), fields sorted canonically
3240        // (id first, then alphabetically by UTF-8 bytes), rows sorted by key
3241        let expected_lines = vec![
3242            "table.edges",
3243            "source target",
3244            ":1 :3",
3245            ":2 :1",
3246            "",
3247            "table.users",
3248            "id active name",
3249            "\"1\" true Alice",
3250            "\"2\" true Bob",
3251            "\"3\" false Charlie",
3252        ];
3253        let expected = expected_lines.join("\n");
3254
3255        assert_eq!(canonical, expected, "\nExpected:\n{}\n\nGot:\n{}", expected, canonical);
3256    }
3257
3258    #[test]
3259    fn test_canonical_isonl_blocks_sorted() {
3260        // ISONL canonical should also sort blocks
3261        let mut doc = Document::new();
3262
3263        let mut zebras = Block::new("table", "zebras");
3264        zebras.fields = vec!["id".to_string()];
3265        let mut row = Row::new();
3266        row.insert("id".to_string(), Value::String("1".to_string()));
3267        zebras.rows.push(row);
3268        doc.blocks.push(zebras);
3269
3270        let mut aardvarks = Block::new("table", "aardvarks");
3271        aardvarks.fields = vec!["id".to_string()];
3272        let mut row = Row::new();
3273        row.insert("id".to_string(), Value::String("2".to_string()));
3274        aardvarks.rows.push(row);
3275        doc.blocks.push(aardvarks);
3276
3277        let canonical_isonl = dumps_canonical_isonl(&doc).unwrap();
3278        let lines: Vec<&str> = canonical_isonl.split('\n').collect();
3279
3280        // First line should be aardvarks (alphabetically first)
3281        assert!(lines[0].contains("table.aardvarks"));
3282        assert!(lines[1].contains("table.zebras"));
3283    }
3284
3285    #[test]
3286    fn test_canonical_isonl_rows_sorted() {
3287        // ISONL canonical should sort rows by key
3288        let mut doc = Document::new();
3289        let mut block = Block::new("table", "items");
3290        block.fields = vec!["id".to_string(), "val".to_string()];
3291
3292        let mut row1 = Row::new();
3293        row1.insert("id".to_string(), Value::String("c".to_string()));
3294        row1.insert("val".to_string(), Value::String("three".to_string()));
3295        block.rows.push(row1);
3296
3297        let mut row2 = Row::new();
3298        row2.insert("id".to_string(), Value::String("a".to_string()));
3299        row2.insert("val".to_string(), Value::String("one".to_string()));
3300        block.rows.push(row2);
3301
3302        let mut row3 = Row::new();
3303        row3.insert("id".to_string(), Value::String("b".to_string()));
3304        row3.insert("val".to_string(), Value::String("two".to_string()));
3305        block.rows.push(row3);
3306
3307        doc.blocks.push(block);
3308
3309        let canonical_isonl = dumps_canonical_isonl(&doc).unwrap();
3310        let lines: Vec<&str> = canonical_isonl.split('\n').collect();
3311
3312        // ISONL format: header|fields|values per line
3313        // Should see a, b, c in order
3314        let idx_a = lines.iter().position(|l| l.contains("a one")).unwrap();
3315        let idx_b = lines.iter().position(|l| l.contains("b two")).unwrap();
3316        let idx_c = lines.iter().position(|l| l.contains("c three")).unwrap();
3317        assert!(idx_a < idx_b && idx_b < idx_c);
3318    }
3319
3320    #[test]
3321    fn test_canonical_empty_string_handling() {
3322        // Empty strings should be quoted in canonical output
3323        let mut doc = Document::new();
3324        let mut block = Block::new("table", "test");
3325        block.fields = vec!["id".to_string(), "name".to_string()];
3326
3327        let mut row1 = Row::new();
3328        row1.insert("id".to_string(), Value::String("1".to_string()));
3329        row1.insert("name".to_string(), Value::String("".to_string()));
3330        block.rows.push(row1);
3331
3332        let mut row2 = Row::new();
3333        row2.insert("id".to_string(), Value::String("2".to_string()));
3334        row2.insert("name".to_string(), Value::String("value".to_string()));
3335        block.rows.push(row2);
3336
3337        doc.blocks.push(block);
3338
3339        let canonical = dumps_canonical(&doc).unwrap();
3340
3341        // Empty string should be quoted
3342        assert!(canonical.contains("\"\""));
3343    }
3344
3345    #[test]
3346    fn test_canonical_field_info_preserved() {
3347        // Field type annotations should be preserved in canonical output
3348        let mut doc = Document::new();
3349        let mut block = Block::new("table", "typed");
3350        block.fields = vec!["id".to_string(), "count".to_string()];
3351        block.field_info = vec![
3352            FieldInfo::with_type("id", "string"),
3353            FieldInfo::with_type("count", "int"),
3354        ];
3355
3356        let mut row = Row::new();
3357        row.insert("id".to_string(), Value::String("1".to_string()));
3358        row.insert("count".to_string(), Value::Int(42));
3359        block.rows.push(row);
3360
3361        doc.blocks.push(block);
3362
3363        let canonical = dumps_canonical(&doc).unwrap();
3364
3365        assert!(canonical.contains("id:string count:int"));
3366    }
3367
3368    #[test]
3369    fn test_canonical_field_sort_golden_fixture() {
3370        // Golden fixture: validates field sorting by UTF-8 bytes across key test cases
3371        // especially the UTF-16 divergence case where A (0xEF) < 😀 (0xF0)
3372
3373        let mut doc = Document::new();
3374
3375        // Test 1: no_id (no id field, all sorted alphabetically)
3376        let mut no_id = Block::new("table", "no_id");
3377        no_id.fields = vec!["name".to_string(), "city".to_string(), "age".to_string()]; // scrambled order
3378        let mut row = Row::new();
3379        row.insert("name".to_string(), Value::String("Charlie".to_string()));
3380        row.insert("city".to_string(), Value::String("New York".to_string()));
3381        row.insert("age".to_string(), Value::Int(30));
3382        no_id.rows.push(row);
3383        doc.blocks.push(no_id);
3384
3385        // Test 2: scrambled (id first, then sorted: active < email < name < score)
3386        let mut scrambled = Block::new("table", "scrambled");
3387        scrambled.fields = vec![
3388            "score".to_string(),
3389            "active".to_string(),
3390            "id".to_string(),
3391            "email".to_string(),
3392            "name".to_string(),
3393        ]; // scrambled order
3394
3395        let mut row1 = Row::new();
3396        row1.insert("score".to_string(), Value::Float(95.5));
3397        row1.insert("active".to_string(), Value::Bool(true));
3398        row1.insert("id".to_string(), Value::Int(1));
3399        row1.insert("email".to_string(), Value::String("alice@example.com".to_string()));
3400        row1.insert("name".to_string(), Value::String("Alice".to_string()));
3401        scrambled.rows.push(row1);
3402
3403        let mut row2 = Row::new();
3404        row2.insert("score".to_string(), Value::Float(87.3));
3405        row2.insert("active".to_string(), Value::Bool(false));
3406        row2.insert("id".to_string(), Value::Int(2));
3407        row2.insert("email".to_string(), Value::String("bob@example.com".to_string()));
3408        row2.insert("name".to_string(), Value::String("Bob".to_string()));
3409        scrambled.rows.push(row2);
3410
3411        doc.blocks.push(scrambled);
3412
3413        // Test 3: UTF-16 divergence (CRITICAL: Afield (0xEF) < 😀field (0xF0))
3414        let mut utf16_div = Block::new("table", "utf16_divergence");
3415        utf16_div.fields = vec![
3416            "😀field".to_string(),
3417            "id".to_string(),
3418            "Afield".to_string(),
3419        ]; // reversed order
3420
3421        let mut row3 = Row::new();
3422        row3.insert("😀field".to_string(), Value::String("non-BMP emoji (U+1F600 starts 0xF0 in UTF-8)".to_string()));
3423        row3.insert("id".to_string(), Value::Int(101));
3424        row3.insert("Afield".to_string(), Value::String("fullwidth A (U+FF21 is 0xEF in UTF-8)".to_string()));
3425        utf16_div.rows.push(row3);
3426
3427        doc.blocks.push(utf16_div);
3428
3429        let canonical = dumps_canonical(&doc).unwrap();
3430
3431        // Verify field ordering: id comes first, then others sorted by UTF-8 bytes
3432        assert!(
3433            canonical.contains("table.no_id\nage city name"),
3434            "no_id: fields should be sorted as 'age city name'"
3435        );
3436
3437        assert!(
3438            canonical.contains("table.scrambled\nid active email name score"),
3439            "scrambled: fields should be sorted as 'id active email name score'"
3440        );
3441
3442        assert!(
3443            canonical.contains("table.utf16_divergence\nid Afield 😀field"),
3444            "utf16_divergence: CRITICAL - Afield (0xEF) should come before 😀field (0xF0)"
3445        );
3446    }
3447
3448    #[test]
3449    #[cfg(feature = "serde")]
3450    fn test_canonical_field_sort_json_golden_fixture() {
3451        // Load the JSON golden fixture and verify canonical output matches expected
3452        let json_input = r#"{
3453  "scrambled": [
3454    {
3455      "score": 95.5,
3456      "active": true,
3457      "id": 1,
3458      "email": "alice@example.com",
3459      "name": "Alice"
3460    },
3461    {
3462      "name": "Bob",
3463      "email": "bob@example.com",
3464      "score": 87.3,
3465      "id": 2,
3466      "active": false
3467    }
3468  ],
3469  "no_id": [
3470    {
3471      "city": "New York",
3472      "age": 30,
3473      "name": "Charlie"
3474    }
3475  ],
3476  "utf16_divergence": [
3477    {
3478      "id": 101,
3479      "😀field": "non-BMP emoji (U+1F600 starts 0xF0 in UTF-8)",
3480      "Afield": "fullwidth A (U+FF21 is 0xEF in UTF-8)"
3481    }
3482  ],
3483  "users_order_1": [
3484    {
3485      "id": 1001,
3486      "name": "David",
3487      "email": "david@example.com"
3488    }
3489  ],
3490  "users_order_2": [
3491    {
3492      "email": "eve@example.com",
3493      "name": "Eve",
3494      "id": 1002
3495    }
3496  ],
3497  "empty": [],
3498  "single_row": [
3499    {
3500      "id": 9999,
3501      "value": "only_one"
3502    }
3503  ]
3504}"#;
3505
3506        // Convert JSON to canonical ISON
3507        let canonical_result = json_to_ison_canonical(json_input);
3508        assert!(canonical_result.is_ok(), "JSON to canonical ISON conversion should succeed");
3509
3510        let canonical = canonical_result.unwrap();
3511
3512        // Verify critical field sorting cases:
3513
3514        // 1. no_id: all fields sorted (no 'id' field to prioritize)
3515        assert!(canonical.contains("table.no_id\nage city name"),
3516                "no_id: fields should be 'age city name' (alphabetically sorted by UTF-8 bytes)");
3517
3518        // 2. scrambled: id first, then others sorted
3519        assert!(canonical.contains("table.scrambled\nid active email name score"),
3520                "scrambled: fields should be 'id active email name score' (id first, then sorted)");
3521
3522        // 3. UTF-16 divergence: THE CRITICAL TEST
3523        // A (U+FF21) = EF BC A1 in UTF-8 (starts with 0xEF)
3524        // 😀 (U+1F600) = F0 9F 98 80 in UTF-8 (starts with 0xF0)
3525        // Since EF < F0, Afield should come before 😀field
3526        assert!(canonical.contains("table.utf16_divergence\nid Afield 😀field"),
3527                "UTF-16 DIVERGENCE: Afield (0xEF...) must come before 😀field (0xF0...) by UTF-8 byte order");
3528
3529        // 4. single_row: id first, then value
3530        assert!(canonical.contains("table.single_row\nid value"),
3531                "single_row: fields should be 'id value' (id first, then value)");
3532
3533        // 5. users_order_1 and users_order_2: both should have same canonical field order
3534        assert!(canonical.contains("table.users_order_1\nid email name"),
3535                "users_order_1: fields should be 'id email name'");
3536        assert!(canonical.contains("table.users_order_2\nid email name"),
3537                "users_order_2: fields should also be 'id email name' (field order is canonical, not input order)");
3538    }
3539}