1use std::collections::HashMap;
30use std::fmt;
31
32pub mod plugins;
34
35#[cfg(feature = "serde")]
36use serde::{Deserialize, Serialize};
37
38pub const VERSION: &str = "1.0.2";
39
40#[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#[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 pub fn new(id: impl Into<String>) -> Self {
79 Self {
80 id: id.into(),
81 ref_type: None,
82 }
83 }
84
85 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 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 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 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 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#[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
222pub type Row = HashMap<String, Value>;
224
225#[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#[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 pub fn len(&self) -> usize {
280 self.rows.len()
281 }
282
283 pub fn is_empty(&self) -> bool {
285 self.rows.is_empty()
286 }
287
288 pub fn get_row(&self, index: usize) -> Option<&Row> {
290 self.rows.get(index)
291 }
292
293 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 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#[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 pub fn get(&self, name: &str) -> Option<&Block> {
333 self.blocks.iter().find(|b| b.name == name)
334 }
335
336 pub fn get_mut(&mut self, name: &str) -> Option<&mut Block> {
338 self.blocks.iter_mut().find(|b| b.name == name)
339 }
340
341 pub fn has(&self, name: &str) -> bool {
343 self.blocks.iter().any(|b| b.name == name)
344 }
345
346 pub fn len(&self) -> usize {
348 self.blocks.len()
349 }
350
351 pub fn is_empty(&self) -> bool {
353 self.blocks.is_empty()
354 }
355
356 #[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
381struct 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 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 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 if line.is_empty() || Self::looks_like_header(&line) {
472 break;
473 }
474
475 self.read_line(); if line.starts_with('#') {
479 continue;
480 }
481
482 if line.trim() == "---" {
484 in_summary = true;
485 continue;
486 }
487
488 let mut values = self.tokenize_line(&line);
489
490 let keep = strip_inline_comment(&values);
492 values.truncate(keep);
493 if values.is_empty() {
494 continue;
495 }
496
497 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 Some((token, true)) => Value::String(token.clone()),
505 Some((token, false)) => self.parse_value(token)?,
506 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 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 while i < chars.len() && (chars[i] == ' ' || chars[i] == '\t') {
536 i += 1;
537 }
538
539 if i >= chars.len() {
540 break;
541 }
542
543 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 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 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; 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 if token == "null" || token == "~" {
618 return Ok(Value::Null);
619 }
620
621 if token == "true" {
623 return Ok(Value::Bool(true));
624 }
625 if token == "false" {
626 return Ok(Value::Bool(false));
627 }
628
629 if token.starts_with(':') {
631 return self.parse_reference(token);
632 }
633
634 if let Ok(i) = token.parse::<i64>() {
636 return Ok(Value::Int(i));
637 }
638
639 if let Ok(f) = token.parse::<f64>() {
641 return Ok(Value::Float(f));
642 }
643
644 Ok(Value::String(token.to_string()))
646 }
647
648 fn parse_reference(&self, token: &str) -> Result<Value> {
649 let content = &token[1..]; 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; }
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
734fn 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
750fn 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
771fn 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
786fn 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 lines.push(format!("{}.{}", block.kind, block.name));
826
827 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 let widths = if self.align_columns {
843 self.calculate_widths(block)
844 } else {
845 vec![]
846 };
847
848 for row in &block.rows {
850 lines.push(self.serialize_row(row, &block.fields, &widths));
851 }
852
853 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 let needs_quotes = s.contains(' ')
919 || s.contains('\t')
920 || s.contains('\n')
921 || s.contains('\r')
922 || s.contains('"')
923 || s.contains('\\')
924 || 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
953struct 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 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 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 other_fields.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
1003
1004 [id_fields, other_fields].concat()
1006 }
1007
1008 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 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 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 let sorted_fields = self.sort_fields_canonical(&block.fields);
1073
1074 let fields: Vec<String> = if !block.field_info.is_empty() {
1076 sorted_fields
1077 .iter()
1078 .map(|field_name| {
1079 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 let sorted_rows = self.sort_rows_by_key_canonical(block, &sorted_fields);
1098
1099 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 lines.push(format!("{}.{}", block.kind, block.name));
1123
1124 let sorted_fields = self.sort_fields_canonical(&block.fields);
1126
1127 let field_defs: Vec<String> = if !block.field_info.is_empty() {
1129 sorted_fields
1130 .iter()
1131 .map(|field_name| {
1132 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 let sorted_rows = self.sort_rows_by_key_canonical(block, &sorted_fields);
1151
1152 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 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 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 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 fn value_to_string(&self, value: &Value) -> String {
1282 match value {
1283 Value::Null => String::new(), 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
1293const ISONL_ENVELOPE_FORBIDDEN: &[char] = &['|', '"', '\\', ' ', '\t', '\n', '\r'];
1300
1301fn 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 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
1343fn 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 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 let mut result = String::new();
1367 i += 1; while i < chars.len() {
1369 let ch = chars[i];
1370 if ch == '"' {
1371 i += 1; 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 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
1410fn 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
1446fn 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
1458const NAME_FORBIDDEN: [char; 4] = [' ', '\t', '\n', '\r'];
1478const FIELD_FORBIDDEN: [char; 6] = [' ', '\t', '\n', '\r', ':', '|'];
1479
1480fn 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
1491fn 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
1521fn 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 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
1556fn 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
1565const REFERENCE_FORBIDDEN_ISON: [char; 4] = [' ', '\t', '\n', '\r'];
1581const REFERENCE_FORBIDDEN_ISONL: [char; 5] = [' ', '\t', '\n', '\r', '|'];
1582
1583fn 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
1610fn 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
1622fn validate_isonl_envelope(block: &Block) -> Result<()> {
1624 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
1679pub 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 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 let parser = Parser::new("");
1737 let mut values = tokenize_isonl_values(values_part);
1738
1739 let keep = strip_inline_comment(&values);
1741 values.truncate(keep);
1742
1743 let block = &doc.blocks[block_idx];
1744
1745 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 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
1765pub 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
1806pub fn parse(text: &str) -> Result<Document> {
1812 Parser::new(text).parse()
1813}
1814
1815pub fn loads(text: &str) -> Result<Document> {
1817 parse(text)
1818}
1819
1820pub fn dumps(doc: &Document, align_columns: bool) -> Result<String> {
1826 Serializer::new(align_columns).serialize(doc)
1827}
1828
1829pub 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
1843pub fn dumps_canonical(doc: &Document) -> Result<String> {
1857 CanonicalSerializer::new().serialize(doc)
1858}
1859
1860pub fn dumps_canonical_isonl(doc: &Document) -> Result<String> {
1873 CanonicalSerializer::new().serialize_isonl(doc)
1874}
1875
1876pub fn loads_isonl(text: &str) -> Result<Document> {
1878 parse_isonl(text)
1879}
1880
1881pub fn ison_to_isonl(ison_text: &str) -> Result<String> {
1883 let doc = parse(ison_text)?;
1884 dumps_isonl(&doc)
1885}
1886
1887pub fn isonl_to_ison(isonl_text: &str) -> Result<String> {
1889 let doc = parse_isonl(isonl_text)?;
1890 dumps(&doc, false)
1891}
1892
1893#[cfg(feature = "serde")]
1895#[derive(Debug, Clone)]
1896pub struct JsonToIsonOptions {
1897 pub flatten: bool,
1899 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#[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#[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 fn is_nested_object(val: &serde_json::Value) -> bool {
1938 val.is_object()
1939 }
1940
1941 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 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 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 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 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 if arr.is_empty() {
2041 continue;
2042 }
2043
2044 if !arr[0].is_object() {
2045 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 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 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 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 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 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 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 } 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 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 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 for block in extra_blocks {
2312 doc.blocks.push(block);
2313 }
2314
2315 dumps(&doc, opts.align_columns)
2316}
2317
2318#[cfg(feature = "serde")]
2325pub fn json_to_document(json_text: &str) -> Result<Document> {
2326 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 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 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 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 if arr.is_empty() {
2419 continue;
2420 }
2421
2422 if !arr[0].is_object() {
2423 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 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 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 for block in extra_blocks {
2499 doc.blocks.push(block);
2500 }
2501
2502 Ok(doc)
2503}
2504
2505#[cfg(feature = "serde")]
2507pub fn json_to_ison_canonical(json_text: &str) -> Result<String> {
2508 dumps_canonical(&json_to_document(json_text)?)
2510}
2511
2512#[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 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 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 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 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 let adversarial: Vec<&str> = vec![
2685 "C:\\path\\", "\\",
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 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 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 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 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 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 assert!(parse("table.t\na b\n1 2 \"#not-a-comment\"").is_err());
2848
2849 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 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 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 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 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 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 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 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 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 #[test]
2995 fn test_canonical_blocks_sorted() {
2996 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 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 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 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 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 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 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 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 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 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 assert!(canonical.contains("short very_long_name"));
3152 assert!(canonical.contains("a b"));
3153 assert!(!canonical.contains("a "));
3155 }
3156
3157 #[test]
3158 fn test_canonical_with_references() {
3159 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 let data_lines: Vec<&str> = lines.iter()
3181 .filter(|l| l.starts_with(':'))
3182 .copied()
3183 .collect();
3184
3185 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 let mut doc = Document::new();
3196
3197 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 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 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 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 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 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 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 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 assert!(canonical.contains("\"\""));
3343 }
3344
3345 #[test]
3346 fn test_canonical_field_info_preserved() {
3347 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 let mut doc = Document::new();
3374
3375 let mut no_id = Block::new("table", "no_id");
3377 no_id.fields = vec!["name".to_string(), "city".to_string(), "age".to_string()]; 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 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 ]; 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 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 ]; 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 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 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 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 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 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 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 assert!(canonical.contains("table.single_row\nid value"),
3531 "single_row: fields should be 'id value' (id first, then value)");
3532
3533 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}