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