ison_rs/
lib.rs

1//! # ISON Parser for Rust
2//!
3//! A Rust implementation of the ISON (Interchange Simple Object Notation) parser.
4//! ISON is a minimal, LLM-friendly data serialization format optimized for AI/ML workflows.
5//!
6//! ## Quick Start
7//!
8//! ```rust
9//! use ison_rs::{parse, dumps, Value};
10//!
11//! let ison_text = r#"
12//! table.users
13//! id name email
14//! 1 Alice alice@example.com
15//! 2 Bob bob@example.com
16//! "#;
17//!
18//! let doc = parse(ison_text).unwrap();
19//! let users = doc.get("users").unwrap();
20//!
21//! for row in &users.rows {
22//!     println!("{}: {}", row.get("id").unwrap(), row.get("name").unwrap());
23//! }
24//!
25//! // Serialize back
26//! let output = dumps(&doc, true);
27//! ```
28
29use std::collections::HashMap;
30use std::fmt;
31
32// Plugins module (feature-gated)
33pub mod plugins;
34
35#[cfg(feature = "serde")]
36use serde::{Deserialize, Serialize};
37
38pub const VERSION: &str = "1.0.0";
39
40// =============================================================================
41// Error Types
42// =============================================================================
43
44/// Errors that can occur during ISON parsing
45#[derive(Debug, Clone)]
46pub struct ISONError {
47    pub message: String,
48    pub line: Option<usize>,
49}
50
51impl fmt::Display for ISONError {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        match self.line {
54            Some(line) => write!(f, "Line {}: {}", line, self.message),
55            None => write!(f, "{}", self.message),
56        }
57    }
58}
59
60impl std::error::Error for ISONError {}
61
62pub type Result<T> = std::result::Result<T, ISONError>;
63
64// =============================================================================
65// Types
66// =============================================================================
67
68/// Reference to another record in the document
69#[derive(Debug, Clone, PartialEq)]
70#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
71pub struct Reference {
72    pub id: String,
73    pub ref_type: Option<String>,
74}
75
76impl Reference {
77    /// Create a new simple reference
78    pub fn new(id: impl Into<String>) -> Self {
79        Self {
80            id: id.into(),
81            ref_type: None,
82        }
83    }
84
85    /// Create a new typed reference
86    pub fn with_type(id: impl Into<String>, ref_type: impl Into<String>) -> Self {
87        Self {
88            id: id.into(),
89            ref_type: Some(ref_type.into()),
90        }
91    }
92
93    /// Check if this is a relationship reference (UPPERCASE type)
94    pub fn is_relationship(&self) -> bool {
95        match &self.ref_type {
96            Some(t) => t.chars().all(|c| c.is_uppercase() || c == '_'),
97            None => false,
98        }
99    }
100
101    /// Get namespace (for non-relationship references)
102    pub fn get_namespace(&self) -> Option<&str> {
103        if self.is_relationship() {
104            None
105        } else {
106            self.ref_type.as_deref()
107        }
108    }
109
110    /// Get relationship type (for relationship references)
111    pub fn relationship_type(&self) -> Option<&str> {
112        if self.is_relationship() {
113            self.ref_type.as_deref()
114        } else {
115            None
116        }
117    }
118
119    /// Convert to ISON string representation
120    pub fn to_ison(&self) -> String {
121        match &self.ref_type {
122            Some(t) => format!(":{}:{}", t, self.id),
123            None => format!(":{}", self.id),
124        }
125    }
126}
127
128impl fmt::Display for Reference {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        write!(f, "{}", self.to_ison())
131    }
132}
133
134/// Value types in ISON
135#[derive(Debug, Clone, PartialEq)]
136#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
137#[cfg_attr(feature = "serde", serde(untagged))]
138pub enum Value {
139    Null,
140    Bool(bool),
141    Int(i64),
142    Float(f64),
143    String(String),
144    Reference(Reference),
145}
146
147impl Value {
148    pub fn is_null(&self) -> bool {
149        matches!(self, Value::Null)
150    }
151
152    pub fn is_bool(&self) -> bool {
153        matches!(self, Value::Bool(_))
154    }
155
156    pub fn is_int(&self) -> bool {
157        matches!(self, Value::Int(_))
158    }
159
160    pub fn is_float(&self) -> bool {
161        matches!(self, Value::Float(_))
162    }
163
164    pub fn is_string(&self) -> bool {
165        matches!(self, Value::String(_))
166    }
167
168    pub fn is_reference(&self) -> bool {
169        matches!(self, Value::Reference(_))
170    }
171
172    pub fn as_bool(&self) -> Option<bool> {
173        match self {
174            Value::Bool(b) => Some(*b),
175            _ => None,
176        }
177    }
178
179    pub fn as_int(&self) -> Option<i64> {
180        match self {
181            Value::Int(i) => Some(*i),
182            _ => None,
183        }
184    }
185
186    pub fn as_float(&self) -> Option<f64> {
187        match self {
188            Value::Float(f) => Some(*f),
189            Value::Int(i) => Some(*i as f64),
190            _ => None,
191        }
192    }
193
194    pub fn as_str(&self) -> Option<&str> {
195        match self {
196            Value::String(s) => Some(s),
197            _ => None,
198        }
199    }
200
201    pub fn as_reference(&self) -> Option<&Reference> {
202        match self {
203            Value::Reference(r) => Some(r),
204            _ => None,
205        }
206    }
207}
208
209impl fmt::Display for Value {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        match self {
212            Value::Null => write!(f, "null"),
213            Value::Bool(b) => write!(f, "{}", b),
214            Value::Int(i) => write!(f, "{}", i),
215            Value::Float(fl) => write!(f, "{}", fl),
216            Value::String(s) => write!(f, "{}", s),
217            Value::Reference(r) => write!(f, "{}", r),
218        }
219    }
220}
221
222/// A row of data (field name -> value mapping)
223pub type Row = HashMap<String, Value>;
224
225/// Field information including optional type annotation
226#[derive(Debug, Clone)]
227#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
228pub struct FieldInfo {
229    pub name: String,
230    pub field_type: Option<String>,
231    pub is_computed: bool,
232}
233
234impl FieldInfo {
235    pub fn new(name: impl Into<String>) -> Self {
236        Self {
237            name: name.into(),
238            field_type: None,
239            is_computed: false,
240        }
241    }
242
243    pub fn with_type(name: impl Into<String>, field_type: impl Into<String>) -> Self {
244        let ft: String = field_type.into();
245        let is_computed = ft == "computed";
246        Self {
247            name: name.into(),
248            field_type: Some(ft),
249            is_computed,
250        }
251    }
252}
253
254/// A block of structured data
255#[derive(Debug, Clone)]
256#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
257pub struct Block {
258    pub kind: String,
259    pub name: String,
260    pub fields: Vec<String>,
261    pub field_info: Vec<FieldInfo>,
262    pub rows: Vec<Row>,
263    pub summary_rows: Vec<Row>,
264}
265
266impl Block {
267    pub fn new(kind: impl Into<String>, name: impl Into<String>) -> Self {
268        Self {
269            kind: kind.into(),
270            name: name.into(),
271            fields: Vec::new(),
272            field_info: Vec::new(),
273            rows: Vec::new(),
274            summary_rows: Vec::new(),
275        }
276    }
277
278    /// Number of data rows
279    pub fn len(&self) -> usize {
280        self.rows.len()
281    }
282
283    /// Check if block has no rows
284    pub fn is_empty(&self) -> bool {
285        self.rows.is_empty()
286    }
287
288    /// Get row by index
289    pub fn get_row(&self, index: usize) -> Option<&Row> {
290        self.rows.get(index)
291    }
292
293    /// Get field type annotation
294    pub fn get_field_type(&self, field_name: &str) -> Option<&str> {
295        self.field_info
296            .iter()
297            .find(|fi| fi.name == field_name)
298            .and_then(|fi| fi.field_type.as_deref())
299    }
300
301    /// Get list of computed fields
302    pub fn get_computed_fields(&self) -> Vec<&str> {
303        self.field_info
304            .iter()
305            .filter(|fi| fi.is_computed)
306            .map(|fi| fi.name.as_str())
307            .collect()
308    }
309}
310
311impl std::ops::Index<usize> for Block {
312    type Output = Row;
313
314    fn index(&self, index: usize) -> &Self::Output {
315        &self.rows[index]
316    }
317}
318
319/// A complete ISON document
320#[derive(Debug, Clone, Default)]
321#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
322pub struct Document {
323    pub blocks: Vec<Block>,
324}
325
326impl Document {
327    pub fn new() -> Self {
328        Self { blocks: Vec::new() }
329    }
330
331    /// Get block by name
332    pub fn get(&self, name: &str) -> Option<&Block> {
333        self.blocks.iter().find(|b| b.name == name)
334    }
335
336    /// Get mutable block by name
337    pub fn get_mut(&mut self, name: &str) -> Option<&mut Block> {
338        self.blocks.iter_mut().find(|b| b.name == name)
339    }
340
341    /// Check if block exists
342    pub fn has(&self, name: &str) -> bool {
343        self.blocks.iter().any(|b| b.name == name)
344    }
345
346    /// Number of blocks
347    pub fn len(&self) -> usize {
348        self.blocks.len()
349    }
350
351    /// Check if document is empty
352    pub fn is_empty(&self) -> bool {
353        self.blocks.is_empty()
354    }
355
356    /// Convert to JSON string (requires serde feature)
357    #[cfg(feature = "serde")]
358    pub fn to_json(&self, pretty: bool) -> String {
359        let map: HashMap<&str, Vec<&Row>> = self
360            .blocks
361            .iter()
362            .map(|b| (b.name.as_str(), b.rows.iter().collect()))
363            .collect();
364
365        if pretty {
366            serde_json::to_string_pretty(&map).unwrap_or_default()
367        } else {
368            serde_json::to_string(&map).unwrap_or_default()
369        }
370    }
371}
372
373impl std::ops::Index<&str> for Document {
374    type Output = Block;
375
376    fn index(&self, name: &str) -> &Self::Output {
377        self.get(name).expect("Block not found")
378    }
379}
380
381// =============================================================================
382// Parser
383// =============================================================================
384
385struct Parser<'a> {
386    text: &'a str,
387    pos: usize,
388    line: usize,
389}
390
391impl<'a> Parser<'a> {
392    fn new(text: &'a str) -> Self {
393        Self {
394            text,
395            pos: 0,
396            line: 1,
397        }
398    }
399
400    fn parse(&mut self) -> Result<Document> {
401        let mut doc = Document::new();
402
403        self.skip_whitespace_and_comments();
404
405        while self.pos < self.text.len() {
406            if let Some(block) = self.parse_block()? {
407                doc.blocks.push(block);
408            }
409            self.skip_whitespace_and_comments();
410        }
411
412        Ok(doc)
413    }
414
415    fn parse_block(&mut self) -> Result<Option<Block>> {
416        let header_line = match self.read_line() {
417            Some(line) => line,
418            None => return Ok(None),
419        };
420
421        if header_line.starts_with('#') || header_line.is_empty() {
422            return Ok(None);
423        }
424
425        let dot_index = header_line.find('.').ok_or_else(|| ISONError {
426            message: format!("Invalid block header: {}", header_line),
427            line: Some(self.line),
428        })?;
429
430        let kind = header_line[..dot_index].trim().to_string();
431        let name = header_line[dot_index + 1..].trim().to_string();
432
433        if kind.is_empty() || name.is_empty() {
434            return Err(ISONError {
435                message: format!("Invalid block header: {}", header_line),
436                line: Some(self.line),
437            });
438        }
439
440        let mut block = Block::new(kind, name);
441
442        // Parse field definitions
443        self.skip_empty_lines();
444        let fields_line = match self.read_line() {
445            Some(line) => line,
446            None => return Ok(Some(block)),
447        };
448
449        let field_tokens = self.tokenize_line(&fields_line);
450        for token in field_tokens {
451            if let Some(colon_idx) = token.find(':') {
452                let field_name = token[..colon_idx].to_string();
453                let field_type = token[colon_idx + 1..].to_string();
454                block.fields.push(field_name.clone());
455                block.field_info.push(FieldInfo::with_type(field_name, field_type));
456            } else {
457                block.fields.push(token.clone());
458                block.field_info.push(FieldInfo::new(token));
459            }
460        }
461
462        // Parse data rows
463        let mut in_summary = false;
464        while self.pos < self.text.len() {
465            let line = match self.peek_line() {
466                Some(line) => line,
467                None => break,
468            };
469
470            // Empty line or new block = end of current block
471            if line.is_empty() || (line.chars().next().map(|c| c.is_alphabetic()).unwrap_or(false)
472                && line.contains('.'))
473            {
474                break;
475            }
476
477            self.read_line(); // consume the line
478
479            // Skip comments
480            if line.starts_with('#') {
481                continue;
482            }
483
484            // Summary separator
485            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        // Remove inline comments
518        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            // Skip whitespace
534            while i < chars.len() && (chars[i] == ' ' || chars[i] == '\t') {
535                i += 1;
536            }
537
538            if i >= chars.len() {
539                break;
540            }
541
542            // Quoted string
543            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                // Unquoted token
549                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; // skip opening quote
563
564        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        // Null
594        if token == "null" || token == "~" {
595            return Ok(Value::Null);
596        }
597
598        // Boolean
599        if token == "true" {
600            return Ok(Value::Bool(true));
601        }
602        if token == "false" {
603            return Ok(Value::Bool(false));
604        }
605
606        // Reference
607        if token.starts_with(':') {
608            return self.parse_reference(token);
609        }
610
611        // Integer
612        if let Ok(i) = token.parse::<i64>() {
613            return Ok(Value::Int(i));
614        }
615
616        // Float
617        if let Ok(f) = token.parse::<f64>() {
618            return Ok(Value::Float(f));
619        }
620
621        // String
622        Ok(Value::String(token.to_string()))
623    }
624
625    fn parse_reference(&self, token: &str) -> Result<Value> {
626        let content = &token[1..]; // skip ':'
627        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; // skip newline
653        }
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
711// =============================================================================
712// Serializer
713// =============================================================================
714
715struct 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        // Header
733        lines.push(format!("{}.{}", block.kind, block.name));
734
735        // Fields with types
736        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        // Calculate column widths for alignment
750        let widths = if self.align_columns {
751            self.calculate_widths(block)
752        } else {
753            vec![]
754        };
755
756        // Data rows
757        for row in &block.rows {
758            lines.push(self.serialize_row(row, &block.fields, &widths));
759        }
760
761        // Summary separator and rows
762        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
845// =============================================================================
846// ISONL Parser/Serializer
847// =============================================================================
848
849/// Parse ISONL format
850pub 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            // Parse fields
887            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        // Parse values
906        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
923/// Serialize to ISONL format
924pub 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
960// =============================================================================
961// Public API
962// =============================================================================
963
964/// Parse an ISON string into a Document
965pub fn parse(text: &str) -> Result<Document> {
966    Parser::new(text).parse()
967}
968
969/// Parse an ISON string into a Document (alias for parse)
970pub fn loads(text: &str) -> Result<Document> {
971    parse(text)
972}
973
974/// Serialize a Document to an ISON string
975pub fn dumps(doc: &Document, align_columns: bool) -> String {
976    Serializer::new(align_columns).serialize(doc)
977}
978
979/// Parse ISONL string (alias for parse_isonl)
980pub fn loads_isonl(text: &str) -> Result<Document> {
981    parse_isonl(text)
982}
983
984/// Convert ISON text to ISONL text
985pub fn ison_to_isonl(ison_text: &str) -> Result<String> {
986    let doc = parse(ison_text)?;
987    Ok(dumps_isonl(&doc))
988}
989
990/// Convert ISONL text to ISON text
991pub 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}