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.0";
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}
718
719impl Serializer {
720 fn new(align_columns: bool) -> Self {
721 Self { align_columns }
722 }
723
724 fn serialize(&self, doc: &Document) -> String {
725 let parts: Vec<String> = doc.blocks.iter().map(|b| self.serialize_block(b)).collect();
726 parts.join("\n\n")
727 }
728
729 fn serialize_block(&self, block: &Block) -> String {
730 let mut lines = Vec::new();
731
732 lines.push(format!("{}.{}", block.kind, block.name));
734
735 let field_defs: Vec<String> = block
737 .field_info
738 .iter()
739 .map(|fi| {
740 if let Some(ref ft) = fi.field_type {
741 format!("{}:{}", fi.name, ft)
742 } else {
743 fi.name.clone()
744 }
745 })
746 .collect();
747 lines.push(field_defs.join(" "));
748
749 let widths = if self.align_columns {
751 self.calculate_widths(block)
752 } else {
753 vec![]
754 };
755
756 for row in &block.rows {
758 lines.push(self.serialize_row(row, &block.fields, &widths));
759 }
760
761 if !block.summary_rows.is_empty() {
763 lines.push("---".to_string());
764 for row in &block.summary_rows {
765 lines.push(self.serialize_row(row, &block.fields, &widths));
766 }
767 }
768
769 lines.join("\n")
770 }
771
772 fn calculate_widths(&self, block: &Block) -> Vec<usize> {
773 let mut widths: Vec<usize> = block.fields.iter().map(|f| f.len()).collect();
774
775 for row in block.rows.iter().chain(block.summary_rows.iter()) {
776 for (i, field) in block.fields.iter().enumerate() {
777 if let Some(value) = row.get(field) {
778 let str_val = self.serialize_value(value);
779 if i < widths.len() {
780 widths[i] = widths[i].max(str_val.len());
781 }
782 }
783 }
784 }
785
786 widths
787 }
788
789 fn serialize_row(&self, row: &Row, fields: &[String], widths: &[usize]) -> String {
790 let mut values = Vec::new();
791
792 for (i, field) in fields.iter().enumerate() {
793 let value = row.get(field).cloned().unwrap_or(Value::Null);
794 let mut str_val = self.serialize_value(&value);
795
796 if self.align_columns && !widths.is_empty() && i < fields.len() - 1 {
797 while str_val.len() < widths[i] {
798 str_val.push(' ');
799 }
800 }
801 values.push(str_val);
802 }
803
804 values.join(" ")
805 }
806
807 fn serialize_value(&self, value: &Value) -> String {
808 match value {
809 Value::Null => "null".to_string(),
810 Value::Bool(b) => if *b { "true" } else { "false" }.to_string(),
811 Value::Int(i) => i.to_string(),
812 Value::Float(f) => f.to_string(),
813 Value::Reference(r) => r.to_ison(),
814 Value::String(s) => self.serialize_string(s),
815 }
816 }
817
818 fn serialize_string(&self, s: &str) -> String {
819 let needs_quotes = s.contains(' ')
820 || s.contains('\t')
821 || s.contains('\n')
822 || s.contains('"')
823 || s.contains('\\')
824 || s == "true"
825 || s == "false"
826 || s == "null"
827 || s.starts_with(':')
828 || s.parse::<f64>().is_ok();
829
830 if !needs_quotes {
831 return s.to_string();
832 }
833
834 let escaped = s
835 .replace('\\', "\\\\")
836 .replace('"', "\\\"")
837 .replace('\n', "\\n")
838 .replace('\t', "\\t")
839 .replace('\r', "\\r");
840
841 format!("\"{}\"", escaped)
842 }
843}
844
845pub fn parse_isonl(text: &str) -> Result<Document> {
851 let mut doc = Document::new();
852 let mut block_map: HashMap<String, usize> = HashMap::new();
853
854 for (line_num, line) in text.lines().enumerate() {
855 let line = line.trim();
856 if line.is_empty() || line.starts_with('#') {
857 continue;
858 }
859
860 let parts: Vec<&str> = line.split('|').collect();
861 if parts.len() != 3 {
862 return Err(ISONError {
863 message: format!("Invalid ISONL line: {}", line),
864 line: Some(line_num + 1),
865 });
866 }
867
868 let header = parts[0];
869 let fields_part = parts[1];
870 let values_part = parts[2];
871
872 let dot_index = header.find('.').ok_or_else(|| ISONError {
873 message: format!("Invalid ISONL header: {}", header),
874 line: Some(line_num + 1),
875 })?;
876
877 let kind = &header[..dot_index];
878 let name = &header[dot_index + 1..];
879 let key = format!("{}.{}", kind, name);
880
881 let block_idx = if let Some(&idx) = block_map.get(&key) {
882 idx
883 } else {
884 let mut block = Block::new(kind, name);
885
886 for f in fields_part.split_whitespace() {
888 if let Some(colon_idx) = f.find(':') {
889 let field_name = f[..colon_idx].to_string();
890 let field_type = f[colon_idx + 1..].to_string();
891 block.fields.push(field_name.clone());
892 block.field_info.push(FieldInfo::with_type(field_name, field_type));
893 } else {
894 block.fields.push(f.to_string());
895 block.field_info.push(FieldInfo::new(f));
896 }
897 }
898
899 let idx = doc.blocks.len();
900 block_map.insert(key, idx);
901 doc.blocks.push(block);
902 idx
903 };
904
905 let parser = Parser::new("");
907 let values = parser.tokenize_line(values_part);
908 let mut row = Row::new();
909
910 let block = &doc.blocks[block_idx];
911 for (i, field) in block.fields.iter().enumerate() {
912 if i < values.len() {
913 row.insert(field.clone(), parser.parse_value(&values[i])?);
914 }
915 }
916
917 doc.blocks[block_idx].rows.push(row);
918 }
919
920 Ok(doc)
921}
922
923pub fn dumps_isonl(doc: &Document) -> String {
925 let serializer = Serializer::new(false);
926 let mut lines = Vec::new();
927
928 for block in &doc.blocks {
929 let header = format!("{}.{}", block.kind, block.name);
930 let fields: Vec<String> = block
931 .field_info
932 .iter()
933 .map(|fi| {
934 if let Some(ref ft) = fi.field_type {
935 format!("{}:{}", fi.name, ft)
936 } else {
937 fi.name.clone()
938 }
939 })
940 .collect();
941 let fields_str = fields.join(" ");
942
943 for row in &block.rows {
944 let values: Vec<String> = block
945 .fields
946 .iter()
947 .map(|f| {
948 row.get(f)
949 .map(|v| serializer.serialize_value(v))
950 .unwrap_or_else(|| "null".to_string())
951 })
952 .collect();
953 lines.push(format!("{}|{}|{}", header, fields_str, values.join(" ")));
954 }
955 }
956
957 lines.join("\n")
958}
959
960pub fn parse(text: &str) -> Result<Document> {
966 Parser::new(text).parse()
967}
968
969pub fn loads(text: &str) -> Result<Document> {
971 parse(text)
972}
973
974pub fn dumps(doc: &Document, align_columns: bool) -> String {
976 Serializer::new(align_columns).serialize(doc)
977}
978
979pub fn loads_isonl(text: &str) -> Result<Document> {
981 parse_isonl(text)
982}
983
984pub fn ison_to_isonl(ison_text: &str) -> Result<String> {
986 let doc = parse(ison_text)?;
987 Ok(dumps_isonl(&doc))
988}
989
990pub fn isonl_to_ison(isonl_text: &str) -> Result<String> {
992 let doc = parse_isonl(isonl_text)?;
993 Ok(dumps(&doc, true))
994}
995
996#[cfg(test)]
997mod tests {
998 use super::*;
999
1000 #[test]
1001 fn test_parse_simple_table() {
1002 let ison = r#"table.users
1003id name email
10041 Alice alice@example.com
10052 Bob bob@example.com"#;
1006
1007 let doc = parse(ison).unwrap();
1008 let users = doc.get("users").unwrap();
1009
1010 assert_eq!(users.kind, "table");
1011 assert_eq!(users.name, "users");
1012 assert_eq!(users.len(), 2);
1013 assert_eq!(users.fields, vec!["id", "name", "email"]);
1014
1015 assert_eq!(users[0].get("id").unwrap().as_int(), Some(1));
1016 assert_eq!(users[0].get("name").unwrap().as_str(), Some("Alice"));
1017 }
1018
1019 #[test]
1020 fn test_parse_references() {
1021 let ison = r#"table.orders
1022id user_id
10231 :42
10242 :user:101
10253 :MEMBER_OF:10"#;
1026
1027 let doc = parse(ison).unwrap();
1028 let orders = doc.get("orders").unwrap();
1029
1030 let ref1 = orders[0].get("user_id").unwrap().as_reference().unwrap();
1031 assert_eq!(ref1.id, "42");
1032 assert!(ref1.ref_type.is_none());
1033
1034 let ref2 = orders[1].get("user_id").unwrap().as_reference().unwrap();
1035 assert_eq!(ref2.id, "101");
1036 assert_eq!(ref2.ref_type, Some("user".to_string()));
1037 assert!(!ref2.is_relationship());
1038
1039 let ref3 = orders[2].get("user_id").unwrap().as_reference().unwrap();
1040 assert_eq!(ref3.id, "10");
1041 assert!(ref3.is_relationship());
1042 }
1043
1044 #[test]
1045 fn test_type_inference() {
1046 let ison = r#"table.test
1047int_val float_val bool_val null_val str_val
104842 3.14 true null hello"#;
1049
1050 let doc = parse(ison).unwrap();
1051 let test = doc.get("test").unwrap();
1052
1053 assert!(test[0].get("int_val").unwrap().is_int());
1054 assert!(test[0].get("float_val").unwrap().is_float());
1055 assert!(test[0].get("bool_val").unwrap().is_bool());
1056 assert!(test[0].get("null_val").unwrap().is_null());
1057 assert!(test[0].get("str_val").unwrap().is_string());
1058 }
1059
1060 #[test]
1061 fn test_roundtrip() {
1062 let original = r#"table.users
1063id name email
10641 Alice alice@example.com
10652 Bob bob@example.com"#;
1066
1067 let doc = parse(original).unwrap();
1068 let serialized = dumps(&doc, true);
1069 let doc2 = parse(&serialized).unwrap();
1070
1071 assert_eq!(doc2.get("users").unwrap().len(), 2);
1072 }
1073
1074 #[test]
1075 fn test_isonl() {
1076 let isonl = "table.users|id name|1 Alice\ntable.users|id name|2 Bob";
1077
1078 let doc = parse_isonl(isonl).unwrap();
1079 let users = doc.get("users").unwrap();
1080
1081 assert_eq!(users.len(), 2);
1082 assert_eq!(users[0].get("name").unwrap().as_str(), Some("Alice"));
1083 }
1084}