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