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.1";
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    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        // Header
738        lines.push(format!("{}.{}", block.kind, block.name));
739
740        // Fields with types
741        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        // Calculate column widths for alignment
755        let widths = if self.align_columns {
756            self.calculate_widths(block)
757        } else {
758            vec![]
759        };
760
761        // Data rows
762        for row in &block.rows {
763            lines.push(self.serialize_row(row, &block.fields, &widths));
764        }
765
766        // Summary separator and rows
767        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('.')  // Avoid confusion with block headers (type.name)
830            || 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
851// =============================================================================
852// ISONL Parser/Serializer
853// =============================================================================
854
855/// Parse ISONL format
856pub 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            // Parse fields
893            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        // Parse values
912        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
929/// Serialize to ISONL format
930pub 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
966// =============================================================================
967// Public API
968// =============================================================================
969
970/// Parse an ISON string into a Document
971pub fn parse(text: &str) -> Result<Document> {
972    Parser::new(text).parse()
973}
974
975/// Parse an ISON string into a Document (alias for parse)
976pub fn loads(text: &str) -> Result<Document> {
977    parse(text)
978}
979
980/// Serialize a Document to an ISON string
981///
982/// # Arguments
983/// * `doc` - The document to serialize
984/// * `align_columns` - Whether to align columns with padding (default: false for token efficiency)
985pub fn dumps(doc: &Document, align_columns: bool) -> String {
986    Serializer::new(align_columns).serialize(doc)
987}
988
989/// Serialize a Document to an ISON string with custom delimiter
990///
991/// # Arguments
992/// * `doc` - The document to serialize
993/// * `align_columns` - Whether to align columns with padding
994/// * `delimiter` - Column separator (default: " ", alternatives: ",")
995pub fn dumps_with_delimiter(doc: &Document, align_columns: bool, delimiter: &str) -> String {
996    Serializer::with_delimiter(align_columns, delimiter).serialize(doc)
997}
998
999/// Parse ISONL string (alias for parse_isonl)
1000pub fn loads_isonl(text: &str) -> Result<Document> {
1001    parse_isonl(text)
1002}
1003
1004/// Convert ISON text to ISONL text
1005pub fn ison_to_isonl(ison_text: &str) -> Result<String> {
1006    let doc = parse(ison_text)?;
1007    Ok(dumps_isonl(&doc))
1008}
1009
1010/// Convert ISONL text to ISON text
1011pub fn isonl_to_ison(isonl_text: &str) -> Result<String> {
1012    let doc = parse_isonl(isonl_text)?;
1013    Ok(dumps(&doc, false))
1014}
1015
1016/// Convert JSON to ISON format (requires serde feature)
1017///
1018/// Converts a JSON object where keys are block names and values are arrays of objects
1019/// into ISON format.
1020#[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        // Get fields from first object
1039        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                            // Check if it's a reference (starts with :)
1069                            if s.starts_with(':') {
1070                                // Parse reference: :id or :type:id
1071                                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/// Convert ISON to JSON format (requires serde feature)
1104#[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        // Test with comma delimiter (emails get quoted because they contain '.')
1209        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        // Test with default space delimiter
1214        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        // Parse it back to verify
1237        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        // Verify it's valid JSON
1254        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1255        assert!(parsed.get("users").is_some());
1256    }
1257}