1use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
49use links_notation::{parse_lino_to_links, LiNo};
50use std::collections::{HashMap, HashSet};
51use std::fmt;
52
53pub mod debug;
54pub mod readable;
55
56pub use readable::{BASE64_MARKER, DEFAULT_INDENT};
57
58mod type_ids {
60 pub const NULL: &str = "null";
61 pub const BOOL: &str = "bool";
62 pub const INT: &str = "int";
63 pub const FLOAT: &str = "float";
64 pub const STR: &str = "str";
65 pub const ARRAY: &str = "array";
66 pub const OBJECT: &str = "object";
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
71pub enum CodecError {
72 ParseError(String),
74 DecodeError(String),
76 UnknownType(String),
78}
79
80impl fmt::Display for CodecError {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 match self {
83 CodecError::ParseError(msg) => write!(f, "Parse error: {}", msg),
84 CodecError::DecodeError(msg) => write!(f, "Decode error: {}", msg),
85 CodecError::UnknownType(t) => write!(f, "Unknown type marker: {}", t),
86 }
87 }
88}
89
90impl std::error::Error for CodecError {}
91
92#[derive(Debug, Clone)]
97pub enum LinoValue {
98 Null,
100 Bool(bool),
102 Int(i64),
104 Float(f64),
106 String(String),
108 Array(Vec<LinoValue>),
110 Object(Vec<(String, LinoValue)>),
112}
113
114impl PartialEq for LinoValue {
115 fn eq(&self, other: &Self) -> bool {
116 match (self, other) {
117 (LinoValue::Null, LinoValue::Null) => true,
118 (LinoValue::Bool(a), LinoValue::Bool(b)) => a == b,
119 (LinoValue::Int(a), LinoValue::Int(b)) => a == b,
120 (LinoValue::Float(a), LinoValue::Float(b)) => {
121 if a.is_nan() && b.is_nan() {
123 true
124 } else {
125 a == b
126 }
127 }
128 (LinoValue::String(a), LinoValue::String(b)) => a == b,
129 (LinoValue::Array(a), LinoValue::Array(b)) => a == b,
130 (LinoValue::Object(a), LinoValue::Object(b)) => {
131 if a.len() != b.len() {
133 return false;
134 }
135 let a_map: HashMap<&str, &LinoValue> =
137 a.iter().map(|(k, v)| (k.as_str(), v)).collect();
138 let b_map: HashMap<&str, &LinoValue> =
139 b.iter().map(|(k, v)| (k.as_str(), v)).collect();
140 a_map == b_map
141 }
142 _ => false,
143 }
144 }
145}
146
147impl LinoValue {
148 pub fn object<I, K, V>(iter: I) -> Self
150 where
151 I: IntoIterator<Item = (K, V)>,
152 K: Into<String>,
153 V: Into<LinoValue>,
154 {
155 LinoValue::Object(
156 iter.into_iter()
157 .map(|(k, v)| (k.into(), v.into()))
158 .collect(),
159 )
160 }
161
162 pub fn array<I, V>(iter: I) -> Self
164 where
165 I: IntoIterator<Item = V>,
166 V: Into<LinoValue>,
167 {
168 LinoValue::Array(iter.into_iter().map(|v| v.into()).collect())
169 }
170
171 pub fn is_null(&self) -> bool {
173 matches!(self, LinoValue::Null)
174 }
175
176 pub fn as_bool(&self) -> Option<bool> {
178 match self {
179 LinoValue::Bool(b) => Some(*b),
180 _ => None,
181 }
182 }
183
184 pub fn as_int(&self) -> Option<i64> {
186 match self {
187 LinoValue::Int(i) => Some(*i),
188 _ => None,
189 }
190 }
191
192 pub fn as_float(&self) -> Option<f64> {
194 match self {
195 LinoValue::Float(f) => Some(*f),
196 LinoValue::Int(i) => Some(*i as f64),
197 _ => None,
198 }
199 }
200
201 pub fn as_str(&self) -> Option<&str> {
203 match self {
204 LinoValue::String(s) => Some(s),
205 _ => None,
206 }
207 }
208
209 pub fn as_array(&self) -> Option<&Vec<LinoValue>> {
211 match self {
212 LinoValue::Array(a) => Some(a),
213 _ => None,
214 }
215 }
216
217 pub fn as_object(&self) -> Option<&Vec<(String, LinoValue)>> {
219 match self {
220 LinoValue::Object(o) => Some(o),
221 _ => None,
222 }
223 }
224
225 pub fn get(&self, key: &str) -> Option<&LinoValue> {
227 match self {
228 LinoValue::Object(o) => o.iter().find(|(k, _)| k == key).map(|(_, v)| v),
229 _ => None,
230 }
231 }
232
233 pub fn get_index(&self, index: usize) -> Option<&LinoValue> {
235 match self {
236 LinoValue::Array(a) => a.get(index),
237 _ => None,
238 }
239 }
240}
241
242impl From<()> for LinoValue {
244 fn from(_: ()) -> Self {
245 LinoValue::Null
246 }
247}
248
249impl From<bool> for LinoValue {
250 fn from(b: bool) -> Self {
251 LinoValue::Bool(b)
252 }
253}
254
255impl From<i32> for LinoValue {
256 fn from(i: i32) -> Self {
257 LinoValue::Int(i as i64)
258 }
259}
260
261impl From<i64> for LinoValue {
262 fn from(i: i64) -> Self {
263 LinoValue::Int(i)
264 }
265}
266
267impl From<f64> for LinoValue {
268 fn from(f: f64) -> Self {
269 LinoValue::Float(f)
270 }
271}
272
273impl From<&str> for LinoValue {
274 fn from(s: &str) -> Self {
275 LinoValue::String(s.to_string())
276 }
277}
278
279impl From<String> for LinoValue {
280 fn from(s: String) -> Self {
281 LinoValue::String(s)
282 }
283}
284
285impl<T: Into<LinoValue>> From<Vec<T>> for LinoValue {
286 fn from(v: Vec<T>) -> Self {
287 LinoValue::Array(v.into_iter().map(|x| x.into()).collect())
288 }
289}
290
291impl<T: Into<LinoValue>> From<Option<T>> for LinoValue {
292 fn from(opt: Option<T>) -> Self {
293 match opt {
294 Some(v) => v.into(),
295 None => LinoValue::Null,
296 }
297 }
298}
299
300pub struct ObjectCodec {
306 encode_counter: usize,
308 encode_memo: HashMap<String, String>,
310 needs_id: HashSet<String>,
312 all_definitions: Vec<(String, LiNo<String>)>,
314 decode_memo: HashMap<String, LinoValue>,
316 all_links: Vec<LiNo<String>>,
318}
319
320impl Default for ObjectCodec {
321 fn default() -> Self {
322 Self::new()
323 }
324}
325
326impl ObjectCodec {
327 pub fn new() -> Self {
329 ObjectCodec {
330 encode_counter: 0,
331 encode_memo: HashMap::new(),
332 needs_id: HashSet::new(),
333 all_definitions: Vec::new(),
334 decode_memo: HashMap::new(),
335 all_links: Vec::new(),
336 }
337 }
338
339 fn reset_encode_state(&mut self) {
341 self.encode_counter = 0;
342 self.encode_memo.clear();
343 self.needs_id.clear();
344 self.all_definitions.clear();
345 }
346
347 fn reset_decode_state(&mut self) {
349 self.decode_memo.clear();
350 self.all_links.clear();
351 }
352
353 fn make_link(&self, parts: &[&str]) -> LiNo<String> {
355 let values: Vec<LiNo<String>> = parts.iter().map(|p| LiNo::Ref(p.to_string())).collect();
356 LiNo::Link { id: None, values }
357 }
358
359 fn make_ref(&self, id: &str) -> LiNo<String> {
361 LiNo::Ref(id.to_string())
362 }
363
364 fn object_key(&self, value: &LinoValue) -> String {
366 format!("{:p}", value)
368 }
369
370 fn find_objects_needing_ids(&mut self, value: &LinoValue, seen: &mut HashMap<String, bool>) {
372 match value {
374 LinoValue::Array(arr) => {
375 let key = self.object_key(value);
376
377 if seen.contains_key(&key) {
378 self.needs_id.insert(key);
380 return;
381 }
382
383 seen.insert(key, true);
384
385 for item in arr {
386 self.find_objects_needing_ids(item, seen);
387 }
388 }
389 LinoValue::Object(obj) => {
390 let key = self.object_key(value);
391
392 if seen.contains_key(&key) {
393 self.needs_id.insert(key);
395 return;
396 }
397
398 seen.insert(key, true);
399
400 for (_, v) in obj {
401 self.find_objects_needing_ids(v, seen);
402 }
403 }
404 _ => {}
405 }
406 }
407
408 pub fn encode(&mut self, value: &LinoValue) -> String {
422 readable::encode(value, DEFAULT_INDENT)
423 }
424
425 pub fn encode_with_indent(&mut self, value: &LinoValue, indent: &str) -> String {
432 readable::encode(value, indent)
433 }
434
435 pub fn encode_compact(&mut self, value: &LinoValue) -> String {
449 self.reset_encode_state();
450
451 let mut seen = HashMap::new();
453 self.find_objects_needing_ids(value, &mut seen);
454
455 let mut visited = HashSet::new();
457 let main_link = self.encode_value(value, &mut visited, 0);
458
459 if !self.all_definitions.is_empty() {
461 let mut all_links = vec![main_link];
462
463 for (ref_id, link) in &self.all_definitions {
465 let main_id = match &all_links[0] {
466 LiNo::Link { id: Some(id), .. } => Some(id.clone()),
467 _ => None,
468 };
469 if main_id.as_ref() != Some(ref_id) {
470 all_links.push(link.clone());
471 }
472 }
473
474 all_links
476 .iter()
477 .map(Self::format_link)
478 .collect::<Vec<_>>()
479 .join("\n")
480 } else {
481 Self::format_link(&main_link)
482 }
483 }
484
485 pub fn encode_obfuscated(&mut self, value: &LinoValue) -> String {
490 self.encode_compact(value)
491 }
492
493 fn format_link(link: &LiNo<String>) -> String {
495 match link {
496 LiNo::Ref(s) => s.clone(),
497 LiNo::Link { id, values } => {
498 let inner: Vec<String> = values.iter().map(Self::format_link).collect();
499
500 if let Some(link_id) = id {
501 if inner.is_empty() {
502 format!("({}:)", link_id)
503 } else {
504 format!("({}: {})", link_id, inner.join(" "))
505 }
506 } else if inner.is_empty() {
507 "()".to_string()
508 } else {
509 format!("({})", inner.join(" "))
510 }
511 }
512 }
513 }
514
515 fn encode_value(
517 &mut self,
518 value: &LinoValue,
519 visited: &mut HashSet<String>,
520 depth: usize,
521 ) -> LiNo<String> {
522 match value {
523 LinoValue::Null => self.make_link(&[type_ids::NULL]),
524
525 LinoValue::Bool(b) => {
526 if *b {
527 self.make_link(&[type_ids::BOOL, "true"])
528 } else {
529 self.make_link(&[type_ids::BOOL, "false"])
530 }
531 }
532
533 LinoValue::Int(i) => self.make_link(&[type_ids::INT, &i.to_string()]),
534
535 LinoValue::Float(f) => {
536 if f.is_nan() {
537 self.make_link(&[type_ids::FLOAT, "NaN"])
538 } else if f.is_infinite() {
539 if f.is_sign_positive() {
540 self.make_link(&[type_ids::FLOAT, "Infinity"])
541 } else {
542 self.make_link(&[type_ids::FLOAT, "-Infinity"])
543 }
544 } else {
545 self.make_link(&[type_ids::FLOAT, &f.to_string()])
546 }
547 }
548
549 LinoValue::String(s) => {
550 let b64_encoded = BASE64.encode(s.as_bytes());
551 self.make_link(&[type_ids::STR, &b64_encoded])
552 }
553
554 LinoValue::Array(arr) => {
555 let obj_key = self.object_key(value);
556
557 if let Some(ref_id) = self.encode_memo.get(&obj_key).cloned() {
559 return self.make_ref(&ref_id);
560 }
561
562 let needs_id = self.needs_id.contains(&obj_key);
564
565 if needs_id {
566 if visited.contains(&obj_key) {
568 if let Some(ref_id) = self.encode_memo.get(&obj_key) {
570 return self.make_ref(ref_id);
571 }
572 }
573
574 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 let mut parts: Vec<LiNo<String>> = vec![LiNo::Ref(type_ids::ARRAY.to_string())];
582 for item in arr {
583 let item_link = self.encode_value(item, visited, depth + 1);
584 parts.push(item_link);
585 }
586
587 let definition = LiNo::Link {
588 id: Some(ref_id.clone()),
589 values: parts,
590 };
591
592 if depth > 0 {
594 self.all_definitions.push((ref_id.clone(), definition));
595 return self.make_ref(&ref_id);
596 }
597
598 definition
599 } else {
600 let mut parts: Vec<LiNo<String>> = vec![LiNo::Ref(type_ids::ARRAY.to_string())];
602 for item in arr {
603 let item_link = self.encode_value(item, visited, depth + 1);
604 parts.push(item_link);
605 }
606 LiNo::Link {
607 id: None,
608 values: parts,
609 }
610 }
611 }
612
613 LinoValue::Object(obj) => {
614 let obj_key = self.object_key(value);
615
616 if let Some(ref_id) = self.encode_memo.get(&obj_key).cloned() {
618 return self.make_ref(&ref_id);
619 }
620
621 let needs_id = self.needs_id.contains(&obj_key);
623
624 if needs_id {
625 if visited.contains(&obj_key) {
627 if let Some(ref_id) = self.encode_memo.get(&obj_key) {
629 return self.make_ref(ref_id);
630 }
631 }
632
633 let ref_id = format!("obj_{}", self.encode_counter);
635 self.encode_counter += 1;
636 self.encode_memo.insert(obj_key.clone(), ref_id.clone());
637 visited.insert(obj_key.clone());
638
639 let mut parts: Vec<LiNo<String>> =
641 vec![LiNo::Ref(type_ids::OBJECT.to_string())];
642 for (k, v) in obj {
643 let key_link =
644 self.encode_value(&LinoValue::String(k.clone()), visited, depth + 1);
645 let value_link = self.encode_value(v, visited, depth + 1);
646 let pair = LiNo::Link {
647 id: None,
648 values: vec![key_link, value_link],
649 };
650 parts.push(pair);
651 }
652
653 let definition = LiNo::Link {
654 id: Some(ref_id.clone()),
655 values: parts,
656 };
657
658 if depth > 0 {
660 self.all_definitions.push((ref_id.clone(), definition));
661 return self.make_ref(&ref_id);
662 }
663
664 definition
665 } else {
666 let mut parts: Vec<LiNo<String>> =
668 vec![LiNo::Ref(type_ids::OBJECT.to_string())];
669 for (k, v) in obj {
670 let key_link =
671 self.encode_value(&LinoValue::String(k.clone()), visited, depth + 1);
672 let value_link = self.encode_value(v, visited, depth + 1);
673 let pair = LiNo::Link {
674 id: None,
675 values: vec![key_link, value_link],
676 };
677 parts.push(pair);
678 }
679 LiNo::Link {
680 id: None,
681 values: parts,
682 }
683 }
684 }
685 }
686 }
687
688 pub fn decode(&mut self, notation: &str) -> Result<LinoValue, CodecError> {
701 if notation.trim().is_empty() {
702 return Ok(LinoValue::Null);
703 }
704
705 if is_compact_notation(notation) {
706 crate::debug::trace("codec.decode", || "compact notation detected".to_string());
707 return self.decode_compact(notation);
708 }
709
710 crate::debug::trace("codec.decode", || "readable notation detected".to_string());
711 readable::decode(notation)
712 }
713
714 pub fn decode_compact(&mut self, notation: &str) -> Result<LinoValue, CodecError> {
724 self.reset_decode_state();
725
726 let links = parse_lino_to_links(notation)
727 .map_err(|e| CodecError::ParseError(format!("{:?}", e)))?;
728
729 if links.is_empty() {
730 return Ok(LinoValue::Null);
731 }
732
733 if links.len() > 1 {
735 self.all_links = links.clone();
736 }
737
738 self.decode_link(&links[0])
740 }
741
742 fn decode_link(&mut self, link: &LiNo<String>) -> Result<LinoValue, CodecError> {
744 match link {
745 LiNo::Ref(id) => {
746 if let Some(value) = self.decode_memo.get(id) {
748 return Ok(value.clone());
749 }
750
751 if id.starts_with("obj_") && !self.all_links.is_empty() {
753 for other_link in self.all_links.clone() {
755 if let LiNo::Link {
756 id: Some(link_id), ..
757 } = &other_link
758 {
759 if link_id == id {
760 return self.decode_link(&other_link);
761 }
762 }
763 }
764 let result = LinoValue::Array(vec![]);
766 self.decode_memo.insert(id.clone(), result.clone());
767 return Ok(result);
768 }
769
770 match id.as_str() {
772 type_ids::NULL => return Ok(LinoValue::Null),
773 type_ids::ARRAY => return Ok(LinoValue::Array(vec![])),
774 type_ids::OBJECT => return Ok(LinoValue::Object(vec![])),
775 type_ids::STR => return Ok(LinoValue::String(String::new())),
776 _ => {}
777 }
778
779 Ok(LinoValue::String(id.clone()))
781 }
782
783 LiNo::Link { id, values } => {
784 let self_ref_id = id.as_ref().filter(|i| i.starts_with("obj_"));
786 if let Some(ref_id) = self_ref_id {
787 if let Some(value) = self.decode_memo.get(ref_id) {
788 return Ok(value.clone());
789 }
790 }
791
792 if values.is_empty() {
793 return Ok(LinoValue::Null);
794 }
795
796 let type_marker = match &values[0] {
798 LiNo::Ref(t) => t.as_str(),
799 LiNo::Link { .. } => return Ok(LinoValue::Null),
800 };
801
802 match type_marker {
803 type_ids::NULL => Ok(LinoValue::Null),
804
805 type_ids::BOOL => {
806 if values.len() > 1 {
807 if let LiNo::Ref(val) = &values[1] {
808 return Ok(LinoValue::Bool(val.eq_ignore_ascii_case("true")));
809 }
810 }
811 Ok(LinoValue::Bool(false))
812 }
813
814 type_ids::INT => {
815 if values.len() > 1 {
816 if let LiNo::Ref(val) = &values[1] {
817 if let Ok(i) = val.parse::<i64>() {
818 return Ok(LinoValue::Int(i));
819 }
820 }
821 }
822 Ok(LinoValue::Int(0))
823 }
824
825 type_ids::FLOAT => {
826 if values.len() > 1 {
827 if let LiNo::Ref(val) = &values[1] {
828 return match val.as_str() {
829 "NaN" => Ok(LinoValue::Float(f64::NAN)),
830 "Infinity" => Ok(LinoValue::Float(f64::INFINITY)),
831 "-Infinity" => Ok(LinoValue::Float(f64::NEG_INFINITY)),
832 s => {
833 if let Ok(f) = s.parse::<f64>() {
834 Ok(LinoValue::Float(f))
835 } else {
836 Ok(LinoValue::Float(0.0))
837 }
838 }
839 };
840 }
841 }
842 Ok(LinoValue::Float(0.0))
843 }
844
845 type_ids::STR => {
846 if values.len() > 1 {
847 if let LiNo::Ref(b64_str) = &values[1] {
848 if let Ok(bytes) = BASE64.decode(b64_str) {
849 if let Ok(s) = String::from_utf8(bytes) {
850 return Ok(LinoValue::String(s));
851 }
852 }
853 return Ok(LinoValue::String(b64_str.clone()));
855 }
856 }
857 Ok(LinoValue::String(String::new()))
858 }
859
860 type_ids::ARRAY => {
861 let result_array = LinoValue::Array(vec![]);
863 if let Some(ref_id) = self_ref_id {
864 self.decode_memo.insert(ref_id.clone(), result_array);
865 }
866
867 let mut items = Vec::new();
869 for item_link in values.iter().skip(1) {
870 let decoded = self.decode_link(item_link)?;
871 items.push(decoded);
872 }
873
874 let result = LinoValue::Array(items);
875
876 if let Some(ref_id) = self_ref_id {
878 self.decode_memo.insert(ref_id.clone(), result.clone());
879 }
880
881 Ok(result)
882 }
883
884 type_ids::OBJECT => {
885 let result_object = LinoValue::Object(vec![]);
887 if let Some(ref_id) = self_ref_id {
888 self.decode_memo.insert(ref_id.clone(), result_object);
889 }
890
891 let mut obj = Vec::new();
893 for pair_link in values.iter().skip(1) {
894 if let LiNo::Link { values: pair, .. } = pair_link {
895 if pair.len() >= 2 {
896 let key = self.decode_link(&pair[0])?;
897 let value = self.decode_link(&pair[1])?;
898
899 if let LinoValue::String(k) = key {
901 obj.push((k, value));
902 }
903 }
904 }
905 }
906
907 let result = LinoValue::Object(obj);
908
909 if let Some(ref_id) = self_ref_id {
911 self.decode_memo.insert(ref_id.clone(), result.clone());
912 }
913
914 Ok(result)
915 }
916
917 unknown => Err(CodecError::UnknownType(unknown.to_string())),
918 }
919 }
920 }
921 }
922}
923
924const COMPACT_TYPE_MARKERS: [&str; 10] = [
936 type_ids::NULL,
937 "None",
938 type_ids::BOOL,
939 type_ids::INT,
940 type_ids::FLOAT,
941 type_ids::STR,
942 type_ids::ARRAY,
943 "list",
944 type_ids::OBJECT,
945 "dict",
946];
947
948fn is_compact_notation(notation: &str) -> bool {
949 let Some(first_line) = notation.lines().map(str::trim).find(|l| !l.is_empty()) else {
950 return false;
951 };
952
953 let Some(rest) = first_line.strip_prefix('(') else {
954 return false;
955 };
956
957 let mut tokens = rest
958 .split(|c: char| c.is_whitespace() || c == '(' || c == ')')
959 .filter(|t| !t.is_empty());
960
961 let Some(mut marker) = tokens.next() else {
962 return false;
963 };
964
965 if let Some(id) = marker.strip_suffix(':') {
967 if !id.starts_with("obj_") {
968 return false;
969 }
970 let Some(next) = tokens.next() else {
971 return false;
972 };
973 marker = next;
974 }
975
976 COMPACT_TYPE_MARKERS.contains(&marker)
977}
978
979thread_local! {
981 static DEFAULT_CODEC: std::cell::RefCell<ObjectCodec> = std::cell::RefCell::new(ObjectCodec::new());
982}
983
984pub fn encode(value: &LinoValue) -> String {
1010 DEFAULT_CODEC.with(|codec| codec.borrow_mut().encode(value))
1011}
1012
1013pub fn encode_with_indent(value: &LinoValue, indent: &str) -> String {
1029 DEFAULT_CODEC.with(|codec| codec.borrow_mut().encode_with_indent(value, indent))
1030}
1031
1032pub fn encode_compact(value: &LinoValue) -> String {
1058 DEFAULT_CODEC.with(|codec| codec.borrow_mut().encode_compact(value))
1059}
1060
1061pub fn encode_obfuscated(value: &LinoValue) -> String {
1065 DEFAULT_CODEC.with(|codec| codec.borrow_mut().encode_obfuscated(value))
1066}
1067
1068pub fn decode(notation: &str) -> Result<LinoValue, CodecError> {
1091 DEFAULT_CODEC.with(|codec| codec.borrow_mut().decode(notation))
1092}
1093
1094pub mod format {
1096 use super::{parse_lino_to_links, LiNo};
1097 use std::collections::HashMap;
1098
1099 #[derive(Debug, Clone, PartialEq, Eq)]
1101 pub enum FormatError {
1102 MissingField(String),
1104 InvalidInput(String),
1106 }
1107
1108 impl std::fmt::Display for FormatError {
1109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1110 match self {
1111 FormatError::MissingField(field) => write!(f, "Missing required field: {}", field),
1112 FormatError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
1113 }
1114 }
1115 }
1116
1117 impl std::error::Error for FormatError {}
1118
1119 pub fn escape_reference(value: &str) -> String {
1131 let needs_escaping = value.chars().any(|c| {
1133 c.is_whitespace() || c == '(' || c == ')' || c == '\'' || c == '"' || c == ':'
1134 }) || value.contains('\n');
1135
1136 if !needs_escaping {
1137 return value.to_string();
1138 }
1139
1140 let has_single = value.contains('\'');
1141 let has_double = value.contains('"');
1142
1143 if has_single && !has_double {
1145 return format!("\"{}\"", value);
1146 }
1147
1148 if has_double && !has_single {
1150 return format!("'{}'", value);
1151 }
1152
1153 if has_single && has_double {
1155 let single_count = value.chars().filter(|&c| c == '\'').count();
1156 let double_count = value.chars().filter(|&c| c == '"').count();
1157
1158 if double_count < single_count {
1159 let escaped = value.replace('"', "\"\"");
1161 return format!("\"{}\"", escaped);
1162 }
1163 let escaped = value.replace('\'', "''");
1165 return format!("'{}'", escaped);
1166 }
1167
1168 format!("'{}'", value)
1170 }
1171
1172 pub fn unescape_reference(s: &str) -> String {
1184 s.replace("\"\"", "\"").replace("''", "'")
1185 }
1186
1187 fn format_indented_value(value: &str) -> String {
1192 let has_single = value.contains('\'');
1193 let has_double = value.contains('"');
1194
1195 if has_double && !has_single {
1197 return format!("'{}'", value);
1198 }
1199
1200 if has_single && !has_double {
1202 return format!("\"{}\"", value);
1203 }
1204
1205 if has_single && has_double {
1207 let escaped = value.replace('\'', "''");
1208 return format!("'{}'", escaped);
1209 }
1210
1211 format!("\"{}\"", value)
1213 }
1214
1215 pub fn format_indented<S: ::std::hash::BuildHasher>(
1250 id: &str,
1251 obj: &HashMap<String, String, S>,
1252 indent: &str,
1253 ) -> Result<String, FormatError> {
1254 if id.is_empty() {
1255 return Err(FormatError::MissingField("id".to_string()));
1256 }
1257
1258 let mut lines = vec![id.to_string()];
1259
1260 for (key, value) in obj {
1261 let escaped_key = escape_reference(key);
1262 let formatted_value = format_indented_value(value);
1263 lines.push(format!("{}{} {}", indent, escaped_key, formatted_value));
1264 }
1265
1266 Ok(lines.join("\n"))
1267 }
1268
1269 pub fn format_indented_ordered(
1284 id: &str,
1285 pairs: &[(&str, &str)],
1286 indent: &str,
1287 ) -> Result<String, FormatError> {
1288 if id.is_empty() {
1289 return Err(FormatError::MissingField("id".to_string()));
1290 }
1291
1292 let mut lines = vec![id.to_string()];
1293
1294 for (key, value) in pairs {
1295 let escaped_key = escape_reference(key);
1296 let formatted_value = format_indented_value(value);
1297 lines.push(format!("{}{} {}", indent, escaped_key, formatted_value));
1298 }
1299
1300 Ok(lines.join("\n"))
1301 }
1302
1303 pub fn parse_indented(text: &str) -> Result<(String, HashMap<String, String>), FormatError> {
1343 if text.is_empty() {
1344 return Err(FormatError::InvalidInput(
1345 "text is required for parse_indented".to_string(),
1346 ));
1347 }
1348
1349 let lines: Vec<&str> = text.lines().collect();
1350 if lines.is_empty() {
1351 return Err(FormatError::InvalidInput(
1352 "text must have at least one line (the identifier)".to_string(),
1353 ));
1354 }
1355
1356 let non_empty_lines: Vec<&str> = lines
1359 .iter()
1360 .filter(|l| !l.trim().is_empty())
1361 .copied()
1362 .collect();
1363
1364 if non_empty_lines.is_empty() {
1365 return Err(FormatError::InvalidInput(
1366 "text must have at least one non-empty line (the identifier)".to_string(),
1367 ));
1368 }
1369
1370 let first_line = non_empty_lines[0].trim();
1373 let lino_text = if first_line.ends_with(':') {
1374 non_empty_lines.join("\n")
1375 } else {
1376 format!("{}:\n{}", first_line, non_empty_lines[1..].join("\n"))
1377 };
1378
1379 let parsed = parse_lino_to_links(&lino_text)
1381 .map_err(|e| FormatError::InvalidInput(format!("Parse error: {:?}", e)))?;
1382
1383 if parsed.is_empty() {
1384 return Err(FormatError::InvalidInput(
1385 "Failed to parse indented Links Notation".to_string(),
1386 ));
1387 }
1388
1389 let main_link = &parsed[0];
1391 let (result_id, values) = match main_link {
1392 LiNo::Link { id, values } => (id.clone().unwrap_or_default(), values),
1393 LiNo::Ref(id) => (id.clone(), &vec![]),
1394 };
1395
1396 let mut obj = HashMap::new();
1397
1398 for child in values {
1400 if let LiNo::Link {
1401 values: child_values,
1402 ..
1403 } = child
1404 {
1405 if child_values.len() == 2 {
1406 let key_ref = &child_values[0];
1407 let value_ref = &child_values[1];
1408
1409 let key = match key_ref {
1411 LiNo::Ref(k) => k.clone(),
1412 LiNo::Link { id, .. } => id.clone().unwrap_or_default(),
1413 };
1414
1415 let value = match value_ref {
1417 LiNo::Ref(v) => v.clone(),
1418 LiNo::Link { id, .. } => id.clone().unwrap_or_default(),
1419 };
1420
1421 obj.insert(key, value);
1422 }
1423 }
1424 }
1425
1426 Ok((result_id, obj))
1427 }
1428}
1429
1430#[cfg(test)]
1431mod tests {
1432 use super::*;
1433
1434 #[test]
1435 fn test_roundtrip_null() {
1436 let original = LinoValue::Null;
1437 let encoded = encode(&original);
1438 let decoded = decode(&encoded).unwrap();
1439 assert_eq!(decoded, original);
1440 }
1441
1442 #[test]
1443 fn test_roundtrip_bool() {
1444 for value in [true, false] {
1445 let original = LinoValue::Bool(value);
1446 let encoded = encode(&original);
1447 let decoded = decode(&encoded).unwrap();
1448 assert_eq!(decoded, original);
1449 }
1450 }
1451
1452 #[test]
1453 fn test_roundtrip_int() {
1454 let test_values: Vec<i64> = vec![0, 1, -1, 42, -42, 123456789, -123456789];
1455 for value in test_values {
1456 let original = LinoValue::Int(value);
1457 let encoded = encode(&original);
1458 let decoded = decode(&encoded).unwrap();
1459 assert_eq!(decoded.as_int(), Some(value));
1460 }
1461 }
1462
1463 #[test]
1464 fn test_roundtrip_float() {
1465 let test_values: Vec<f64> = vec![0.0, 1.0, -1.0, 3.14, -3.14, 0.123456789, -999.999];
1466 for value in test_values {
1467 let original = LinoValue::Float(value);
1468 let encoded = encode(&original);
1469 let decoded = decode(&encoded).unwrap();
1470 let decoded_f = decoded.as_float().unwrap();
1471 assert!((decoded_f - value).abs() < 0.0001);
1472 }
1473 }
1474
1475 #[test]
1476 fn test_float_special_values() {
1477 let inf = LinoValue::Float(f64::INFINITY);
1479 let encoded = encode(&inf);
1480 let decoded = decode(&encoded).unwrap();
1481 let decoded_f = decoded.as_float().unwrap();
1482 assert!(decoded_f.is_infinite());
1483 assert!(decoded_f.is_sign_positive());
1484
1485 let neg_inf = LinoValue::Float(f64::NEG_INFINITY);
1487 let encoded = encode(&neg_inf);
1488 let decoded = decode(&encoded).unwrap();
1489 let decoded_f = decoded.as_float().unwrap();
1490 assert!(decoded_f.is_infinite());
1491 assert!(decoded_f.is_sign_negative());
1492
1493 let nan = LinoValue::Float(f64::NAN);
1495 let encoded = encode(&nan);
1496 let decoded = decode(&encoded).unwrap();
1497 let decoded_f = decoded.as_float().unwrap();
1498 assert!(decoded_f.is_nan());
1499 }
1500
1501 #[test]
1502 fn test_roundtrip_string() {
1503 let test_values = [
1504 "",
1505 "hello",
1506 "hello world",
1507 "Hello, World!",
1508 "multi\nline\nstring",
1509 "tab\tseparated",
1510 "special chars: @#$%^&*()",
1511 ];
1512 for value in test_values {
1513 let original = LinoValue::String(value.to_string());
1514 let encoded = encode(&original);
1515 let decoded = decode(&encoded).unwrap();
1516 assert_eq!(decoded.as_str(), Some(value));
1517 }
1518 }
1519
1520 #[test]
1521 fn test_string_with_quotes() {
1522 let test_values = [
1523 "string with 'single quotes'",
1524 "string with \"double quotes\"",
1525 "string with \"both\" 'quotes'",
1526 ];
1527 for value in test_values {
1528 let original = LinoValue::String(value.to_string());
1529 let encoded = encode(&original);
1530 let decoded = decode(&encoded).unwrap();
1531 assert_eq!(decoded.as_str(), Some(value));
1532 }
1533 }
1534
1535 #[test]
1536 fn test_unicode_string() {
1537 let value = "unicode: 你好世界 🌍";
1538 let original = LinoValue::String(value.to_string());
1539 let encoded = encode(&original);
1540 let decoded = decode(&encoded).unwrap();
1541 assert_eq!(decoded.as_str(), Some(value));
1542 }
1543
1544 #[test]
1545 fn test_roundtrip_empty_array() {
1546 let original = LinoValue::Array(vec![]);
1547 let encoded = encode(&original);
1548 let decoded = decode(&encoded).unwrap();
1549 assert_eq!(decoded, original);
1550 }
1551
1552 #[test]
1553 fn test_roundtrip_simple_array() {
1554 let original = LinoValue::Array(vec![
1555 LinoValue::Int(1),
1556 LinoValue::Int(2),
1557 LinoValue::Int(3),
1558 ]);
1559 let encoded = encode(&original);
1560 let decoded = decode(&encoded).unwrap();
1561 assert_eq!(decoded, original);
1562 }
1563
1564 #[test]
1565 fn test_roundtrip_mixed_array() {
1566 let original = LinoValue::Array(vec![
1567 LinoValue::Int(1),
1568 LinoValue::String("hello".to_string()),
1569 LinoValue::Bool(true),
1570 LinoValue::Null,
1571 ]);
1572 let encoded = encode(&original);
1573 let decoded = decode(&encoded).unwrap();
1574 assert_eq!(decoded, original);
1575 }
1576
1577 #[test]
1578 fn test_nested_arrays() {
1579 let original = LinoValue::Array(vec![LinoValue::Array(vec![])]);
1580 let encoded = encode(&original);
1581 let decoded = decode(&encoded).unwrap();
1582 assert_eq!(decoded, original);
1583
1584 let original2 = LinoValue::Array(vec![
1585 LinoValue::Array(vec![LinoValue::Int(1), LinoValue::Int(2)]),
1586 LinoValue::Array(vec![LinoValue::Int(3), LinoValue::Int(4)]),
1587 ]);
1588 let encoded2 = encode(&original2);
1589 let decoded2 = decode(&encoded2).unwrap();
1590 assert_eq!(decoded2, original2);
1591 }
1592
1593 #[test]
1594 fn test_roundtrip_empty_object() {
1595 let original = LinoValue::Object(vec![]);
1596 let encoded = encode(&original);
1597 let decoded = decode(&encoded).unwrap();
1598 assert_eq!(decoded, original);
1599 }
1600
1601 #[test]
1602 fn test_roundtrip_simple_object() {
1603 let original = LinoValue::object([("a", LinoValue::Int(1)), ("b", LinoValue::Int(2))]);
1604 let encoded = encode(&original);
1605 let decoded = decode(&encoded).unwrap();
1606 assert_eq!(decoded, original);
1607 }
1608
1609 #[test]
1610 fn test_nested_objects() {
1611 let original = LinoValue::object([(
1612 "user",
1613 LinoValue::object([
1614 ("name", LinoValue::String("Alice".to_string())),
1615 (
1616 "address",
1617 LinoValue::object([
1618 ("city", LinoValue::String("NYC".to_string())),
1619 ("zip", LinoValue::String("10001".to_string())),
1620 ]),
1621 ),
1622 ]),
1623 )]);
1624 let encoded = encode(&original);
1625 let decoded = decode(&encoded).unwrap();
1626 assert_eq!(decoded, original);
1627 }
1628
1629 #[test]
1630 fn test_complex_structure() {
1631 let original = LinoValue::object([
1632 ("id", LinoValue::Int(123)),
1633 ("name", LinoValue::String("Test Object".to_string())),
1634 ("active", LinoValue::Bool(true)),
1635 (
1636 "tags",
1637 LinoValue::array([
1638 LinoValue::String("tag1".to_string()),
1639 LinoValue::String("tag2".to_string()),
1640 LinoValue::String("tag3".to_string()),
1641 ]),
1642 ),
1643 (
1644 "metadata",
1645 LinoValue::object([
1646 ("created", LinoValue::String("2025-01-01".to_string())),
1647 ("modified", LinoValue::Null),
1648 ("count", LinoValue::Int(42)),
1649 ]),
1650 ),
1651 (
1652 "items",
1653 LinoValue::array([
1654 LinoValue::object([
1655 ("id", LinoValue::Int(1)),
1656 ("value", LinoValue::String("first".to_string())),
1657 ]),
1658 LinoValue::object([
1659 ("id", LinoValue::Int(2)),
1660 ("value", LinoValue::String("second".to_string())),
1661 ]),
1662 ]),
1663 ),
1664 ]);
1665 let encoded = encode(&original);
1666 let decoded = decode(&encoded).unwrap();
1667 assert_eq!(decoded, original);
1668 }
1669
1670 #[test]
1671 fn test_list_of_dicts() {
1672 let original = LinoValue::array([
1673 LinoValue::object([
1674 ("name", LinoValue::String("Alice".to_string())),
1675 ("age", LinoValue::Int(30)),
1676 ]),
1677 LinoValue::object([
1678 ("name", LinoValue::String("Bob".to_string())),
1679 ("age", LinoValue::Int(25)),
1680 ]),
1681 ]);
1682 let encoded = encode(&original);
1683 let decoded = decode(&encoded).unwrap();
1684 assert_eq!(decoded, original);
1685 }
1686
1687 #[test]
1688 fn test_dict_of_lists() {
1689 let original = LinoValue::object([
1690 (
1691 "numbers",
1692 LinoValue::array([LinoValue::Int(1), LinoValue::Int(2), LinoValue::Int(3)]),
1693 ),
1694 (
1695 "strings",
1696 LinoValue::array([
1697 LinoValue::String("a".to_string()),
1698 LinoValue::String("b".to_string()),
1699 LinoValue::String("c".to_string()),
1700 ]),
1701 ),
1702 ]);
1703 let encoded = encode(&original);
1704 let decoded = decode(&encoded).unwrap();
1705 assert_eq!(decoded, original);
1706 }
1707}
1708
1709#[cfg(test)]
1710mod format_tests {
1711 use super::format::*;
1712 use std::collections::HashMap;
1713
1714 #[test]
1715 fn test_escape_reference_simple_string() {
1716 assert_eq!(escape_reference("hello"), "hello");
1717 assert_eq!(escape_reference("world"), "world");
1718 }
1719
1720 #[test]
1721 fn test_escape_reference_string_with_spaces() {
1722 let result = escape_reference("hello world");
1723 assert!(result.starts_with('\'') || result.starts_with('"'));
1724 assert!(result.contains("hello world"));
1725 }
1726
1727 #[test]
1728 fn test_escape_reference_string_with_single_quotes() {
1729 let result = escape_reference("it's");
1730 assert_eq!(result, "\"it's\"");
1731 }
1732
1733 #[test]
1734 fn test_escape_reference_string_with_double_quotes() {
1735 let result = escape_reference("he said \"hello\"");
1736 assert_eq!(result, "'he said \"hello\"'");
1737 }
1738
1739 #[test]
1740 fn test_unescape_reference_doubled_quotes() {
1741 assert_eq!(
1742 unescape_reference("he said \"\"hello\"\""),
1743 "he said \"hello\""
1744 );
1745 assert_eq!(unescape_reference("it''s"), "it's");
1746 }
1747
1748 #[test]
1749 fn test_format_indented_ordered_basic() {
1750 let pairs = [
1751 ("uuid", "6dcf4c1b-ff3f-482c-95ab-711ea7d1b019"),
1752 ("status", "executed"),
1753 ("command", "echo test"),
1754 ("exitCode", "0"),
1755 ];
1756 let result =
1757 format_indented_ordered("6dcf4c1b-ff3f-482c-95ab-711ea7d1b019", &pairs, " ").unwrap();
1758 let lines: Vec<&str> = result.lines().collect();
1759 assert_eq!(lines[0], "6dcf4c1b-ff3f-482c-95ab-711ea7d1b019");
1760 assert_eq!(lines[1], " uuid \"6dcf4c1b-ff3f-482c-95ab-711ea7d1b019\"");
1761 assert_eq!(lines[2], " status \"executed\"");
1762 assert_eq!(lines[3], " command \"echo test\"");
1763 assert_eq!(lines[4], " exitCode \"0\"");
1764 }
1765
1766 #[test]
1767 fn test_format_indented_value_with_quotes() {
1768 let pairs = [("message", "He said \"hello\"")];
1770 let result = format_indented_ordered("test-id", &pairs, " ").unwrap();
1771 let lines: Vec<&str> = result.lines().collect();
1772 assert_eq!(lines[1], " message 'He said \"hello\"'");
1773 }
1774
1775 #[test]
1776 fn test_format_indented_requires_id() {
1777 let mut obj = HashMap::new();
1778 obj.insert("key".to_string(), "value".to_string());
1779 let result = format_indented("", &obj, " ");
1780 assert!(result.is_err());
1781 }
1782
1783 #[test]
1784 fn test_parse_indented_basic() {
1785 let text = "6dcf4c1b-ff3f-482c-95ab-711ea7d1b019\n uuid \"6dcf4c1b-ff3f-482c-95ab-711ea7d1b019\"\n status \"executed\"\n exitCode \"0\"";
1786 let (id, obj) = parse_indented(text).unwrap();
1787 assert_eq!(id, "6dcf4c1b-ff3f-482c-95ab-711ea7d1b019");
1788 assert_eq!(
1789 obj.get("uuid"),
1790 Some(&"6dcf4c1b-ff3f-482c-95ab-711ea7d1b019".to_string())
1791 );
1792 assert_eq!(obj.get("status"), Some(&"executed".to_string()));
1793 assert_eq!(obj.get("exitCode"), Some(&"0".to_string()));
1794 }
1795
1796 #[test]
1797 fn test_parse_indented_with_quotes() {
1798 let text = "test-id\n message 'He said \"hello\"'";
1800 let (id, obj) = parse_indented(text).unwrap();
1801 assert_eq!(id, "test-id");
1802 assert_eq!(obj.get("message"), Some(&"He said \"hello\"".to_string()));
1803 }
1804
1805 #[test]
1806 fn test_parse_indented_empty_lines_skipped() {
1807 let text = "test-id\n\n key \"value\"\n\n another \"value2\"";
1808 let (id, obj) = parse_indented(text).unwrap();
1809 assert_eq!(id, "test-id");
1810 assert_eq!(obj.get("key"), Some(&"value".to_string()));
1811 assert_eq!(obj.get("another"), Some(&"value2".to_string()));
1812 }
1813
1814 #[test]
1815 fn test_parse_indented_requires_text() {
1816 let result = parse_indented("");
1817 assert!(result.is_err());
1818 }
1819
1820 #[test]
1821 fn test_roundtrip_format_indented() {
1822 let pairs = [
1823 ("uuid", "6dcf4c1b-ff3f-482c-95ab-711ea7d1b019"),
1824 ("status", "executed"),
1825 ("command", "echo test"),
1826 ("exitCode", "0"),
1827 ];
1828 let formatted =
1829 format_indented_ordered("6dcf4c1b-ff3f-482c-95ab-711ea7d1b019", &pairs, " ").unwrap();
1830 let (parsed_id, parsed_obj) = parse_indented(&formatted).unwrap();
1831
1832 assert_eq!(parsed_id, "6dcf4c1b-ff3f-482c-95ab-711ea7d1b019");
1833 for (key, value) in pairs {
1834 assert_eq!(parsed_obj.get(key), Some(&value.to_string()));
1835 }
1836 }
1837
1838 #[test]
1839 fn test_roundtrip_with_quotes() {
1840 let pairs = [("message", "He said \"hello\"")];
1841 let formatted = format_indented_ordered("test-id", &pairs, " ").unwrap();
1842 let (parsed_id, parsed_obj) = parse_indented(&formatted).unwrap();
1843
1844 assert_eq!(parsed_id, "test-id");
1845 assert_eq!(
1846 parsed_obj.get("message"),
1847 Some(&"He said \"hello\"".to_string())
1848 );
1849 }
1850}