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.1";
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() || (line.chars().next().map(|c| c.is_alphabetic()).unwrap_or(false)
472 && line.contains('.'))
473 {
474 break;
475 }
476
477 self.read_line(); if line.starts_with('#') {
481 continue;
482 }
483
484 if line.trim() == "---" {
486 in_summary = true;
487 continue;
488 }
489
490 let values = self.tokenize_line(&line);
491 if values.is_empty() {
492 break;
493 }
494
495 let mut row = Row::new();
496 for (i, field) in block.fields.iter().enumerate() {
497 if i < values.len() {
498 row.insert(field.clone(), self.parse_value(&values[i])?);
499 }
500 }
501
502 if in_summary {
503 block.summary_rows.push(row);
504 } else {
505 block.rows.push(row);
506 }
507 }
508
509 Ok(Some(block))
510 }
511
512 fn tokenize_line(&self, line: &str) -> Vec<String> {
513 let mut tokens = Vec::new();
514 let mut chars: Vec<char> = line.chars().collect();
515 let mut i = 0;
516
517 let mut in_quote = false;
519 let mut comment_start = None;
520 for (idx, &ch) in chars.iter().enumerate() {
521 if ch == '"' && (idx == 0 || chars[idx - 1] != '\\') {
522 in_quote = !in_quote;
523 } else if ch == '#' && !in_quote {
524 comment_start = Some(idx);
525 break;
526 }
527 }
528 if let Some(start) = comment_start {
529 chars.truncate(start);
530 }
531
532 while i < chars.len() {
533 while i < chars.len() && (chars[i] == ' ' || chars[i] == '\t') {
535 i += 1;
536 }
537
538 if i >= chars.len() {
539 break;
540 }
541
542 if chars[i] == '"' {
544 let (token, new_pos) = self.parse_quoted_string(&chars, i);
545 tokens.push(token);
546 i = new_pos;
547 } else {
548 let start = i;
550 while i < chars.len() && chars[i] != ' ' && chars[i] != '\t' {
551 i += 1;
552 }
553 tokens.push(chars[start..i].iter().collect());
554 }
555 }
556
557 tokens
558 }
559
560 fn parse_quoted_string(&self, chars: &[char], start: usize) -> (String, usize) {
561 let mut result = String::new();
562 let mut i = start + 1; while i < chars.len() {
565 if chars[i] == '\\' {
566 if i + 1 < chars.len() {
567 let next = chars[i + 1];
568 match next {
569 'n' => result.push('\n'),
570 't' => result.push('\t'),
571 'r' => result.push('\r'),
572 '\\' => result.push('\\'),
573 '"' => result.push('"'),
574 _ => result.push(next),
575 }
576 i += 2;
577 } else {
578 result.push('\\');
579 i += 1;
580 }
581 } else if chars[i] == '"' {
582 return (result, i + 1);
583 } else {
584 result.push(chars[i]);
585 i += 1;
586 }
587 }
588
589 (result, i)
590 }
591
592 fn parse_value(&self, token: &str) -> Result<Value> {
593 if token == "null" || token == "~" {
595 return Ok(Value::Null);
596 }
597
598 if token == "true" {
600 return Ok(Value::Bool(true));
601 }
602 if token == "false" {
603 return Ok(Value::Bool(false));
604 }
605
606 if token.starts_with(':') {
608 return self.parse_reference(token);
609 }
610
611 if let Ok(i) = token.parse::<i64>() {
613 return Ok(Value::Int(i));
614 }
615
616 if let Ok(f) = token.parse::<f64>() {
618 return Ok(Value::Float(f));
619 }
620
621 Ok(Value::String(token.to_string()))
623 }
624
625 fn parse_reference(&self, token: &str) -> Result<Value> {
626 let content = &token[1..]; let parts: Vec<&str> = content.split(':').collect();
628
629 match parts.len() {
630 1 => Ok(Value::Reference(Reference::new(parts[0]))),
631 2 => Ok(Value::Reference(Reference::with_type(parts[1], parts[0]))),
632 _ => Err(ISONError {
633 message: format!("Invalid reference: {}", token),
634 line: Some(self.line),
635 }),
636 }
637 }
638
639 fn read_line(&mut self) -> Option<String> {
640 if self.pos >= self.text.len() {
641 return None;
642 }
643
644 let start = self.pos;
645 while self.pos < self.text.len() && self.text.as_bytes()[self.pos] != b'\n' {
646 self.pos += 1;
647 }
648
649 let line = self.text[start..self.pos].trim().to_string();
650
651 if self.pos < self.text.len() {
652 self.pos += 1; }
654 self.line += 1;
655
656 Some(line)
657 }
658
659 fn peek_line(&self) -> Option<String> {
660 if self.pos >= self.text.len() {
661 return None;
662 }
663
664 let mut end = self.pos;
665 while end < self.text.len() && self.text.as_bytes()[end] != b'\n' {
666 end += 1;
667 }
668
669 Some(self.text[self.pos..end].trim().to_string())
670 }
671
672 fn skip_whitespace_and_comments(&mut self) {
673 while self.pos < self.text.len() {
674 let ch = self.text.as_bytes()[self.pos];
675 match ch {
676 b' ' | b'\t' | b'\r' => self.pos += 1,
677 b'\n' => {
678 self.pos += 1;
679 self.line += 1;
680 }
681 b'#' => {
682 while self.pos < self.text.len() && self.text.as_bytes()[self.pos] != b'\n' {
683 self.pos += 1;
684 }
685 }
686 _ => break,
687 }
688 }
689 }
690
691 fn skip_empty_lines(&mut self) {
692 while self.pos < self.text.len() {
693 let ch = self.text.as_bytes()[self.pos];
694 match ch {
695 b' ' | b'\t' | b'\r' => self.pos += 1,
696 b'\n' => {
697 self.pos += 1;
698 self.line += 1;
699 }
700 b'#' => {
701 while self.pos < self.text.len() && self.text.as_bytes()[self.pos] != b'\n' {
702 self.pos += 1;
703 }
704 }
705 _ => break,
706 }
707 }
708 }
709}
710
711struct Serializer {
716 align_columns: bool,
717 delimiter: String,
718}
719
720impl Serializer {
721 fn new(align_columns: bool) -> Self {
722 Self { align_columns, delimiter: " ".to_string() }
723 }
724
725 fn with_delimiter(align_columns: bool, delimiter: &str) -> Self {
726 Self { align_columns, delimiter: delimiter.to_string() }
727 }
728
729 fn serialize(&self, doc: &Document) -> String {
730 let parts: Vec<String> = doc.blocks.iter().map(|b| self.serialize_block(b)).collect();
731 parts.join("\n\n")
732 }
733
734 fn serialize_block(&self, block: &Block) -> String {
735 let mut lines = Vec::new();
736
737 lines.push(format!("{}.{}", block.kind, block.name));
739
740 let field_defs: Vec<String> = block
742 .field_info
743 .iter()
744 .map(|fi| {
745 if let Some(ref ft) = fi.field_type {
746 format!("{}:{}", fi.name, ft)
747 } else {
748 fi.name.clone()
749 }
750 })
751 .collect();
752 lines.push(field_defs.join(&self.delimiter));
753
754 let widths = if self.align_columns {
756 self.calculate_widths(block)
757 } else {
758 vec![]
759 };
760
761 for row in &block.rows {
763 lines.push(self.serialize_row(row, &block.fields, &widths));
764 }
765
766 if !block.summary_rows.is_empty() {
768 lines.push("---".to_string());
769 for row in &block.summary_rows {
770 lines.push(self.serialize_row(row, &block.fields, &widths));
771 }
772 }
773
774 lines.join("\n")
775 }
776
777 fn calculate_widths(&self, block: &Block) -> Vec<usize> {
778 let mut widths: Vec<usize> = block.fields.iter().map(|f| f.len()).collect();
779
780 for row in block.rows.iter().chain(block.summary_rows.iter()) {
781 for (i, field) in block.fields.iter().enumerate() {
782 if let Some(value) = row.get(field) {
783 let str_val = self.serialize_value(value);
784 if i < widths.len() {
785 widths[i] = widths[i].max(str_val.len());
786 }
787 }
788 }
789 }
790
791 widths
792 }
793
794 fn serialize_row(&self, row: &Row, fields: &[String], widths: &[usize]) -> String {
795 let mut values = Vec::new();
796
797 for (i, field) in fields.iter().enumerate() {
798 let value = row.get(field).cloned().unwrap_or(Value::Null);
799 let mut str_val = self.serialize_value(&value);
800
801 if self.align_columns && !widths.is_empty() && i < fields.len() - 1 {
802 while str_val.len() < widths[i] {
803 str_val.push(' ');
804 }
805 }
806 values.push(str_val);
807 }
808
809 values.join(&self.delimiter)
810 }
811
812 fn serialize_value(&self, value: &Value) -> String {
813 match value {
814 Value::Null => "null".to_string(),
815 Value::Bool(b) => if *b { "true" } else { "false" }.to_string(),
816 Value::Int(i) => i.to_string(),
817 Value::Float(f) => f.to_string(),
818 Value::Reference(r) => r.to_ison(),
819 Value::String(s) => self.serialize_string(s),
820 }
821 }
822
823 fn serialize_string(&self, s: &str) -> String {
824 let needs_quotes = s.contains(' ')
825 || s.contains('\t')
826 || s.contains('\n')
827 || s.contains('"')
828 || s.contains('\\')
829 || s.contains('.') || s == "true"
831 || s == "false"
832 || s == "null"
833 || s.starts_with(':')
834 || s.parse::<f64>().is_ok();
835
836 if !needs_quotes {
837 return s.to_string();
838 }
839
840 let escaped = s
841 .replace('\\', "\\\\")
842 .replace('"', "\\\"")
843 .replace('\n', "\\n")
844 .replace('\t', "\\t")
845 .replace('\r', "\\r");
846
847 format!("\"{}\"", escaped)
848 }
849}
850
851pub fn parse_isonl(text: &str) -> Result<Document> {
857 let mut doc = Document::new();
858 let mut block_map: HashMap<String, usize> = HashMap::new();
859
860 for (line_num, line) in text.lines().enumerate() {
861 let line = line.trim();
862 if line.is_empty() || line.starts_with('#') {
863 continue;
864 }
865
866 let parts: Vec<&str> = line.split('|').collect();
867 if parts.len() != 3 {
868 return Err(ISONError {
869 message: format!("Invalid ISONL line: {}", line),
870 line: Some(line_num + 1),
871 });
872 }
873
874 let header = parts[0];
875 let fields_part = parts[1];
876 let values_part = parts[2];
877
878 let dot_index = header.find('.').ok_or_else(|| ISONError {
879 message: format!("Invalid ISONL header: {}", header),
880 line: Some(line_num + 1),
881 })?;
882
883 let kind = &header[..dot_index];
884 let name = &header[dot_index + 1..];
885 let key = format!("{}.{}", kind, name);
886
887 let block_idx = if let Some(&idx) = block_map.get(&key) {
888 idx
889 } else {
890 let mut block = Block::new(kind, name);
891
892 for f in fields_part.split_whitespace() {
894 if let Some(colon_idx) = f.find(':') {
895 let field_name = f[..colon_idx].to_string();
896 let field_type = f[colon_idx + 1..].to_string();
897 block.fields.push(field_name.clone());
898 block.field_info.push(FieldInfo::with_type(field_name, field_type));
899 } else {
900 block.fields.push(f.to_string());
901 block.field_info.push(FieldInfo::new(f));
902 }
903 }
904
905 let idx = doc.blocks.len();
906 block_map.insert(key, idx);
907 doc.blocks.push(block);
908 idx
909 };
910
911 let parser = Parser::new("");
913 let values = parser.tokenize_line(values_part);
914 let mut row = Row::new();
915
916 let block = &doc.blocks[block_idx];
917 for (i, field) in block.fields.iter().enumerate() {
918 if i < values.len() {
919 row.insert(field.clone(), parser.parse_value(&values[i])?);
920 }
921 }
922
923 doc.blocks[block_idx].rows.push(row);
924 }
925
926 Ok(doc)
927}
928
929pub fn dumps_isonl(doc: &Document) -> String {
931 let serializer = Serializer::new(false);
932 let mut lines = Vec::new();
933
934 for block in &doc.blocks {
935 let header = format!("{}.{}", block.kind, block.name);
936 let fields: Vec<String> = block
937 .field_info
938 .iter()
939 .map(|fi| {
940 if let Some(ref ft) = fi.field_type {
941 format!("{}:{}", fi.name, ft)
942 } else {
943 fi.name.clone()
944 }
945 })
946 .collect();
947 let fields_str = fields.join(" ");
948
949 for row in &block.rows {
950 let values: Vec<String> = block
951 .fields
952 .iter()
953 .map(|f| {
954 row.get(f)
955 .map(|v| serializer.serialize_value(v))
956 .unwrap_or_else(|| "null".to_string())
957 })
958 .collect();
959 lines.push(format!("{}|{}|{}", header, fields_str, values.join(" ")));
960 }
961 }
962
963 lines.join("\n")
964}
965
966pub fn parse(text: &str) -> Result<Document> {
972 Parser::new(text).parse()
973}
974
975pub fn loads(text: &str) -> Result<Document> {
977 parse(text)
978}
979
980pub fn dumps(doc: &Document, align_columns: bool) -> String {
986 Serializer::new(align_columns).serialize(doc)
987}
988
989pub fn dumps_with_delimiter(doc: &Document, align_columns: bool, delimiter: &str) -> String {
996 Serializer::with_delimiter(align_columns, delimiter).serialize(doc)
997}
998
999pub fn loads_isonl(text: &str) -> Result<Document> {
1001 parse_isonl(text)
1002}
1003
1004pub fn ison_to_isonl(ison_text: &str) -> Result<String> {
1006 let doc = parse(ison_text)?;
1007 Ok(dumps_isonl(&doc))
1008}
1009
1010pub fn isonl_to_ison(isonl_text: &str) -> Result<String> {
1012 let doc = parse_isonl(isonl_text)?;
1013 Ok(dumps(&doc, false))
1014}
1015
1016#[cfg(feature = "serde")]
1021pub fn json_to_ison(json_text: &str) -> Result<String> {
1022 let json_value: serde_json::Value = serde_json::from_str(json_text)
1023 .map_err(|e| ISONError { message: format!("JSON parse error: {}", e), line: None })?;
1024
1025 let obj = json_value.as_object()
1026 .ok_or_else(|| ISONError { message: "JSON must be an object".to_string(), line: None })?;
1027
1028 let mut doc = Document::new();
1029
1030 for (block_name, block_value) in obj {
1031 let arr = block_value.as_array()
1032 .ok_or_else(|| ISONError { message: format!("Block '{}' must be an array", block_name), line: None })?;
1033
1034 if arr.is_empty() {
1035 continue;
1036 }
1037
1038 let first_obj = arr[0].as_object()
1040 .ok_or_else(|| ISONError { message: "Array items must be objects".to_string(), line: None })?;
1041
1042 let fields: Vec<String> = first_obj.keys().cloned().collect();
1043 let field_info: Vec<FieldInfo> = fields.iter()
1044 .map(|f| FieldInfo { name: f.clone(), field_type: None, is_computed: false })
1045 .collect();
1046
1047 let mut rows = Vec::new();
1048 for item in arr {
1049 let item_obj = item.as_object()
1050 .ok_or_else(|| ISONError { message: "Array items must be objects".to_string(), line: None })?;
1051
1052 let mut row = Row::new();
1053 for field in &fields {
1054 if let Some(val) = item_obj.get(field) {
1055 let value = match val {
1056 serde_json::Value::Null => Value::Null,
1057 serde_json::Value::Bool(b) => Value::Bool(*b),
1058 serde_json::Value::Number(n) => {
1059 if let Some(i) = n.as_i64() {
1060 Value::Int(i)
1061 } else if let Some(f) = n.as_f64() {
1062 Value::Float(f)
1063 } else {
1064 Value::String(n.to_string())
1065 }
1066 }
1067 serde_json::Value::String(s) => {
1068 if s.starts_with(':') {
1070 let parts: Vec<&str> = s[1..].splitn(2, ':').collect();
1072 if parts.len() == 2 {
1073 Value::Reference(Reference::with_type(parts[1], parts[0]))
1074 } else {
1075 Value::Reference(Reference::new(parts[0]))
1076 }
1077 } else {
1078 Value::String(s.clone())
1079 }
1080 }
1081 _ => Value::String(val.to_string()),
1082 };
1083 row.insert(field.clone(), value);
1084 }
1085 }
1086 rows.push(row);
1087 }
1088
1089 let block = Block {
1090 kind: "table".to_string(),
1091 name: block_name.clone(),
1092 fields,
1093 field_info,
1094 rows,
1095 summary_rows: vec![],
1096 };
1097 doc.blocks.push(block);
1098 }
1099
1100 Ok(dumps(&doc, false))
1101}
1102
1103#[cfg(feature = "serde")]
1105pub fn ison_to_json(ison_text: &str, pretty: bool) -> Result<String> {
1106 let doc = parse(ison_text)?;
1107 Ok(doc.to_json(pretty))
1108}
1109
1110#[cfg(test)]
1111mod tests {
1112 use super::*;
1113
1114 #[test]
1115 fn test_parse_simple_table() {
1116 let ison = r#"table.users
1117id name email
11181 Alice alice@example.com
11192 Bob bob@example.com"#;
1120
1121 let doc = parse(ison).unwrap();
1122 let users = doc.get("users").unwrap();
1123
1124 assert_eq!(users.kind, "table");
1125 assert_eq!(users.name, "users");
1126 assert_eq!(users.len(), 2);
1127 assert_eq!(users.fields, vec!["id", "name", "email"]);
1128
1129 assert_eq!(users[0].get("id").unwrap().as_int(), Some(1));
1130 assert_eq!(users[0].get("name").unwrap().as_str(), Some("Alice"));
1131 }
1132
1133 #[test]
1134 fn test_parse_references() {
1135 let ison = r#"table.orders
1136id user_id
11371 :42
11382 :user:101
11393 :MEMBER_OF:10"#;
1140
1141 let doc = parse(ison).unwrap();
1142 let orders = doc.get("orders").unwrap();
1143
1144 let ref1 = orders[0].get("user_id").unwrap().as_reference().unwrap();
1145 assert_eq!(ref1.id, "42");
1146 assert!(ref1.ref_type.is_none());
1147
1148 let ref2 = orders[1].get("user_id").unwrap().as_reference().unwrap();
1149 assert_eq!(ref2.id, "101");
1150 assert_eq!(ref2.ref_type, Some("user".to_string()));
1151 assert!(!ref2.is_relationship());
1152
1153 let ref3 = orders[2].get("user_id").unwrap().as_reference().unwrap();
1154 assert_eq!(ref3.id, "10");
1155 assert!(ref3.is_relationship());
1156 }
1157
1158 #[test]
1159 fn test_type_inference() {
1160 let ison = r#"table.test
1161int_val float_val bool_val null_val str_val
116242 3.14 true null hello"#;
1163
1164 let doc = parse(ison).unwrap();
1165 let test = doc.get("test").unwrap();
1166
1167 assert!(test[0].get("int_val").unwrap().is_int());
1168 assert!(test[0].get("float_val").unwrap().is_float());
1169 assert!(test[0].get("bool_val").unwrap().is_bool());
1170 assert!(test[0].get("null_val").unwrap().is_null());
1171 assert!(test[0].get("str_val").unwrap().is_string());
1172 }
1173
1174 #[test]
1175 fn test_roundtrip() {
1176 let original = r#"table.users
1177id name email
11781 Alice alice@example.com
11792 Bob bob@example.com"#;
1180
1181 let doc = parse(original).unwrap();
1182 let serialized = dumps(&doc, true);
1183 let doc2 = parse(&serialized).unwrap();
1184
1185 assert_eq!(doc2.get("users").unwrap().len(), 2);
1186 }
1187
1188 #[test]
1189 fn test_isonl() {
1190 let isonl = "table.users|id name|1 Alice\ntable.users|id name|2 Bob";
1191
1192 let doc = parse_isonl(isonl).unwrap();
1193 let users = doc.get("users").unwrap();
1194
1195 assert_eq!(users.len(), 2);
1196 assert_eq!(users[0].get("name").unwrap().as_str(), Some("Alice"));
1197 }
1198
1199 #[test]
1200 fn test_dumps_with_delimiter() {
1201 let ison = r#"table.users
1202id name email
12031 Alice "alice@example.com"
12042 Bob "bob@example.com""#;
1205
1206 let doc = parse(ison).unwrap();
1207
1208 let comma_output = dumps_with_delimiter(&doc, false, ",");
1210 assert!(comma_output.contains("id,name,email"));
1211 assert!(comma_output.contains("1,Alice,\"alice@example.com\""));
1212
1213 let space_output = dumps_with_delimiter(&doc, false, " ");
1215 assert!(space_output.contains("id name email"));
1216 assert!(space_output.contains("1 Alice \"alice@example.com\""));
1217 }
1218
1219 #[test]
1220 fn test_version() {
1221 assert_eq!(VERSION, "1.0.1");
1222 }
1223
1224 #[test]
1225 fn test_json_to_ison() {
1226 let json = r#"{
1227 "users": [
1228 {"id": 1, "name": "Alice", "email": "alice@example.com"},
1229 {"id": 2, "name": "Bob", "email": "bob@example.com"}
1230 ]
1231 }"#;
1232
1233 let ison = json_to_ison(json).unwrap();
1234 assert!(ison.contains("table.users"));
1235
1236 let doc = parse(&ison).unwrap();
1238 let users = doc.get("users").unwrap();
1239 assert_eq!(users.len(), 2);
1240 }
1241
1242 #[test]
1243 fn test_ison_to_json() {
1244 let ison = r#"table.users
1245id name email
12461 Alice alice@example.com
12472 Bob bob@example.com"#;
1248
1249 let json = ison_to_json(ison, false).unwrap();
1250 assert!(json.contains("Alice"));
1251 assert!(json.contains("Bob"));
1252
1253 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1255 assert!(parsed.get("users").is_some());
1256 }
1257}