Skip to main content

lino_objects_codec/
lib.rs

1//! Object encoder/decoder for Links Notation format.
2//!
3//! This library provides encoding and decoding of JSON-like objects to/from
4//! Links Notation format. It supports all common JSON types plus special
5//! float values (NaN, Infinity, -Infinity).
6//!
7//! # Features
8//!
9//! - **Readable by Default**: `encode()` writes plain, indented text that can be read and reviewed
10//! - **Universal Serialization**: Encode objects to Links Notation format
11//! - **Type Support**: Handle all common types: null, boolean, integer, float, string, array, object
12//! - **Special Float Values**: Support for NaN, Infinity, -Infinity (which are not valid JSON)
13//! - **Circular References**: Detect and preserve circular references (via object IDs)
14//! - **Object Identity**: Maintain object identity for shared references
15//! - **UTF-8 Support**: Full Unicode string support, always written as text -- a string is never
16//!   base64-encoded, so every word of it stays greppable. A character the chosen form cannot hold
17//!   (a carriage return in either form, a newline on a single line) is percent-escaped, and only
18//!   that one value is marked, as `(escaped "line one%0Aline two")`
19//! - **Simple API**: Easy-to-use `encode()` and `decode()` functions
20//!
21//! # Example
22//!
23//! ```rust
24//! use lino_objects_codec::{encode, decode, LinoValue};
25//!
26//! // Encode a simple object
27//! let data = LinoValue::object([
28//!     ("name", LinoValue::String("Alice".to_string())),
29//!     ("age", LinoValue::Int(30)),
30//!     ("active", LinoValue::Bool(true)),
31//! ]);
32//! let encoded = encode(&data);
33//! assert_eq!(encoded, "(\n  name \"Alice\"\n  age 30\n  active true\n)");
34//! let decoded = decode(&encoded).unwrap();
35//! assert_eq!(decoded, data);
36//! ```
37//!
38//! # Output formats
39//!
40//! | Function | Output |
41//! |---|---|
42//! | [`encode`] | readable, indented plain text (default) |
43//! | [`encode_with_indent`] | the same, with a custom indentation string |
44//! | [`encode_line`] | readable plain text on a single line, one record per line |
45//! | [`encode_compact`] / [`encode_obfuscated`] | the previous single-line, base64 form |
46//!
47//! [`decode`] accepts all of them, so files written by earlier versions keep working
48//! and migrate to the readable form on the next write. [`decode_line`] is the exact
49//! inverse of [`encode_line`] and reads one record at a time.
50
51use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
52use links_notation::{LiNo, parse_lino_to_links};
53use std::collections::{HashMap, HashSet};
54use std::fmt;
55
56pub mod debug;
57pub mod readable;
58
59pub use readable::{BASE64_MARKER, DEFAULT_INDENT, ESCAPED_MARKER, OBJECT_MARKER};
60
61/// Type identifiers used in the compact (base64) Links Notation format
62mod type_ids {
63    pub const NULL: &str = "null";
64    pub const BOOL: &str = "bool";
65    pub const INT: &str = "int";
66    pub const FLOAT: &str = "float";
67    pub const STR: &str = "str";
68    pub const ARRAY: &str = "array";
69    pub const OBJECT: &str = "object";
70}
71
72/// Error types for codec operations
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub enum CodecError {
75    /// Parsing error
76    ParseError(String),
77    /// Decoding error
78    DecodeError(String),
79    /// Unknown type marker
80    UnknownType(String),
81}
82
83impl fmt::Display for CodecError {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        match self {
86            CodecError::ParseError(msg) => write!(f, "Parse error: {}", msg),
87            CodecError::DecodeError(msg) => write!(f, "Decode error: {}", msg),
88            CodecError::UnknownType(t) => write!(f, "Unknown type marker: {}", t),
89        }
90    }
91}
92
93impl std::error::Error for CodecError {}
94
95/// A value that can be encoded/decoded using the Links Notation codec.
96///
97/// This type supports all the types available in Python/JavaScript versions
98/// including special float values (NaN, Infinity) that are not valid JSON.
99#[derive(Debug, Clone)]
100pub enum LinoValue {
101    /// Null value
102    Null,
103    /// Boolean value
104    Bool(bool),
105    /// Integer value (64-bit signed)
106    Int(i64),
107    /// Floating point value (64-bit)
108    Float(f64),
109    /// String value
110    String(String),
111    /// Array of values
112    Array(Vec<LinoValue>),
113    /// Object/dictionary with string keys
114    Object(Vec<(String, LinoValue)>),
115}
116
117impl PartialEq for LinoValue {
118    fn eq(&self, other: &Self) -> bool {
119        match (self, other) {
120            (LinoValue::Null, LinoValue::Null) => true,
121            (LinoValue::Bool(a), LinoValue::Bool(b)) => a == b,
122            (LinoValue::Int(a), LinoValue::Int(b)) => a == b,
123            (LinoValue::Float(a), LinoValue::Float(b)) => {
124                // Handle NaN comparison
125                if a.is_nan() && b.is_nan() {
126                    true
127                } else {
128                    a == b
129                }
130            }
131            (LinoValue::String(a), LinoValue::String(b)) => a == b,
132            (LinoValue::Array(a), LinoValue::Array(b)) => a == b,
133            (LinoValue::Object(a), LinoValue::Object(b)) => {
134                // Objects are equal if they have the same keys and values
135                if a.len() != b.len() {
136                    return false;
137                }
138                // Create hashmaps for comparison (order-independent)
139                let a_map: HashMap<&str, &LinoValue> =
140                    a.iter().map(|(k, v)| (k.as_str(), v)).collect();
141                let b_map: HashMap<&str, &LinoValue> =
142                    b.iter().map(|(k, v)| (k.as_str(), v)).collect();
143                a_map == b_map
144            }
145            _ => false,
146        }
147    }
148}
149
150impl LinoValue {
151    /// Create an object from an iterator of key-value pairs.
152    pub fn object<I, K, V>(iter: I) -> Self
153    where
154        I: IntoIterator<Item = (K, V)>,
155        K: Into<String>,
156        V: Into<LinoValue>,
157    {
158        LinoValue::Object(
159            iter.into_iter()
160                .map(|(k, v)| (k.into(), v.into()))
161                .collect(),
162        )
163    }
164
165    /// Create an array from an iterator of values.
166    pub fn array<I, V>(iter: I) -> Self
167    where
168        I: IntoIterator<Item = V>,
169        V: Into<LinoValue>,
170    {
171        LinoValue::Array(iter.into_iter().map(|v| v.into()).collect())
172    }
173
174    /// Check if this is a null value.
175    pub fn is_null(&self) -> bool {
176        matches!(self, LinoValue::Null)
177    }
178
179    /// Get as a boolean, if this is a bool.
180    pub fn as_bool(&self) -> Option<bool> {
181        match self {
182            LinoValue::Bool(b) => Some(*b),
183            _ => None,
184        }
185    }
186
187    /// Get as an integer, if this is an int.
188    pub fn as_int(&self) -> Option<i64> {
189        match self {
190            LinoValue::Int(i) => Some(*i),
191            _ => None,
192        }
193    }
194
195    /// Get as a float, if this is a float or int.
196    pub fn as_float(&self) -> Option<f64> {
197        match self {
198            LinoValue::Float(f) => Some(*f),
199            LinoValue::Int(i) => Some(*i as f64),
200            _ => None,
201        }
202    }
203
204    /// Get as a string, if this is a string.
205    pub fn as_str(&self) -> Option<&str> {
206        match self {
207            LinoValue::String(s) => Some(s),
208            _ => None,
209        }
210    }
211
212    /// Get as an array, if this is an array.
213    pub fn as_array(&self) -> Option<&Vec<LinoValue>> {
214        match self {
215            LinoValue::Array(a) => Some(a),
216            _ => None,
217        }
218    }
219
220    /// Get as an object, if this is an object.
221    pub fn as_object(&self) -> Option<&Vec<(String, LinoValue)>> {
222        match self {
223            LinoValue::Object(o) => Some(o),
224            _ => None,
225        }
226    }
227
228    /// Get a value from an object by key.
229    pub fn get(&self, key: &str) -> Option<&LinoValue> {
230        match self {
231            LinoValue::Object(o) => o.iter().find(|(k, _)| k == key).map(|(_, v)| v),
232            _ => None,
233        }
234    }
235
236    /// Get a value from an array by index.
237    pub fn get_index(&self, index: usize) -> Option<&LinoValue> {
238        match self {
239            LinoValue::Array(a) => a.get(index),
240            _ => None,
241        }
242    }
243}
244
245// Implement From traits for convenience
246impl From<()> for LinoValue {
247    fn from(_: ()) -> Self {
248        LinoValue::Null
249    }
250}
251
252impl From<bool> for LinoValue {
253    fn from(b: bool) -> Self {
254        LinoValue::Bool(b)
255    }
256}
257
258impl From<i32> for LinoValue {
259    fn from(i: i32) -> Self {
260        LinoValue::Int(i as i64)
261    }
262}
263
264impl From<i64> for LinoValue {
265    fn from(i: i64) -> Self {
266        LinoValue::Int(i)
267    }
268}
269
270impl From<f64> for LinoValue {
271    fn from(f: f64) -> Self {
272        LinoValue::Float(f)
273    }
274}
275
276impl From<&str> for LinoValue {
277    fn from(s: &str) -> Self {
278        LinoValue::String(s.to_string())
279    }
280}
281
282impl From<String> for LinoValue {
283    fn from(s: String) -> Self {
284        LinoValue::String(s)
285    }
286}
287
288impl<T: Into<LinoValue>> From<Vec<T>> for LinoValue {
289    fn from(v: Vec<T>) -> Self {
290        LinoValue::Array(v.into_iter().map(|x| x.into()).collect())
291    }
292}
293
294impl<T: Into<LinoValue>> From<Option<T>> for LinoValue {
295    fn from(opt: Option<T>) -> Self {
296        match opt {
297            Some(v) => v.into(),
298            None => LinoValue::Null,
299        }
300    }
301}
302
303/// Codec for encoding/decoding LinoValue to/from Links Notation.
304///
305/// This codec handles the conversion between `LinoValue` and Links Notation
306/// format strings. It supports circular references and shared object identity
307/// through object ID references.
308pub struct ObjectCodec {
309    /// Counter for generating unique object IDs
310    encode_counter: usize,
311    /// Maps object representations to their assigned IDs (for detecting shared references)
312    encode_memo: HashMap<String, String>,
313    /// Set of object representations that need IDs (referenced multiple times)
314    needs_id: HashSet<String>,
315    /// All link definitions generated during encoding (for multi-link format)
316    all_definitions: Vec<(String, LiNo<String>)>,
317    /// Maps object IDs to decoded values during decoding
318    decode_memo: HashMap<String, LinoValue>,
319    /// All links available for forward reference resolution
320    all_links: Vec<LiNo<String>>,
321}
322
323impl Default for ObjectCodec {
324    fn default() -> Self {
325        Self::new()
326    }
327}
328
329impl ObjectCodec {
330    /// Create a new ObjectCodec instance.
331    pub fn new() -> Self {
332        ObjectCodec {
333            encode_counter: 0,
334            encode_memo: HashMap::new(),
335            needs_id: HashSet::new(),
336            all_definitions: Vec::new(),
337            decode_memo: HashMap::new(),
338            all_links: Vec::new(),
339        }
340    }
341
342    /// Reset the encoder state for a new encoding operation.
343    fn reset_encode_state(&mut self) {
344        self.encode_counter = 0;
345        self.encode_memo.clear();
346        self.needs_id.clear();
347        self.all_definitions.clear();
348    }
349
350    /// Reset the decoder state for a new decoding operation.
351    fn reset_decode_state(&mut self) {
352        self.decode_memo.clear();
353        self.all_links.clear();
354    }
355
356    /// Create a Link from string parts.
357    fn make_link(&self, parts: &[&str]) -> LiNo<String> {
358        let values: Vec<LiNo<String>> = parts.iter().map(|p| LiNo::Ref(p.to_string())).collect();
359        LiNo::Link { id: None, values }
360    }
361
362    /// Create a reference Link with just an ID.
363    fn make_ref(&self, id: &str) -> LiNo<String> {
364        LiNo::Ref(id.to_string())
365    }
366
367    /// Generate a unique object key for detecting shared references.
368    fn object_key(&self, value: &LinoValue) -> String {
369        // Use pointer-based identity
370        format!("{:p}", value)
371    }
372
373    /// First pass: identify which objects need IDs (referenced multiple times or circularly).
374    fn find_objects_needing_ids(&mut self, value: &LinoValue, seen: &mut HashMap<String, bool>) {
375        // Only track arrays and objects (compound types)
376        match value {
377            LinoValue::Array(arr) => {
378                let key = self.object_key(value);
379
380                if seen.contains_key(&key) {
381                    // Already seen - needs an ID
382                    self.needs_id.insert(key);
383                    return;
384                }
385
386                seen.insert(key, true);
387
388                for item in arr {
389                    self.find_objects_needing_ids(item, seen);
390                }
391            }
392            LinoValue::Object(obj) => {
393                let key = self.object_key(value);
394
395                if seen.contains_key(&key) {
396                    // Already seen - needs an ID
397                    self.needs_id.insert(key);
398                    return;
399                }
400
401                seen.insert(key, true);
402
403                for (_, v) in obj {
404                    self.find_objects_needing_ids(v, seen);
405                }
406            }
407            _ => {}
408        }
409    }
410
411    /// Encode a LinoValue to the readable, indented Links Notation format.
412    ///
413    /// This is the default representation: keys and values are written as plain
414    /// text, one per line, so the result can be read and reviewed directly.
415    /// See [`readable`] for the exact shape.
416    ///
417    /// # Arguments
418    ///
419    /// * `value` - The value to encode
420    ///
421    /// # Returns
422    ///
423    /// A string in readable Links Notation format
424    pub fn encode(&mut self, value: &LinoValue) -> String {
425        readable::encode(value, DEFAULT_INDENT)
426    }
427
428    /// Encode a LinoValue to the readable format using a custom indentation string.
429    ///
430    /// # Arguments
431    ///
432    /// * `value` - The value to encode
433    /// * `indent` - The indentation string used per nesting level (for example `"    "`)
434    pub fn encode_with_indent(&mut self, value: &LinoValue, indent: &str) -> String {
435        readable::encode(value, indent)
436    }
437
438    /// Encode a LinoValue to the readable, single-line Links Notation format.
439    ///
440    /// The document is written as plain text on one line, so one value is one
441    /// line of an append-only log: appending is a single write, `grep`, `tail -f`
442    /// and `wc -l` keep working, and a compactor can cut at any newline without
443    /// splitting a record. See [`readable`] for the exact shape.
444    ///
445    /// # Arguments
446    ///
447    /// * `value` - The value to encode
448    ///
449    /// # Returns
450    ///
451    /// A string in readable Links Notation format, containing no newline
452    pub fn encode_line(&mut self, value: &LinoValue) -> String {
453        readable::encode_line(value)
454    }
455
456    /// Encode a LinoValue to the compact, single-line Links Notation format.
457    ///
458    /// Every value is tagged with its type and every string is base64-encoded, so
459    /// the whole document fits on one line and carries no readable text. This was
460    /// the default before the readable format; callers now opt into it explicitly.
461    ///
462    /// # Arguments
463    ///
464    /// * `value` - The value to encode
465    ///
466    /// # Returns
467    ///
468    /// A string in compact Links Notation format
469    pub fn encode_compact(&mut self, value: &LinoValue) -> String {
470        self.reset_encode_state();
471
472        // First pass: identify which objects need IDs
473        let mut seen = HashMap::new();
474        self.find_objects_needing_ids(value, &mut seen);
475
476        // Encode the value
477        let mut visited = HashSet::new();
478        let main_link = self.encode_value(value, &mut visited, 0);
479
480        // If we have additional definitions, output them all as multi-link format
481        if !self.all_definitions.is_empty() {
482            let mut all_links = vec![main_link];
483
484            // Add all other definitions (avoid duplicates)
485            for (ref_id, link) in &self.all_definitions {
486                let main_id = match &all_links[0] {
487                    LiNo::Link { id: Some(id), .. } => Some(id.clone()),
488                    _ => None,
489                };
490                if main_id.as_ref() != Some(ref_id) {
491                    all_links.push(link.clone());
492                }
493            }
494
495            // Format as multi-link (newline separated)
496            all_links
497                .iter()
498                .map(Self::format_link)
499                .collect::<Vec<_>>()
500                .join("\n")
501        } else {
502            Self::format_link(&main_link)
503        }
504    }
505
506    /// Encode a LinoValue to the compact, base64 form.
507    ///
508    /// Alias of [`ObjectCodec::encode_compact`], named after what the form does to
509    /// its content: nothing in the output can be read without decoding it.
510    pub fn encode_obfuscated(&mut self, value: &LinoValue) -> String {
511        self.encode_compact(value)
512    }
513
514    /// Format a single link to its string representation.
515    fn format_link(link: &LiNo<String>) -> String {
516        match link {
517            LiNo::Ref(s) => s.clone(),
518            LiNo::Link { id, values } => {
519                let inner: Vec<String> = values.iter().map(Self::format_link).collect();
520
521                if let Some(link_id) = id {
522                    if inner.is_empty() {
523                        format!("({}:)", link_id)
524                    } else {
525                        format!("({}: {})", link_id, inner.join(" "))
526                    }
527                } else if inner.is_empty() {
528                    "()".to_string()
529                } else {
530                    format!("({})", inner.join(" "))
531                }
532            }
533        }
534    }
535
536    /// Encode a value into a Link.
537    fn encode_value(
538        &mut self,
539        value: &LinoValue,
540        visited: &mut HashSet<String>,
541        depth: usize,
542    ) -> LiNo<String> {
543        match value {
544            LinoValue::Null => self.make_link(&[type_ids::NULL]),
545
546            LinoValue::Bool(b) => {
547                if *b {
548                    self.make_link(&[type_ids::BOOL, "true"])
549                } else {
550                    self.make_link(&[type_ids::BOOL, "false"])
551                }
552            }
553
554            LinoValue::Int(i) => self.make_link(&[type_ids::INT, &i.to_string()]),
555
556            LinoValue::Float(f) => {
557                if f.is_nan() {
558                    self.make_link(&[type_ids::FLOAT, "NaN"])
559                } else if f.is_infinite() {
560                    if f.is_sign_positive() {
561                        self.make_link(&[type_ids::FLOAT, "Infinity"])
562                    } else {
563                        self.make_link(&[type_ids::FLOAT, "-Infinity"])
564                    }
565                } else {
566                    self.make_link(&[type_ids::FLOAT, &f.to_string()])
567                }
568            }
569
570            LinoValue::String(s) => {
571                let b64_encoded = BASE64.encode(s.as_bytes());
572                self.make_link(&[type_ids::STR, &b64_encoded])
573            }
574
575            LinoValue::Array(arr) => {
576                let obj_key = self.object_key(value);
577
578                // Check if we've already encoded this object
579                if let Some(ref_id) = self.encode_memo.get(&obj_key).cloned() {
580                    return self.make_ref(&ref_id);
581                }
582
583                // Check if this object needs an ID
584                let needs_id = self.needs_id.contains(&obj_key);
585
586                if needs_id {
587                    // Check for cycle
588                    if visited.contains(&obj_key) {
589                        // We're in a cycle - must have assigned ID already
590                        if let Some(ref_id) = self.encode_memo.get(&obj_key) {
591                            return self.make_ref(ref_id);
592                        }
593                    }
594
595                    // Assign an ID
596                    let ref_id = format!("obj_{}", self.encode_counter);
597                    self.encode_counter += 1;
598                    self.encode_memo.insert(obj_key.clone(), ref_id.clone());
599                    visited.insert(obj_key.clone());
600
601                    // Encode items
602                    let mut parts: Vec<LiNo<String>> = vec![LiNo::Ref(type_ids::ARRAY.to_string())];
603                    for item in arr {
604                        let item_link = self.encode_value(item, visited, depth + 1);
605                        parts.push(item_link);
606                    }
607
608                    let definition = LiNo::Link {
609                        id: Some(ref_id.clone()),
610                        values: parts,
611                    };
612
613                    // Store for multi-link output if not at top level
614                    if depth > 0 {
615                        self.all_definitions.push((ref_id.clone(), definition));
616                        return self.make_ref(&ref_id);
617                    }
618
619                    definition
620                } else {
621                    // No ID needed - simple array
622                    let mut parts: Vec<LiNo<String>> = vec![LiNo::Ref(type_ids::ARRAY.to_string())];
623                    for item in arr {
624                        let item_link = self.encode_value(item, visited, depth + 1);
625                        parts.push(item_link);
626                    }
627                    LiNo::Link {
628                        id: None,
629                        values: parts,
630                    }
631                }
632            }
633
634            LinoValue::Object(obj) => {
635                let obj_key = self.object_key(value);
636
637                // Check if we've already encoded this object
638                if let Some(ref_id) = self.encode_memo.get(&obj_key).cloned() {
639                    return self.make_ref(&ref_id);
640                }
641
642                // Check if this object needs an ID
643                let needs_id = self.needs_id.contains(&obj_key);
644
645                if needs_id {
646                    // Check for cycle
647                    if visited.contains(&obj_key) {
648                        // We're in a cycle - must have assigned ID already
649                        if let Some(ref_id) = self.encode_memo.get(&obj_key) {
650                            return self.make_ref(ref_id);
651                        }
652                    }
653
654                    // Assign an ID
655                    let ref_id = format!("obj_{}", self.encode_counter);
656                    self.encode_counter += 1;
657                    self.encode_memo.insert(obj_key.clone(), ref_id.clone());
658                    visited.insert(obj_key.clone());
659
660                    // Encode key-value pairs
661                    let mut parts: Vec<LiNo<String>> =
662                        vec![LiNo::Ref(type_ids::OBJECT.to_string())];
663                    for (k, v) in obj {
664                        let key_link =
665                            self.encode_value(&LinoValue::String(k.clone()), visited, depth + 1);
666                        let value_link = self.encode_value(v, visited, depth + 1);
667                        let pair = LiNo::Link {
668                            id: None,
669                            values: vec![key_link, value_link],
670                        };
671                        parts.push(pair);
672                    }
673
674                    let definition = LiNo::Link {
675                        id: Some(ref_id.clone()),
676                        values: parts,
677                    };
678
679                    // Store for multi-link output if not at top level
680                    if depth > 0 {
681                        self.all_definitions.push((ref_id.clone(), definition));
682                        return self.make_ref(&ref_id);
683                    }
684
685                    definition
686                } else {
687                    // No ID needed - simple object
688                    let mut parts: Vec<LiNo<String>> =
689                        vec![LiNo::Ref(type_ids::OBJECT.to_string())];
690                    for (k, v) in obj {
691                        let key_link =
692                            self.encode_value(&LinoValue::String(k.clone()), visited, depth + 1);
693                        let value_link = self.encode_value(v, visited, depth + 1);
694                        let pair = LiNo::Link {
695                            id: None,
696                            values: vec![key_link, value_link],
697                        };
698                        parts.push(pair);
699                    }
700                    LiNo::Link {
701                        id: None,
702                        values: parts,
703                    }
704                }
705            }
706        }
707    }
708
709    /// Decode Links Notation format to a LinoValue.
710    ///
711    /// # Arguments
712    ///
713    /// * `notation` - String in Links Notation format
714    ///
715    /// # Returns
716    ///
717    /// The reconstructed value, or an error
718    ///
719    /// Both the readable format and the compact (base64) format are accepted, so
720    /// files written by earlier versions keep working and migrate on next write.
721    pub fn decode(&mut self, notation: &str) -> Result<LinoValue, CodecError> {
722        if notation.trim().is_empty() {
723            return Ok(LinoValue::Null);
724        }
725
726        if is_compact_notation(notation) {
727            crate::debug::trace("codec.decode", || "compact notation detected".to_string());
728            return self.decode_compact(notation);
729        }
730
731        crate::debug::trace("codec.decode", || "readable notation detected".to_string());
732        readable::decode(notation)
733    }
734
735    /// Decode the readable, single-line Links Notation format.
736    ///
737    /// This is the exact inverse of [`ObjectCodec::encode_line`]. A document
738    /// spanning more than one line is rejected, so a reader that hands over one
739    /// record at a time cannot silently merge two records into one value.
740    ///
741    /// # Arguments
742    ///
743    /// * `notation` - One line in readable Links Notation format
744    ///
745    /// # Returns
746    ///
747    /// The reconstructed value, or an error
748    pub fn decode_line(&mut self, notation: &str) -> Result<LinoValue, CodecError> {
749        readable::decode_line(notation)
750    }
751
752    /// Decode the compact (base64) Links Notation format.
753    ///
754    /// # Arguments
755    ///
756    /// * `notation` - String in compact Links Notation format
757    ///
758    /// # Returns
759    ///
760    /// The reconstructed value, or an error
761    pub fn decode_compact(&mut self, notation: &str) -> Result<LinoValue, CodecError> {
762        self.reset_decode_state();
763
764        let links = parse_lino_to_links(notation)
765            .map_err(|e| CodecError::ParseError(format!("{:?}", e)))?;
766
767        if links.is_empty() {
768            return Ok(LinoValue::Null);
769        }
770
771        // Store all links for forward reference resolution
772        if links.len() > 1 {
773            self.all_links = links.clone();
774        }
775
776        // Decode the first link
777        self.decode_link(&links[0])
778    }
779
780    /// Decode a Link into a LinoValue.
781    fn decode_link(&mut self, link: &LiNo<String>) -> Result<LinoValue, CodecError> {
782        match link {
783            LiNo::Ref(id) => {
784                // Check if this is a reference to a previously decoded object
785                if let Some(value) = self.decode_memo.get(id) {
786                    return Ok(value.clone());
787                }
788
789                // Check if it's a forward reference
790                if id.starts_with("obj_") && !self.all_links.is_empty() {
791                    // Look for this ID in remaining links
792                    for other_link in self.all_links.clone() {
793                        if let LiNo::Link {
794                            id: Some(link_id), ..
795                        } = &other_link
796                        {
797                            if link_id == id {
798                                return self.decode_link(&other_link);
799                            }
800                        }
801                    }
802                    // Not found - return empty array as fallback
803                    let result = LinoValue::Array(vec![]);
804                    self.decode_memo.insert(id.clone(), result.clone());
805                    return Ok(result);
806                }
807
808                // Handle single-element type markers (parser returns Ref for single values like "(null)")
809                match id.as_str() {
810                    type_ids::NULL => return Ok(LinoValue::Null),
811                    type_ids::ARRAY => return Ok(LinoValue::Array(vec![])),
812                    type_ids::OBJECT => return Ok(LinoValue::Object(vec![])),
813                    type_ids::STR => return Ok(LinoValue::String(String::new())),
814                    _ => {}
815                }
816
817                // Just a plain string reference
818                Ok(LinoValue::String(id.clone()))
819            }
820
821            LiNo::Link { id, values } => {
822                // Check for self-reference ID (already in memo)
823                let self_ref_id = id.as_ref().filter(|i| i.starts_with("obj_"));
824                if let Some(ref_id) = self_ref_id {
825                    if let Some(value) = self.decode_memo.get(ref_id) {
826                        return Ok(value.clone());
827                    }
828                }
829
830                if values.is_empty() {
831                    return Ok(LinoValue::Null);
832                }
833
834                // Get the type marker from the first value
835                let type_marker = match &values[0] {
836                    LiNo::Ref(t) => t.as_str(),
837                    LiNo::Link { .. } => return Ok(LinoValue::Null),
838                };
839
840                match type_marker {
841                    type_ids::NULL => Ok(LinoValue::Null),
842
843                    type_ids::BOOL => {
844                        if values.len() > 1 {
845                            if let LiNo::Ref(val) = &values[1] {
846                                return Ok(LinoValue::Bool(val.eq_ignore_ascii_case("true")));
847                            }
848                        }
849                        Ok(LinoValue::Bool(false))
850                    }
851
852                    type_ids::INT => {
853                        if values.len() > 1 {
854                            if let LiNo::Ref(val) = &values[1] {
855                                if let Ok(i) = val.parse::<i64>() {
856                                    return Ok(LinoValue::Int(i));
857                                }
858                            }
859                        }
860                        Ok(LinoValue::Int(0))
861                    }
862
863                    type_ids::FLOAT => {
864                        if values.len() > 1 {
865                            if let LiNo::Ref(val) = &values[1] {
866                                return match val.as_str() {
867                                    "NaN" => Ok(LinoValue::Float(f64::NAN)),
868                                    "Infinity" => Ok(LinoValue::Float(f64::INFINITY)),
869                                    "-Infinity" => Ok(LinoValue::Float(f64::NEG_INFINITY)),
870                                    s => {
871                                        if let Ok(f) = s.parse::<f64>() {
872                                            Ok(LinoValue::Float(f))
873                                        } else {
874                                            Ok(LinoValue::Float(0.0))
875                                        }
876                                    }
877                                };
878                            }
879                        }
880                        Ok(LinoValue::Float(0.0))
881                    }
882
883                    type_ids::STR => {
884                        if values.len() > 1 {
885                            if let LiNo::Ref(b64_str) = &values[1] {
886                                if let Ok(bytes) = BASE64.decode(b64_str) {
887                                    if let Ok(s) = String::from_utf8(bytes) {
888                                        return Ok(LinoValue::String(s));
889                                    }
890                                }
891                                // If decode fails, return raw value
892                                return Ok(LinoValue::String(b64_str.clone()));
893                            }
894                        }
895                        Ok(LinoValue::String(String::new()))
896                    }
897
898                    type_ids::ARRAY => {
899                        // Create result array and register in memo early for circular refs
900                        let result_array = LinoValue::Array(vec![]);
901                        if let Some(ref_id) = self_ref_id {
902                            self.decode_memo.insert(ref_id.clone(), result_array);
903                        }
904
905                        // Decode items (skip type marker at index 0)
906                        let mut items = Vec::new();
907                        for item_link in values.iter().skip(1) {
908                            let decoded = self.decode_link(item_link)?;
909                            items.push(decoded);
910                        }
911
912                        let result = LinoValue::Array(items);
913
914                        // Update memo if needed
915                        if let Some(ref_id) = self_ref_id {
916                            self.decode_memo.insert(ref_id.clone(), result.clone());
917                        }
918
919                        Ok(result)
920                    }
921
922                    type_ids::OBJECT => {
923                        // Create result object and register in memo early for circular refs
924                        let result_object = LinoValue::Object(vec![]);
925                        if let Some(ref_id) = self_ref_id {
926                            self.decode_memo.insert(ref_id.clone(), result_object);
927                        }
928
929                        // Decode key-value pairs (skip type marker at index 0)
930                        let mut obj = Vec::new();
931                        for pair_link in values.iter().skip(1) {
932                            if let LiNo::Link { values: pair, .. } = pair_link {
933                                if pair.len() >= 2 {
934                                    let key = self.decode_link(&pair[0])?;
935                                    let value = self.decode_link(&pair[1])?;
936
937                                    // Key should be a string
938                                    if let LinoValue::String(k) = key {
939                                        obj.push((k, value));
940                                    }
941                                }
942                            }
943                        }
944
945                        let result = LinoValue::Object(obj);
946
947                        // Update memo if needed
948                        if let Some(ref_id) = self_ref_id {
949                            self.decode_memo.insert(ref_id.clone(), result.clone());
950                        }
951
952                        Ok(result)
953                    }
954
955                    unknown => Err(CodecError::UnknownType(unknown.to_string())),
956                }
957            }
958        }
959    }
960}
961
962/// Detect the compact (base64) format.
963///
964/// Compact output always starts a line with `(` immediately followed by a type
965/// marker — optionally preceded by an object id, as in `(obj_0: object …)`.
966/// Readable output never does: its first line is either a lone `(` or a scalar.
967/// Type markers that open a compact document, across all implementations.
968///
969/// The languages historically disagreed on three of them -- Python writes
970/// `None`/`list`/`dict` where JavaScript and Rust write `null`/`array`/`object`
971/// -- so every implementation accepts the union and can read a compact document
972/// written by any of the others.
973const COMPACT_TYPE_MARKERS: [&str; 10] = [
974    type_ids::NULL,
975    "None",
976    type_ids::BOOL,
977    type_ids::INT,
978    type_ids::FLOAT,
979    type_ids::STR,
980    type_ids::ARRAY,
981    "list",
982    type_ids::OBJECT,
983    "dict",
984];
985
986/// Markers a compact document writes without a payload, so `(null)` is a compact
987/// null while `(null 1)` is a readable line holding two values.
988const EMPTY_BODY_MARKERS: [&str; 2] = [type_ids::NULL, "None"];
989
990fn is_compact_notation(notation: &str) -> bool {
991    let Some(first_line) = notation.lines().map(str::trim).find(|l| !l.is_empty()) else {
992        return false;
993    };
994
995    let Some(rest) = first_line.strip_prefix('(') else {
996        return false;
997    };
998
999    // A compact document names the type of its value first, so a link that opens
1000    // another link straight away is the readable form, whose links nest.
1001    let (mut marker, mut rest) = split_token(rest.trim_start());
1002
1003    // Skip the `obj_N:` definition id, if present.
1004    if let Some(id) = marker.strip_suffix(':') {
1005        if !id.starts_with("obj_") {
1006            return false;
1007        }
1008        (marker, rest) = split_token(rest.trim_start());
1009    }
1010
1011    if !COMPACT_TYPE_MARKERS.contains(&marker) {
1012        return false;
1013    }
1014
1015    // A compact null is the whole link: `(null)`. A link that holds more than the
1016    // marker is a readable line whose first value happens to be null.
1017    if EMPTY_BODY_MARKERS.contains(&marker) {
1018        return rest.trim_start().starts_with(')');
1019    }
1020
1021    true
1022}
1023
1024/// Split off the first token of a link body: the text up to the next whitespace
1025/// or parenthesis. A body that opens with a parenthesis has no token of its own.
1026fn split_token(input: &str) -> (&str, &str) {
1027    let end = input
1028        .find(|c: char| c.is_whitespace() || c == '(' || c == ')')
1029        .unwrap_or(input.len());
1030    input.split_at(end)
1031}
1032
1033// Global codec instance for convenience functions
1034thread_local! {
1035    static DEFAULT_CODEC: std::cell::RefCell<ObjectCodec> = std::cell::RefCell::new(ObjectCodec::new());
1036}
1037
1038/// Encode a value to the readable, indented Links Notation format.
1039///
1040/// This is a convenience function that uses a thread-local codec instance.
1041///
1042/// # Arguments
1043///
1044/// * `value` - The value to encode
1045///
1046/// # Returns
1047///
1048/// A string in readable Links Notation format
1049///
1050/// # Example
1051///
1052/// ```rust
1053/// use lino_objects_codec::{encode, LinoValue};
1054///
1055/// let data = LinoValue::object([
1056///     ("name", LinoValue::String("Alice".to_string())),
1057///     ("age", LinoValue::Int(30)),
1058/// ]);
1059/// let encoded = encode(&data);
1060/// // Names and values are written as they are, one per line
1061/// assert_eq!(encoded, "(\n  name \"Alice\"\n  age 30\n)");
1062/// ```
1063pub fn encode(value: &LinoValue) -> String {
1064    DEFAULT_CODEC.with(|codec| codec.borrow_mut().encode(value))
1065}
1066
1067/// Encode a value to the readable format using a custom indentation string.
1068///
1069/// # Arguments
1070///
1071/// * `value` - The value to encode
1072/// * `indent` - The indentation string used per nesting level
1073///
1074/// # Example
1075///
1076/// ```rust
1077/// use lino_objects_codec::{encode_with_indent, LinoValue};
1078///
1079/// let data = LinoValue::object([("age", LinoValue::Int(30))]);
1080/// assert_eq!(encode_with_indent(&data, "    "), "(\n    age 30\n)");
1081/// ```
1082pub fn encode_with_indent(value: &LinoValue, indent: &str) -> String {
1083    DEFAULT_CODEC.with(|codec| codec.borrow_mut().encode_with_indent(value, indent))
1084}
1085
1086/// Encode a value to the readable, single-line Links Notation format.
1087///
1088/// This is a convenience function that uses a thread-local codec instance.
1089///
1090/// The output holds no newline, so a record written with it is a line: an
1091/// append-only log stays greppable, tailable and countable by `wc -l`, and the
1092/// values in it can be read without decoding anything.
1093///
1094/// # Arguments
1095///
1096/// * `value` - The value to encode
1097///
1098/// # Returns
1099///
1100/// A string in readable Links Notation format, containing no newline
1101///
1102/// # Example
1103///
1104/// ```rust
1105/// use lino_objects_codec::{decode_line, encode_line, LinoValue};
1106///
1107/// let record = LinoValue::object([
1108///     ("bytes", LinoValue::Int(2827)),
1109///     ("complete", LinoValue::Bool(true)),
1110///     ("phase", LinoValue::String("stream_end".to_string())),
1111/// ]);
1112/// let line = encode_line(&record);
1113/// assert_eq!(line, r#"(o: (bytes 2827) (complete true) (phase "stream_end"))"#);
1114/// assert_eq!(decode_line(&line).unwrap(), record);
1115/// ```
1116pub fn encode_line(value: &LinoValue) -> String {
1117    DEFAULT_CODEC.with(|codec| codec.borrow_mut().encode_line(value))
1118}
1119
1120/// Decode one line of readable Links Notation back into a value.
1121///
1122/// This is a convenience function that uses a thread-local codec instance. It is
1123/// the exact inverse of [`encode_line`]; input spanning more than one line is an
1124/// error rather than a silently merged value.
1125///
1126/// # Arguments
1127///
1128/// * `notation` - One line in readable Links Notation format
1129///
1130/// # Returns
1131///
1132/// The reconstructed value, or an error
1133///
1134/// # Example
1135///
1136/// ```rust
1137/// use lino_objects_codec::{decode_line, LinoValue};
1138///
1139/// let decoded = decode_line("(o: (id 1) (tags (\"a\" \"b\")))").unwrap();
1140/// assert_eq!(
1141///     decoded,
1142///     LinoValue::object([
1143///         ("id", LinoValue::Int(1)),
1144///         (
1145///             "tags",
1146///             LinoValue::array([
1147///                 LinoValue::String("a".to_string()),
1148///                 LinoValue::String("b".to_string()),
1149///             ])
1150///         ),
1151///     ])
1152/// );
1153/// ```
1154pub fn decode_line(notation: &str) -> Result<LinoValue, CodecError> {
1155    DEFAULT_CODEC.with(|codec| codec.borrow_mut().decode_line(notation))
1156}
1157
1158/// Encode a value to the compact, single-line Links Notation format.
1159///
1160/// Every string is base64-encoded and the whole document is written on one line.
1161/// [`decode`] reads this form as well, so stored files remain readable by the
1162/// library after switching to the default readable output.
1163///
1164/// # Arguments
1165///
1166/// * `value` - The value to encode
1167///
1168/// # Returns
1169///
1170/// A string in compact Links Notation format
1171///
1172/// # Example
1173///
1174/// ```rust
1175/// use lino_objects_codec::{encode_compact, decode, LinoValue};
1176///
1177/// let data = LinoValue::object([("name", LinoValue::String("Alice".to_string()))]);
1178/// let encoded = encode_compact(&data);
1179/// // String "Alice" is base64-encoded as "QWxpY2U="
1180/// assert!(encoded.contains("QWxpY2U="));
1181/// assert_eq!(decode(&encoded).unwrap(), data);
1182/// ```
1183pub fn encode_compact(value: &LinoValue) -> String {
1184    DEFAULT_CODEC.with(|codec| codec.borrow_mut().encode_compact(value))
1185}
1186
1187/// Encode a value to the compact, base64 form.
1188///
1189/// Alias of [`encode_compact`], named after what the form does to its content.
1190pub fn encode_obfuscated(value: &LinoValue) -> String {
1191    DEFAULT_CODEC.with(|codec| codec.borrow_mut().encode_obfuscated(value))
1192}
1193
1194/// Decode Links Notation format to a value.
1195///
1196/// This is a convenience function that uses a thread-local codec instance.
1197///
1198/// # Arguments
1199///
1200/// * `notation` - String in Links Notation format
1201///
1202/// # Returns
1203///
1204/// The reconstructed value, or an error
1205///
1206/// # Example
1207///
1208/// ```rust
1209/// use lino_objects_codec::{encode, decode, LinoValue};
1210///
1211/// let original = LinoValue::Int(42);
1212/// let encoded = encode(&original);
1213/// let decoded = decode(&encoded).unwrap();
1214/// assert_eq!(decoded, original);
1215/// ```
1216pub fn decode(notation: &str) -> Result<LinoValue, CodecError> {
1217    DEFAULT_CODEC.with(|codec| codec.borrow_mut().decode(notation))
1218}
1219
1220/// Formatting utilities for indented Links Notation format.
1221pub mod format {
1222    use super::{LiNo, parse_lino_to_links};
1223    use std::collections::HashMap;
1224
1225    /// Error types for format operations
1226    #[derive(Debug, Clone, PartialEq, Eq)]
1227    pub enum FormatError {
1228        /// Missing required field
1229        MissingField(String),
1230        /// Invalid input
1231        InvalidInput(String),
1232    }
1233
1234    impl std::fmt::Display for FormatError {
1235        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1236            match self {
1237                FormatError::MissingField(field) => write!(f, "Missing required field: {}", field),
1238                FormatError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
1239            }
1240        }
1241    }
1242
1243    impl std::error::Error for FormatError {}
1244
1245    /// Escape a reference for Links Notation.
1246    ///
1247    /// References need escaping when they contain spaces, quotes, parentheses, colons, or newlines.
1248    ///
1249    /// # Arguments
1250    ///
1251    /// * `value` - The value to escape
1252    ///
1253    /// # Returns
1254    ///
1255    /// The escaped reference string
1256    pub fn escape_reference(value: &str) -> String {
1257        // Check if escaping is needed
1258        let needs_escaping = value.chars().any(|c| {
1259            c.is_whitespace() || c == '(' || c == ')' || c == '\'' || c == '"' || c == ':'
1260        }) || value.contains('\n');
1261
1262        if !needs_escaping {
1263            return value.to_string();
1264        }
1265
1266        let has_single = value.contains('\'');
1267        let has_double = value.contains('"');
1268
1269        // If contains single quotes but not double quotes, use double quotes
1270        if has_single && !has_double {
1271            return format!("\"{}\"", value);
1272        }
1273
1274        // If contains double quotes but not single quotes, use single quotes
1275        if has_double && !has_single {
1276            return format!("'{}'", value);
1277        }
1278
1279        // If contains both quotes, count which one appears more
1280        if has_single && has_double {
1281            let single_count = value.chars().filter(|&c| c == '\'').count();
1282            let double_count = value.chars().filter(|&c| c == '"').count();
1283
1284            if double_count < single_count {
1285                // Use double quotes, escape internal double quotes by doubling
1286                let escaped = value.replace('"', "\"\"");
1287                return format!("\"{}\"", escaped);
1288            }
1289            // Use single quotes, escape internal single quotes by doubling
1290            let escaped = value.replace('\'', "''");
1291            return format!("'{}'", escaped);
1292        }
1293
1294        // Just spaces or other special characters, use single quotes by default
1295        format!("'{}'", value)
1296    }
1297
1298    /// Unescape a reference from Links Notation format.
1299    ///
1300    /// Reverses the escaping done by escape_reference.
1301    ///
1302    /// # Arguments
1303    ///
1304    /// * `s` - The escaped reference string
1305    ///
1306    /// # Returns
1307    ///
1308    /// The unescaped string
1309    pub fn unescape_reference(s: &str) -> String {
1310        s.replace("\"\"", "\"").replace("''", "'")
1311    }
1312
1313    /// Format a value for display in indented Links Notation.
1314    /// Uses quoting strategy compatible with the links-notation parser:
1315    /// - If value contains double quotes, wrap in single quotes
1316    /// - Otherwise, wrap in double quotes
1317    fn format_indented_value(value: &str) -> String {
1318        let has_single = value.contains('\'');
1319        let has_double = value.contains('"');
1320
1321        // If contains double quotes but no single quotes, use single quotes
1322        if has_double && !has_single {
1323            return format!("'{}'", value);
1324        }
1325
1326        // If contains single quotes but no double quotes, use double quotes
1327        if has_single && !has_double {
1328            return format!("\"{}\"", value);
1329        }
1330
1331        // If contains both, use single quotes and escape internal single quotes
1332        if has_single && has_double {
1333            let escaped = value.replace('\'', "''");
1334            return format!("'{}'", escaped);
1335        }
1336
1337        // Default: use double quotes
1338        format!("\"{}\"", value)
1339    }
1340
1341    /// Format an object in indented Links Notation format.
1342    ///
1343    /// This format is designed for human readability, displaying objects as:
1344    ///
1345    /// ```text
1346    /// <identifier>
1347    ///   <key> "<value>"
1348    ///   <key> "<value>"
1349    ///   ...
1350    /// ```
1351    ///
1352    /// # Arguments
1353    ///
1354    /// * `id` - The object identifier (displayed on first line)
1355    /// * `obj` - The object as key-value pairs to format
1356    /// * `indent` - The indentation string (default: 2 spaces)
1357    ///
1358    /// # Returns
1359    ///
1360    /// Formatted indented Links Notation string, or an error
1361    ///
1362    /// # Example
1363    ///
1364    /// ```rust
1365    /// use lino_objects_codec::format::format_indented;
1366    /// use std::collections::HashMap;
1367    ///
1368    /// let mut obj = HashMap::new();
1369    /// obj.insert("status".to_string(), "executed".to_string());
1370    /// obj.insert("exitCode".to_string(), "0".to_string());
1371    ///
1372    /// let result = format_indented("my-uuid", &obj, "  ").unwrap();
1373    /// assert!(result.starts_with("my-uuid\n"));
1374    /// ```
1375    pub fn format_indented<S: ::std::hash::BuildHasher>(
1376        id: &str,
1377        obj: &HashMap<String, String, S>,
1378        indent: &str,
1379    ) -> Result<String, FormatError> {
1380        if id.is_empty() {
1381            return Err(FormatError::MissingField("id".to_string()));
1382        }
1383
1384        let mut lines = vec![id.to_string()];
1385
1386        for (key, value) in obj {
1387            let escaped_key = escape_reference(key);
1388            let formatted_value = format_indented_value(value);
1389            lines.push(format!("{}{} {}", indent, escaped_key, formatted_value));
1390        }
1391
1392        Ok(lines.join("\n"))
1393    }
1394
1395    /// Format an object in indented Links Notation format, maintaining key order.
1396    ///
1397    /// This is similar to `format_indented` but takes a slice of tuples to preserve
1398    /// the order of keys.
1399    ///
1400    /// # Arguments
1401    ///
1402    /// * `id` - The object identifier (displayed on first line)
1403    /// * `pairs` - The key-value pairs in order
1404    /// * `indent` - The indentation string (default: 2 spaces)
1405    ///
1406    /// # Returns
1407    ///
1408    /// Formatted indented Links Notation string, or an error
1409    pub fn format_indented_ordered(
1410        id: &str,
1411        pairs: &[(&str, &str)],
1412        indent: &str,
1413    ) -> Result<String, FormatError> {
1414        if id.is_empty() {
1415            return Err(FormatError::MissingField("id".to_string()));
1416        }
1417
1418        let mut lines = vec![id.to_string()];
1419
1420        for (key, value) in pairs {
1421            let escaped_key = escape_reference(key);
1422            let formatted_value = format_indented_value(value);
1423            lines.push(format!("{}{} {}", indent, escaped_key, formatted_value));
1424        }
1425
1426        Ok(lines.join("\n"))
1427    }
1428
1429    /// Parse an indented Links Notation string back to an object.
1430    ///
1431    /// This function uses the links-notation parser for proper parsing,
1432    /// supporting the standard Links Notation indented syntax.
1433    ///
1434    /// Parses strings like:
1435    ///
1436    /// ```text
1437    /// <identifier>
1438    ///   <key> "<value>"
1439    ///   <key> "<value>"
1440    ///   ...
1441    /// ```
1442    ///
1443    /// The format with colon after identifier is also supported (standard lino):
1444    ///
1445    /// ```text
1446    /// <identifier>:
1447    ///   <key> "<value>"
1448    /// ```
1449    ///
1450    /// # Arguments
1451    ///
1452    /// * `text` - The indented Links Notation string to parse
1453    ///
1454    /// # Returns
1455    ///
1456    /// A tuple of (id, HashMap of key-value pairs), or an error
1457    ///
1458    /// # Example
1459    ///
1460    /// ```rust
1461    /// use lino_objects_codec::format::parse_indented;
1462    ///
1463    /// let text = "my-uuid\n  status \"executed\"\n  exitCode \"0\"";
1464    /// let (id, obj) = parse_indented(text).unwrap();
1465    /// assert_eq!(id, "my-uuid");
1466    /// assert_eq!(obj.get("status"), Some(&"executed".to_string()));
1467    /// ```
1468    pub fn parse_indented(text: &str) -> Result<(String, HashMap<String, String>), FormatError> {
1469        if text.is_empty() {
1470            return Err(FormatError::InvalidInput(
1471                "text is required for parse_indented".to_string(),
1472            ));
1473        }
1474
1475        let lines: Vec<&str> = text.lines().collect();
1476        if lines.is_empty() {
1477            return Err(FormatError::InvalidInput(
1478                "text must have at least one line (the identifier)".to_string(),
1479            ));
1480        }
1481
1482        // Filter out empty lines to preserve indentation structure for the parser
1483        // Empty lines would break the indentation context in links-notation
1484        let non_empty_lines: Vec<&str> = lines
1485            .iter()
1486            .filter(|l| !l.trim().is_empty())
1487            .copied()
1488            .collect();
1489
1490        if non_empty_lines.is_empty() {
1491            return Err(FormatError::InvalidInput(
1492                "text must have at least one non-empty line (the identifier)".to_string(),
1493            ));
1494        }
1495
1496        // Convert to standard lino format by adding colon after first line if not present
1497        // This allows the links-notation parser to properly parse the indented structure
1498        let first_line = non_empty_lines[0].trim();
1499        let lino_text = if first_line.ends_with(':') {
1500            non_empty_lines.join("\n")
1501        } else {
1502            format!("{}:\n{}", first_line, non_empty_lines[1..].join("\n"))
1503        };
1504
1505        // Use links-notation parser
1506        let parsed = parse_lino_to_links(&lino_text)
1507            .map_err(|e| FormatError::InvalidInput(format!("Parse error: {:?}", e)))?;
1508
1509        if parsed.is_empty() {
1510            return Err(FormatError::InvalidInput(
1511                "Failed to parse indented Links Notation".to_string(),
1512            ));
1513        }
1514
1515        // Extract id and key-value pairs from parsed result
1516        let main_link = &parsed[0];
1517        let (result_id, values) = match main_link {
1518            LiNo::Link { id, values } => (id.clone().unwrap_or_default(), values),
1519            LiNo::Ref(id) => (id.clone(), &vec![]),
1520        };
1521
1522        let mut obj = HashMap::new();
1523
1524        // Process the values array - each entry is a doublet (key value)
1525        for child in values {
1526            if let LiNo::Link {
1527                values: child_values,
1528                ..
1529            } = child
1530            {
1531                if child_values.len() == 2 {
1532                    let key_ref = &child_values[0];
1533                    let value_ref = &child_values[1];
1534
1535                    // Get key string
1536                    let key = match key_ref {
1537                        LiNo::Ref(k) => k.clone(),
1538                        LiNo::Link { id, .. } => id.clone().unwrap_or_default(),
1539                    };
1540
1541                    // Get value string
1542                    let value = match value_ref {
1543                        LiNo::Ref(v) => v.clone(),
1544                        LiNo::Link { id, .. } => id.clone().unwrap_or_default(),
1545                    };
1546
1547                    obj.insert(key, value);
1548                }
1549            }
1550        }
1551
1552        Ok((result_id, obj))
1553    }
1554}
1555
1556#[cfg(test)]
1557mod tests {
1558    use super::*;
1559
1560    #[test]
1561    fn test_roundtrip_null() {
1562        let original = LinoValue::Null;
1563        let encoded = encode(&original);
1564        let decoded = decode(&encoded).unwrap();
1565        assert_eq!(decoded, original);
1566    }
1567
1568    #[test]
1569    fn test_roundtrip_bool() {
1570        for value in [true, false] {
1571            let original = LinoValue::Bool(value);
1572            let encoded = encode(&original);
1573            let decoded = decode(&encoded).unwrap();
1574            assert_eq!(decoded, original);
1575        }
1576    }
1577
1578    #[test]
1579    fn test_roundtrip_int() {
1580        let test_values: Vec<i64> = vec![0, 1, -1, 42, -42, 123456789, -123456789];
1581        for value in test_values {
1582            let original = LinoValue::Int(value);
1583            let encoded = encode(&original);
1584            let decoded = decode(&encoded).unwrap();
1585            assert_eq!(decoded.as_int(), Some(value));
1586        }
1587    }
1588
1589    #[test]
1590    fn test_roundtrip_float() {
1591        let test_values: Vec<f64> = vec![0.0, 1.0, -1.0, 3.14, -3.14, 0.123456789, -999.999];
1592        for value in test_values {
1593            let original = LinoValue::Float(value);
1594            let encoded = encode(&original);
1595            let decoded = decode(&encoded).unwrap();
1596            let decoded_f = decoded.as_float().unwrap();
1597            assert!((decoded_f - value).abs() < 0.0001);
1598        }
1599    }
1600
1601    #[test]
1602    fn test_float_special_values() {
1603        // Test infinity
1604        let inf = LinoValue::Float(f64::INFINITY);
1605        let encoded = encode(&inf);
1606        let decoded = decode(&encoded).unwrap();
1607        let decoded_f = decoded.as_float().unwrap();
1608        assert!(decoded_f.is_infinite());
1609        assert!(decoded_f.is_sign_positive());
1610
1611        // Test negative infinity
1612        let neg_inf = LinoValue::Float(f64::NEG_INFINITY);
1613        let encoded = encode(&neg_inf);
1614        let decoded = decode(&encoded).unwrap();
1615        let decoded_f = decoded.as_float().unwrap();
1616        assert!(decoded_f.is_infinite());
1617        assert!(decoded_f.is_sign_negative());
1618
1619        // Test NaN
1620        let nan = LinoValue::Float(f64::NAN);
1621        let encoded = encode(&nan);
1622        let decoded = decode(&encoded).unwrap();
1623        let decoded_f = decoded.as_float().unwrap();
1624        assert!(decoded_f.is_nan());
1625    }
1626
1627    #[test]
1628    fn test_roundtrip_string() {
1629        let test_values = [
1630            "",
1631            "hello",
1632            "hello world",
1633            "Hello, World!",
1634            "multi\nline\nstring",
1635            "tab\tseparated",
1636            "special chars: @#$%^&*()",
1637        ];
1638        for value in test_values {
1639            let original = LinoValue::String(value.to_string());
1640            let encoded = encode(&original);
1641            let decoded = decode(&encoded).unwrap();
1642            assert_eq!(decoded.as_str(), Some(value));
1643        }
1644    }
1645
1646    #[test]
1647    fn test_string_with_quotes() {
1648        let test_values = [
1649            "string with 'single quotes'",
1650            "string with \"double quotes\"",
1651            "string with \"both\" 'quotes'",
1652        ];
1653        for value in test_values {
1654            let original = LinoValue::String(value.to_string());
1655            let encoded = encode(&original);
1656            let decoded = decode(&encoded).unwrap();
1657            assert_eq!(decoded.as_str(), Some(value));
1658        }
1659    }
1660
1661    #[test]
1662    fn test_unicode_string() {
1663        let value = "unicode: 你好世界 🌍";
1664        let original = LinoValue::String(value.to_string());
1665        let encoded = encode(&original);
1666        let decoded = decode(&encoded).unwrap();
1667        assert_eq!(decoded.as_str(), Some(value));
1668    }
1669
1670    #[test]
1671    fn test_roundtrip_empty_array() {
1672        let original = LinoValue::Array(vec![]);
1673        let encoded = encode(&original);
1674        let decoded = decode(&encoded).unwrap();
1675        assert_eq!(decoded, original);
1676    }
1677
1678    #[test]
1679    fn test_roundtrip_simple_array() {
1680        let original = LinoValue::Array(vec![
1681            LinoValue::Int(1),
1682            LinoValue::Int(2),
1683            LinoValue::Int(3),
1684        ]);
1685        let encoded = encode(&original);
1686        let decoded = decode(&encoded).unwrap();
1687        assert_eq!(decoded, original);
1688    }
1689
1690    #[test]
1691    fn test_roundtrip_mixed_array() {
1692        let original = LinoValue::Array(vec![
1693            LinoValue::Int(1),
1694            LinoValue::String("hello".to_string()),
1695            LinoValue::Bool(true),
1696            LinoValue::Null,
1697        ]);
1698        let encoded = encode(&original);
1699        let decoded = decode(&encoded).unwrap();
1700        assert_eq!(decoded, original);
1701    }
1702
1703    #[test]
1704    fn test_nested_arrays() {
1705        let original = LinoValue::Array(vec![LinoValue::Array(vec![])]);
1706        let encoded = encode(&original);
1707        let decoded = decode(&encoded).unwrap();
1708        assert_eq!(decoded, original);
1709
1710        let original2 = LinoValue::Array(vec![
1711            LinoValue::Array(vec![LinoValue::Int(1), LinoValue::Int(2)]),
1712            LinoValue::Array(vec![LinoValue::Int(3), LinoValue::Int(4)]),
1713        ]);
1714        let encoded2 = encode(&original2);
1715        let decoded2 = decode(&encoded2).unwrap();
1716        assert_eq!(decoded2, original2);
1717    }
1718
1719    #[test]
1720    fn test_roundtrip_empty_object() {
1721        let original = LinoValue::Object(vec![]);
1722        let encoded = encode(&original);
1723        let decoded = decode(&encoded).unwrap();
1724        assert_eq!(decoded, original);
1725    }
1726
1727    #[test]
1728    fn test_roundtrip_simple_object() {
1729        let original = LinoValue::object([("a", LinoValue::Int(1)), ("b", LinoValue::Int(2))]);
1730        let encoded = encode(&original);
1731        let decoded = decode(&encoded).unwrap();
1732        assert_eq!(decoded, original);
1733    }
1734
1735    #[test]
1736    fn test_nested_objects() {
1737        let original = LinoValue::object([(
1738            "user",
1739            LinoValue::object([
1740                ("name", LinoValue::String("Alice".to_string())),
1741                (
1742                    "address",
1743                    LinoValue::object([
1744                        ("city", LinoValue::String("NYC".to_string())),
1745                        ("zip", LinoValue::String("10001".to_string())),
1746                    ]),
1747                ),
1748            ]),
1749        )]);
1750        let encoded = encode(&original);
1751        let decoded = decode(&encoded).unwrap();
1752        assert_eq!(decoded, original);
1753    }
1754
1755    #[test]
1756    fn test_complex_structure() {
1757        let original = LinoValue::object([
1758            ("id", LinoValue::Int(123)),
1759            ("name", LinoValue::String("Test Object".to_string())),
1760            ("active", LinoValue::Bool(true)),
1761            (
1762                "tags",
1763                LinoValue::array([
1764                    LinoValue::String("tag1".to_string()),
1765                    LinoValue::String("tag2".to_string()),
1766                    LinoValue::String("tag3".to_string()),
1767                ]),
1768            ),
1769            (
1770                "metadata",
1771                LinoValue::object([
1772                    ("created", LinoValue::String("2025-01-01".to_string())),
1773                    ("modified", LinoValue::Null),
1774                    ("count", LinoValue::Int(42)),
1775                ]),
1776            ),
1777            (
1778                "items",
1779                LinoValue::array([
1780                    LinoValue::object([
1781                        ("id", LinoValue::Int(1)),
1782                        ("value", LinoValue::String("first".to_string())),
1783                    ]),
1784                    LinoValue::object([
1785                        ("id", LinoValue::Int(2)),
1786                        ("value", LinoValue::String("second".to_string())),
1787                    ]),
1788                ]),
1789            ),
1790        ]);
1791        let encoded = encode(&original);
1792        let decoded = decode(&encoded).unwrap();
1793        assert_eq!(decoded, original);
1794    }
1795
1796    #[test]
1797    fn test_list_of_dicts() {
1798        let original = LinoValue::array([
1799            LinoValue::object([
1800                ("name", LinoValue::String("Alice".to_string())),
1801                ("age", LinoValue::Int(30)),
1802            ]),
1803            LinoValue::object([
1804                ("name", LinoValue::String("Bob".to_string())),
1805                ("age", LinoValue::Int(25)),
1806            ]),
1807        ]);
1808        let encoded = encode(&original);
1809        let decoded = decode(&encoded).unwrap();
1810        assert_eq!(decoded, original);
1811    }
1812
1813    #[test]
1814    fn test_dict_of_lists() {
1815        let original = LinoValue::object([
1816            (
1817                "numbers",
1818                LinoValue::array([LinoValue::Int(1), LinoValue::Int(2), LinoValue::Int(3)]),
1819            ),
1820            (
1821                "strings",
1822                LinoValue::array([
1823                    LinoValue::String("a".to_string()),
1824                    LinoValue::String("b".to_string()),
1825                    LinoValue::String("c".to_string()),
1826                ]),
1827            ),
1828        ]);
1829        let encoded = encode(&original);
1830        let decoded = decode(&encoded).unwrap();
1831        assert_eq!(decoded, original);
1832    }
1833}
1834
1835#[cfg(test)]
1836mod format_tests {
1837    use super::format::*;
1838    use std::collections::HashMap;
1839
1840    #[test]
1841    fn test_escape_reference_simple_string() {
1842        assert_eq!(escape_reference("hello"), "hello");
1843        assert_eq!(escape_reference("world"), "world");
1844    }
1845
1846    #[test]
1847    fn test_escape_reference_string_with_spaces() {
1848        let result = escape_reference("hello world");
1849        assert!(result.starts_with('\'') || result.starts_with('"'));
1850        assert!(result.contains("hello world"));
1851    }
1852
1853    #[test]
1854    fn test_escape_reference_string_with_single_quotes() {
1855        let result = escape_reference("it's");
1856        assert_eq!(result, "\"it's\"");
1857    }
1858
1859    #[test]
1860    fn test_escape_reference_string_with_double_quotes() {
1861        let result = escape_reference("he said \"hello\"");
1862        assert_eq!(result, "'he said \"hello\"'");
1863    }
1864
1865    #[test]
1866    fn test_unescape_reference_doubled_quotes() {
1867        assert_eq!(
1868            unescape_reference("he said \"\"hello\"\""),
1869            "he said \"hello\""
1870        );
1871        assert_eq!(unescape_reference("it''s"), "it's");
1872    }
1873
1874    #[test]
1875    fn test_format_indented_ordered_basic() {
1876        let pairs = [
1877            ("uuid", "6dcf4c1b-ff3f-482c-95ab-711ea7d1b019"),
1878            ("status", "executed"),
1879            ("command", "echo test"),
1880            ("exitCode", "0"),
1881        ];
1882        let result =
1883            format_indented_ordered("6dcf4c1b-ff3f-482c-95ab-711ea7d1b019", &pairs, "  ").unwrap();
1884        let lines: Vec<&str> = result.lines().collect();
1885        assert_eq!(lines[0], "6dcf4c1b-ff3f-482c-95ab-711ea7d1b019");
1886        assert_eq!(lines[1], "  uuid \"6dcf4c1b-ff3f-482c-95ab-711ea7d1b019\"");
1887        assert_eq!(lines[2], "  status \"executed\"");
1888        assert_eq!(lines[3], "  command \"echo test\"");
1889        assert_eq!(lines[4], "  exitCode \"0\"");
1890    }
1891
1892    #[test]
1893    fn test_format_indented_value_with_quotes() {
1894        // Values containing double quotes are wrapped in single quotes (links-notation style)
1895        let pairs = [("message", "He said \"hello\"")];
1896        let result = format_indented_ordered("test-id", &pairs, "  ").unwrap();
1897        let lines: Vec<&str> = result.lines().collect();
1898        assert_eq!(lines[1], "  message 'He said \"hello\"'");
1899    }
1900
1901    #[test]
1902    fn test_format_indented_requires_id() {
1903        let mut obj = HashMap::new();
1904        obj.insert("key".to_string(), "value".to_string());
1905        let result = format_indented("", &obj, "  ");
1906        assert!(result.is_err());
1907    }
1908
1909    #[test]
1910    fn test_parse_indented_basic() {
1911        let text = "6dcf4c1b-ff3f-482c-95ab-711ea7d1b019\n  uuid \"6dcf4c1b-ff3f-482c-95ab-711ea7d1b019\"\n  status \"executed\"\n  exitCode \"0\"";
1912        let (id, obj) = parse_indented(text).unwrap();
1913        assert_eq!(id, "6dcf4c1b-ff3f-482c-95ab-711ea7d1b019");
1914        assert_eq!(
1915            obj.get("uuid"),
1916            Some(&"6dcf4c1b-ff3f-482c-95ab-711ea7d1b019".to_string())
1917        );
1918        assert_eq!(obj.get("status"), Some(&"executed".to_string()));
1919        assert_eq!(obj.get("exitCode"), Some(&"0".to_string()));
1920    }
1921
1922    #[test]
1923    fn test_parse_indented_with_quotes() {
1924        // Links-notation style: use single quotes to wrap value containing double quotes
1925        let text = "test-id\n  message 'He said \"hello\"'";
1926        let (id, obj) = parse_indented(text).unwrap();
1927        assert_eq!(id, "test-id");
1928        assert_eq!(obj.get("message"), Some(&"He said \"hello\"".to_string()));
1929    }
1930
1931    #[test]
1932    fn test_parse_indented_empty_lines_skipped() {
1933        let text = "test-id\n\n  key \"value\"\n\n  another \"value2\"";
1934        let (id, obj) = parse_indented(text).unwrap();
1935        assert_eq!(id, "test-id");
1936        assert_eq!(obj.get("key"), Some(&"value".to_string()));
1937        assert_eq!(obj.get("another"), Some(&"value2".to_string()));
1938    }
1939
1940    #[test]
1941    fn test_parse_indented_requires_text() {
1942        let result = parse_indented("");
1943        assert!(result.is_err());
1944    }
1945
1946    #[test]
1947    fn test_roundtrip_format_indented() {
1948        let pairs = [
1949            ("uuid", "6dcf4c1b-ff3f-482c-95ab-711ea7d1b019"),
1950            ("status", "executed"),
1951            ("command", "echo test"),
1952            ("exitCode", "0"),
1953        ];
1954        let formatted =
1955            format_indented_ordered("6dcf4c1b-ff3f-482c-95ab-711ea7d1b019", &pairs, "  ").unwrap();
1956        let (parsed_id, parsed_obj) = parse_indented(&formatted).unwrap();
1957
1958        assert_eq!(parsed_id, "6dcf4c1b-ff3f-482c-95ab-711ea7d1b019");
1959        for (key, value) in pairs {
1960            assert_eq!(parsed_obj.get(key), Some(&value.to_string()));
1961        }
1962    }
1963
1964    #[test]
1965    fn test_roundtrip_with_quotes() {
1966        let pairs = [("message", "He said \"hello\"")];
1967        let formatted = format_indented_ordered("test-id", &pairs, "  ").unwrap();
1968        let (parsed_id, parsed_obj) = parse_indented(&formatted).unwrap();
1969
1970        assert_eq!(parsed_id, "test-id");
1971        assert_eq!(
1972            parsed_obj.get("message"),
1973            Some(&"He said \"hello\"".to_string())
1974        );
1975    }
1976}