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