1use crate::debug::trace;
79use crate::{CodecError, LinoValue};
80use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
81
82pub const DEFAULT_INDENT: &str = " ";
84
85pub const BASE64_MARKER: &str = "base64";
88
89pub const ESCAPED_MARKER: &str = "escaped";
95
96pub const OBJECT_MARKER: &str = "o";
98
99pub fn encode(value: &LinoValue, indent: &str) -> String {
101 let mut out = String::new();
102 write_value(value, indent, 0, &mut out);
103 out
104}
105
106pub fn encode_line(value: &LinoValue) -> String {
111 let mut out = String::new();
112 write_line_value(value, &mut out);
113 out
114}
115
116pub fn decode_line(text: &str) -> Result<LinoValue, CodecError> {
122 let line = text.trim_matches(|c: char| c == '\n' || c == '\r');
123 if line.contains('\n') || line.contains('\r') {
124 return Err(CodecError::ParseError(
125 "a single-line document cannot contain a line break".to_string(),
126 ));
127 }
128 decode(line)
129}
130
131pub fn decode(text: &str) -> Result<LinoValue, CodecError> {
133 let tokens = tokenize(text)?;
134 trace("readable.decode", || format!("{} tokens", tokens.len()));
135 let mut cursor = Cursor { tokens, pos: 0 };
136 let rows = cursor.parse_rows(true)?;
137
138 if cursor.pos < cursor.tokens.len() {
139 return Err(CodecError::ParseError(
140 "unexpected ')' in readable notation".to_string(),
141 ));
142 }
143
144 if rows.len() == 1 && rows[0].len() == 1 {
146 return node_to_value(&rows[0][0]);
147 }
148
149 rows_to_value(&rows, true, false)
150}
151
152pub fn format_value_single_line(value: &str) -> String {
154 format_string(value, Form::Line)
155}
156
157pub fn format_value_verbatim(value: &str) -> String {
160 format_string(value, Form::Indented)
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170enum Form {
171 Indented,
172 Line,
173}
174
175fn write_value(value: &LinoValue, indent: &str, level: usize, out: &mut String) {
176 match value {
177 LinoValue::Object(pairs) => {
178 if pairs.is_empty() {
179 out.push_str("(\n");
181 push_indent(indent, level, out);
182 out.push(')');
183 return;
184 }
185
186 out.push('(');
187 for (key, child) in pairs {
188 out.push('\n');
189 push_indent(indent, level + 1, out);
190 out.push_str(&format_key(key, Form::Indented));
191 out.push(' ');
192 write_value(child, indent, level + 1, out);
193 }
194 out.push('\n');
195 push_indent(indent, level, out);
196 out.push(')');
197 }
198
199 LinoValue::Array(items) => {
200 if items.is_empty() {
201 out.push_str("()");
202 return;
203 }
204
205 out.push('(');
206 for item in items {
207 out.push('\n');
208 push_indent(indent, level + 1, out);
209 write_value(item, indent, level + 1, out);
210 }
211 out.push('\n');
212 push_indent(indent, level, out);
213 out.push(')');
214 }
215
216 scalar => out.push_str(&format_scalar(scalar, Form::Indented)),
217 }
218}
219
220fn write_line_value(value: &LinoValue, out: &mut String) {
223 match value {
224 LinoValue::Object(pairs) => {
225 if pairs.is_empty() {
226 out.push('(');
228 out.push_str(OBJECT_MARKER);
229 out.push_str(":)");
230 return;
231 }
232
233 out.push('(');
234 out.push_str(OBJECT_MARKER);
235 out.push(':');
236 for (key, child) in pairs {
237 out.push_str(" (");
238 out.push_str(&format_key(key, Form::Line));
239 out.push(' ');
240 write_line_value(child, out);
241 out.push(')');
242 }
243 out.push(')');
244 }
245
246 LinoValue::Array(items) => {
247 out.push('(');
248 if matches!(items.as_slice(), [LinoValue::Null]) {
250 out.push_str("a: ");
251 }
252 for (index, item) in items.iter().enumerate() {
253 if index > 0 {
254 out.push(' ');
255 }
256 write_line_value(item, out);
257 }
258 out.push(')');
259 }
260
261 scalar => out.push_str(&format_scalar(scalar, Form::Line)),
262 }
263}
264
265fn push_indent(indent: &str, level: usize, out: &mut String) {
266 for _ in 0..level {
267 out.push_str(indent);
268 }
269}
270
271fn format_scalar(value: &LinoValue, form: Form) -> String {
274 match value {
275 LinoValue::Null => "null".to_string(),
276 LinoValue::Bool(b) => b.to_string(),
277 LinoValue::Int(i) => i.to_string(),
278 LinoValue::Float(f) => format_float(*f),
279 #[cfg(feature = "serde_json")]
280 LinoValue::JsonNumber(n) => format!("(json-number {})", quote(&n.to_string())),
281 LinoValue::String(s) => format_string(s, form),
282 LinoValue::Array(_) | LinoValue::Object(_) => String::new(),
284 }
285}
286
287fn format_float(f: f64) -> String {
288 if f.is_nan() {
289 "NaN".to_string()
290 } else if f.is_infinite() {
291 if f.is_sign_positive() {
292 "Infinity".to_string()
293 } else {
294 "-Infinity".to_string()
295 }
296 } else {
297 format!("{:?}", f)
300 }
301}
302
303fn format_string(value: &str, form: Form) -> String {
307 match escape_unwritable(value, form) {
308 Some(escaped) => format!("({} {})", ESCAPED_MARKER, quote(&escaped)),
309 None => quote(value),
310 }
311}
312
313fn escape_unwritable(value: &str, form: Form) -> Option<String> {
316 if !value.chars().any(|c| is_unwritable(c, form)) {
317 return None;
318 }
319
320 let mut out = String::with_capacity(value.len());
321 let mut buffer = [0u8; 4];
322 for c in value.chars() {
323 if c == '%' || is_unwritable(c, form) {
324 for byte in c.encode_utf8(&mut buffer).as_bytes() {
325 out.push('%');
326 out.push(hex_digit(byte >> 4));
327 out.push(hex_digit(byte & 0xf));
328 }
329 } else {
330 out.push(c);
331 }
332 }
333 Some(out)
334}
335
336fn hex_digit(value: u8) -> char {
338 char::from_digit(u32::from(value), 16).map_or('0', |digit| digit.to_ascii_uppercase())
339}
340
341fn is_unwritable(c: char, form: Form) -> bool {
346 if !c.is_control() {
347 return false;
348 }
349 match c {
350 '\t' => false,
351 '\n' => form == Form::Line,
352 _ => true,
353 }
354}
355
356pub fn quote(value: &str) -> String {
365 if !value.contains('"') {
366 return format!("\"{}\"", value);
367 }
368 if !value.contains('\'') {
369 return format!("'{}'", value);
370 }
371
372 let delimiter = if value.starts_with('"') { '\'' } else { '"' };
373 let count = (longest_run(value, delimiter) + 1).max(3);
376 let run: String = std::iter::repeat_n(delimiter, count).collect();
377 format!("{run}{value}{run}")
378}
379
380fn longest_run(value: &str, c: char) -> usize {
382 let mut longest = 0;
383 let mut current = 0;
384 for candidate in value.chars() {
385 if candidate == c {
386 current += 1;
387 longest = longest.max(current);
388 } else {
389 current = 0;
390 }
391 }
392 longest
393}
394
395fn format_key(key: &str, form: Form) -> String {
397 let plain = !key.is_empty()
398 && key != BASE64_MARKER
399 && key != ESCAPED_MARKER
400 && key != "json-number"
401 && !key.chars().any(|c| {
402 c.is_whitespace() || c.is_control() || matches!(c, '(' | ')' | '\'' | '"' | ':' | '`')
403 });
404
405 if plain {
406 key.to_string()
407 } else {
408 format_string(key, form)
409 }
410}
411
412#[derive(Debug, Clone, PartialEq, Eq)]
415enum Token {
416 Open,
417 Close,
418 Newline,
419 Ref { value: String, quoted: bool },
420}
421
422#[derive(Debug, Clone)]
425enum Node {
426 Ref {
427 value: String,
428 quoted: bool,
429 },
430 Link {
431 rows: Vec<Vec<Node>>,
432 multiline: bool,
433 object: bool,
435 },
436}
437
438fn tokenize(text: &str) -> Result<Vec<Token>, CodecError> {
439 let chars: Vec<char> = text.chars().collect();
440 let mut tokens = Vec::new();
441 let mut i = 0;
442
443 while i < chars.len() {
444 let c = chars[i];
445
446 if c == '\n' {
447 tokens.push(Token::Newline);
448 i += 1;
449 } else if c.is_whitespace() {
450 i += 1;
451 } else if c == '(' {
452 tokens.push(Token::Open);
453 i += 1;
454 } else if c == ')' {
455 tokens.push(Token::Close);
456 i += 1;
457 } else if matches!(c, '"' | '\'' | '`') {
458 let (value, next) = read_quoted(&chars, i, c)?;
459 tokens.push(Token::Ref {
460 value,
461 quoted: true,
462 });
463 i = next;
464 } else {
465 let start = i;
466 while i < chars.len()
467 && !chars[i].is_whitespace()
468 && !matches!(chars[i], '(' | ')' | '"' | '\'' | '`')
469 {
470 i += 1;
471 }
472 tokens.push(Token::Ref {
473 value: chars[start..i].iter().collect(),
474 quoted: false,
475 });
476 }
477 }
478
479 Ok(tokens)
480}
481
482fn read_quoted(
492 chars: &[char],
493 start: usize,
494 quote_char: char,
495) -> Result<(String, usize), CodecError> {
496 let opening = run_length(chars, start, quote_char);
497
498 if opening == 2 {
499 return Ok((String::new(), start + 2));
500 }
501
502 if opening == 1 {
503 let mut value = String::new();
504 let mut i = start + 1;
505
506 while i < chars.len() {
507 if chars[i] == quote_char {
508 if chars.get(i + 1) == Some("e_char) {
509 value.push(quote_char);
510 i += 2;
511 continue;
512 }
513 return Ok((value, i + 1));
514 }
515 value.push(chars[i]);
516 i += 1;
517 }
518
519 return Err(unterminated_quote(start));
520 }
521
522 let mut i = start + opening;
523 while i < chars.len() {
524 if chars[i] != quote_char {
525 i += 1;
526 continue;
527 }
528
529 let run = run_length(chars, i, quote_char);
530 if run >= opening {
531 let value = chars[start + opening..i + run - opening].iter().collect();
532 return Ok((value, i + run));
533 }
534 i += run;
535 }
536
537 Err(unterminated_quote(start))
538}
539
540fn run_length(chars: &[char], start: usize, c: char) -> usize {
542 chars[start..].iter().take_while(|&&x| x == c).count()
543}
544
545fn unterminated_quote(start: usize) -> CodecError {
546 CodecError::ParseError(format!(
547 "unterminated quoted value starting at character {}",
548 start
549 ))
550}
551
552struct Cursor {
553 tokens: Vec<Token>,
554 pos: usize,
555}
556
557impl Cursor {
558 fn parse_rows(&mut self, top_level: bool) -> Result<Vec<Vec<Node>>, CodecError> {
561 let mut rows: Vec<Vec<Node>> = Vec::new();
562 let mut row: Vec<Node> = Vec::new();
563
564 while self.pos < self.tokens.len() {
565 match &self.tokens[self.pos] {
566 Token::Close => {
567 if top_level {
568 break;
569 }
570 self.pos += 1;
571 if !row.is_empty() {
572 rows.push(row);
573 }
574 return Ok(rows);
575 }
576 Token::Newline => {
577 self.pos += 1;
578 if !row.is_empty() {
579 rows.push(std::mem::take(&mut row));
580 }
581 }
582 _ => row.push(self.parse_node()?),
583 }
584 }
585
586 if !top_level {
587 return Err(CodecError::ParseError(
588 "unterminated '(' in readable notation".to_string(),
589 ));
590 }
591
592 if !row.is_empty() {
593 rows.push(row);
594 }
595 Ok(rows)
596 }
597
598 fn parse_node(&mut self) -> Result<Node, CodecError> {
599 match self.tokens[self.pos].clone() {
600 Token::Ref { value, quoted } => {
601 self.pos += 1;
602 Ok(Node::Ref { value, quoted })
603 }
604 Token::Open => {
605 self.pos += 1;
606 let object = self.take_object_marker();
607 let multiline = self.link_is_multiline();
608 let rows = self.parse_rows(false)?;
609 Ok(Node::Link {
610 rows,
611 multiline,
612 object,
613 })
614 }
615 Token::Close | Token::Newline => Err(CodecError::ParseError(
616 "unexpected token in readable notation".to_string(),
617 )),
618 }
619 }
620
621 fn take_object_marker(&mut self) -> bool {
624 let marker = format!("{}:", OBJECT_MARKER);
625 let is_marker = matches!(
626 self.tokens.get(self.pos),
627 Some(Token::Ref { value, quoted: false }) if *value == marker
628 );
629 let array_marker = matches!(self.tokens.get(self.pos),
630 Some(Token::Ref { value, quoted: false }) if value == "a:");
631 if is_marker || array_marker {
632 self.pos += 1;
633 }
634 is_marker
635 }
636
637 fn link_is_multiline(&self) -> bool {
640 self.tokens[self.pos..]
641 .iter()
642 .take_while(|t| **t != Token::Close)
643 .any(|t| *t == Token::Newline)
644 }
645}
646
647fn node_to_value(node: &Node) -> Result<LinoValue, CodecError> {
648 match node {
649 Node::Ref { value, quoted } => Ok(ref_to_value(value, *quoted)),
650 Node::Link {
651 rows,
652 multiline,
653 object,
654 } => rows_to_value(rows, *multiline, *object),
655 }
656}
657
658fn rows_to_value(
659 rows: &[Vec<Node>],
660 multiline: bool,
661 object_marker: bool,
662) -> Result<LinoValue, CodecError> {
663 if object_marker {
664 return marked_object_to_value(rows);
665 }
666
667 if rows.is_empty() {
668 return Ok(if multiline {
669 LinoValue::Object(vec![])
670 } else {
671 LinoValue::Array(vec![])
672 });
673 }
674
675 if let Some(marked) = decode_marked_value(rows) {
676 return marked;
677 }
678
679 if !multiline {
682 let mut items = Vec::new();
683 for row in rows {
684 for node in row {
685 items.push(node_to_value(node)?);
686 }
687 }
688 return Ok(LinoValue::Array(items));
689 }
690
691 let is_object = rows
693 .iter()
694 .all(|row| row.len() == 2 && node_to_key(&row[0]).is_some());
695
696 if is_object {
697 let mut pairs = Vec::with_capacity(rows.len());
698 for row in rows {
699 let Some(key) = node_to_key(&row[0]) else {
700 unreachable!("checked by is_object")
701 };
702 pairs.push((key, node_to_value(&row[1])?));
703 }
704 return Ok(LinoValue::Object(pairs));
705 }
706
707 let mut items = Vec::new();
708 for row in rows {
709 for node in row {
710 items.push(node_to_value(node)?);
711 }
712 }
713 Ok(LinoValue::Array(items))
714}
715
716fn marked_object_to_value(rows: &[Vec<Node>]) -> Result<LinoValue, CodecError> {
719 let mut pairs = Vec::new();
720
721 for node in rows.iter().flatten() {
722 let Node::Link {
723 rows: pair,
724 object: false,
725 ..
726 } = node
727 else {
728 return Err(CodecError::ParseError(format!(
729 "an object marked '{}:' holds (key value) pairs, found a value that is not a pair",
730 OBJECT_MARKER
731 )));
732 };
733
734 let [row] = pair.as_slice() else {
735 return Err(CodecError::ParseError(format!(
736 "an object marked '{}:' holds (key value) pairs, found a link of {} lines",
737 OBJECT_MARKER,
738 pair.len()
739 )));
740 };
741
742 let [key_node, value] = row.as_slice() else {
743 return Err(CodecError::ParseError(format!(
744 "an object marked '{}:' holds (key value) pairs, found a link of {} values",
745 OBJECT_MARKER,
746 row.len()
747 )));
748 };
749
750 let Some(key) = node_to_key(key_node) else {
751 return Err(CodecError::ParseError(format!(
752 "an object marked '{}:' holds (key value) pairs, found a pair whose key is not text",
753 OBJECT_MARKER
754 )));
755 };
756
757 pairs.push((key, node_to_value(value)?));
758 }
759
760 Ok(LinoValue::Object(pairs))
761}
762
763fn node_to_key(node: &Node) -> Option<String> {
768 match node {
769 Node::Ref { value, .. } => Some(value.clone()),
770 Node::Link {
771 rows,
772 object: false,
773 ..
774 } => match decode_marked_value(rows)? {
775 Ok(LinoValue::String(key)) => Some(key),
776 _ => None,
777 },
778 Node::Link { .. } => None,
779 }
780}
781
782fn decode_marked_value(rows: &[Vec<Node>]) -> Option<Result<LinoValue, CodecError>> {
787 if rows.len() != 1 || rows[0].len() != 2 {
788 return None;
789 }
790
791 let Node::Ref {
792 value: marker,
793 quoted: false,
794 } = &rows[0][0]
795 else {
796 return None;
797 };
798
799 let Node::Ref {
800 value: payload,
801 quoted: true,
802 } = &rows[0][1]
803 else {
804 return None;
805 };
806
807 #[cfg(feature = "serde_json")]
808 if marker == "json-number" {
809 return Some(
810 payload
811 .parse::<serde_json::Number>()
812 .map(LinoValue::JsonNumber)
813 .map_err(|e| CodecError::DecodeError(format!("invalid JSON number: {e}"))),
814 );
815 }
816
817 if marker == ESCAPED_MARKER {
818 return Some(unescape(payload).map(LinoValue::String));
819 }
820
821 if marker == BASE64_MARKER {
822 return Some(
823 BASE64
824 .decode(payload)
825 .map_err(|e| CodecError::DecodeError(format!("invalid base64 value: {}", e)))
826 .and_then(|bytes| {
827 String::from_utf8(bytes)
828 .map(LinoValue::String)
829 .map_err(|e| CodecError::DecodeError(format!("invalid UTF-8 value: {}", e)))
830 }),
831 );
832 }
833
834 None
835}
836
837pub fn unescape(payload: &str) -> Result<String, CodecError> {
841 let bytes = payload.as_bytes();
842 let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
843 let mut i = 0;
844
845 while i < bytes.len() {
846 if bytes[i] != b'%' {
847 out.push(bytes[i]);
848 i += 1;
849 continue;
850 }
851
852 let escape = payload.get(i + 1..i + 3).ok_or_else(|| {
853 CodecError::DecodeError(format!(
854 "truncated escape at character {} of an escaped value",
855 i
856 ))
857 })?;
858 let byte = u8::from_str_radix(escape, 16).map_err(|_| {
859 CodecError::DecodeError(format!("invalid escape '%{}' in an escaped value", escape))
860 })?;
861 out.push(byte);
862 i += 3;
863 }
864
865 String::from_utf8(out)
866 .map_err(|e| CodecError::DecodeError(format!("invalid UTF-8 escaped value: {}", e)))
867}
868
869fn ref_to_value(value: &str, quoted: bool) -> LinoValue {
872 if quoted {
873 return LinoValue::String(value.to_string());
874 }
875
876 match value {
877 "null" => return LinoValue::Null,
878 "true" => return LinoValue::Bool(true),
879 "false" => return LinoValue::Bool(false),
880 "NaN" => return LinoValue::Float(f64::NAN),
881 "Infinity" => return LinoValue::Float(f64::INFINITY),
882 "-Infinity" => return LinoValue::Float(f64::NEG_INFINITY),
883 _ => {}
884 }
885
886 if let Ok(i) = value.parse::<i64>() {
887 return LinoValue::Int(i);
888 }
889 if value.contains(['.', 'e', 'E']) {
890 if let Ok(f) = value.parse::<f64>() {
891 return LinoValue::Float(f);
892 }
893 }
894
895 LinoValue::String(value.to_string())
896}
897
898#[cfg(test)]
899mod tests {
900 use super::*;
901
902 fn roundtrip(value: &LinoValue) -> LinoValue {
903 let text = encode(value, DEFAULT_INDENT);
904 decode(&text).unwrap_or_else(|e| panic!("failed to decode {:?}: {}", text, e))
905 }
906
907 #[test]
908 fn empty_containers_keep_their_type() {
909 assert_eq!(encode(&LinoValue::Array(vec![]), DEFAULT_INDENT), "()");
910 assert_eq!(encode(&LinoValue::Object(vec![]), DEFAULT_INDENT), "(\n)");
911 assert_eq!(
912 roundtrip(&LinoValue::Array(vec![])),
913 LinoValue::Array(vec![])
914 );
915 assert_eq!(
916 roundtrip(&LinoValue::Object(vec![])),
917 LinoValue::Object(vec![])
918 );
919 }
920
921 #[test]
922 fn single_pair_object_is_not_an_array() {
923 let value = LinoValue::object([("a", LinoValue::Int(1))]);
924 assert_eq!(encode(&value, DEFAULT_INDENT), "(\n a 1\n)");
925 assert_eq!(roundtrip(&value), value);
926 }
927
928 #[test]
929 fn numeric_looking_strings_stay_strings() {
930 let value = LinoValue::object([
931 ("count", LinoValue::Int(42)),
932 ("zip", LinoValue::String("10001".to_string())),
933 ("flag", LinoValue::String("true".to_string())),
934 ]);
935 assert_eq!(roundtrip(&value), value);
936 }
937
938 #[test]
939 fn whole_floats_stay_floats() {
940 let value = LinoValue::Float(1.0);
941 assert_eq!(encode(&value, DEFAULT_INDENT), "1.0");
942 assert!(matches!(roundtrip(&value), LinoValue::Float(f) if (f - 1.0).abs() < f64::EPSILON));
943 }
944
945 #[test]
946 fn base64_key_is_quoted_so_it_is_not_a_marker() {
947 let value = LinoValue::object([("base64", LinoValue::String("plain".to_string()))]);
948 assert_eq!(
949 encode(&value, DEFAULT_INDENT),
950 "(\n \"base64\" \"plain\"\n)"
951 );
952 assert_eq!(roundtrip(&value), value);
953 }
954
955 #[test]
956 fn text_is_written_as_text_and_only_the_rest_is_escaped() {
957 let value = LinoValue::object([
958 ("plain", LinoValue::String("visible".to_string())),
959 ("raw", LinoValue::String("line1\nline2".to_string())),
960 ("returned", LinoValue::String("line1\r\nline2".to_string())),
961 ]);
962 let text = encode(&value, DEFAULT_INDENT);
963 assert!(text.contains("plain \"visible\""), "{}", text);
964 assert!(text.contains("raw \"line1\nline2\""), "{}", text);
965 assert!(
966 text.contains("returned (escaped \"line1%0D\nline2\")"),
967 "{}",
968 text
969 );
970 assert!(!text.contains("base64"), "{}", text);
971 assert_eq!(roundtrip(&value), value);
972 }
973
974 #[test]
977 fn every_written_document_parses_as_links_notation() {
978 let texts = [
979 "plain",
980 "it's",
981 "he said \"hello\"",
982 "both \"kinds\" of 'quotes'",
983 "a\"\"b'c",
984 "a\"\"\"b'c",
985 "trailing quote\"'",
986 "'\"",
987 "line one\nline two",
988 "a\tb",
989 "null\u{0}byte",
990 "unicode: 你好世界 🌍",
991 ];
992
993 for text in texts {
994 let value = LinoValue::object([
995 ("message", LinoValue::String(text.to_string())),
996 ("level", LinoValue::String("info".to_string())),
997 ]);
998
999 for document in [encode(&value, DEFAULT_INDENT), encode_line(&value)] {
1000 assert!(
1001 links_notation::parse_lino(&document).is_ok(),
1002 "links-notation rejected {:?}",
1003 document
1004 );
1005 }
1006 }
1007 }
1008
1009 #[test]
1012 fn links_notation_reads_back_the_text_that_was_written() {
1013 for text in [
1014 "he said \"hello\"",
1015 "both \"kinds\" of 'quotes'",
1016 "a\"\"b'c",
1017 "line one\nline two",
1018 ] {
1019 let document = encode(
1020 &LinoValue::object([("message", LinoValue::String(text.to_string()))]),
1021 DEFAULT_INDENT,
1022 );
1023 let parsed = links_notation::parse_lino(&document)
1024 .unwrap_or_else(|e| panic!("links-notation rejected {:?}: {}", document, e));
1025
1026 let mut refs = Vec::new();
1027 collect_refs(&parsed, &mut refs);
1028 assert!(
1029 refs.contains(&text.to_string()),
1030 "links-notation read {:?} out of {:?}",
1031 refs,
1032 document
1033 );
1034 }
1035 }
1036
1037 fn collect_refs(node: &links_notation::LiNo<String>, out: &mut Vec<String>) {
1038 match node {
1039 links_notation::LiNo::Ref(value) => out.push(value.clone()),
1040 links_notation::LiNo::Link { values, .. } => {
1041 for value in values {
1042 collect_refs(value, out);
1043 }
1044 }
1045 }
1046 }
1047
1048 #[test]
1049 fn custom_indent_is_used() {
1050 let value = LinoValue::object([("a", LinoValue::Int(1))]);
1051 assert_eq!(encode(&value, " "), "(\n a 1\n)");
1052 }
1053
1054 #[test]
1055 fn handwritten_document_without_root_parentheses_decodes() {
1056 let decoded = decode("a 1\nb \"two\"").unwrap();
1057 assert_eq!(
1058 decoded,
1059 LinoValue::object([
1060 ("a", LinoValue::Int(1)),
1061 ("b", LinoValue::String("two".into()))
1062 ])
1063 );
1064 }
1065
1066 fn line_roundtrip(value: &LinoValue) -> LinoValue {
1067 let text = encode_line(value);
1068 assert!(
1069 !text.contains('\n'),
1070 "line form must hold no newline: {:?}",
1071 text
1072 );
1073 decode_line(&text).unwrap_or_else(|e| panic!("failed to decode {:?}: {}", text, e))
1074 }
1075
1076 #[test]
1077 fn line_form_marks_objects_and_leaves_arrays_bare() {
1078 assert_eq!(encode_line(&LinoValue::Array(vec![])), "()");
1079 assert_eq!(encode_line(&LinoValue::Object(vec![])), "(o:)");
1080 assert_eq!(
1081 encode_line(&LinoValue::object([("a", LinoValue::Int(1))])),
1082 "(o: (a 1))"
1083 );
1084 assert_eq!(
1085 encode_line(&LinoValue::array([
1086 LinoValue::String("key".into()),
1087 LinoValue::String("value".into()),
1088 ])),
1089 "(\"key\" \"value\")"
1090 );
1091 }
1092
1093 #[test]
1094 fn line_form_tells_a_one_pair_object_from_a_two_element_array() {
1095 let object = LinoValue::object([("key", LinoValue::String("value".into()))]);
1096 let array = LinoValue::array([
1097 LinoValue::String("key".into()),
1098 LinoValue::String("value".into()),
1099 ]);
1100 assert_ne!(encode_line(&object), encode_line(&array));
1101 assert_eq!(line_roundtrip(&object), object);
1102 assert_eq!(line_roundtrip(&array), array);
1103 }
1104
1105 #[test]
1106 fn line_form_roundtrips_nested_and_empty_containers() {
1107 let value = LinoValue::object([
1108 ("empty_array", LinoValue::Array(vec![])),
1109 ("empty_object", LinoValue::Object(vec![])),
1110 (
1111 "records",
1112 LinoValue::array([
1113 LinoValue::object([("id", LinoValue::Int(1))]),
1114 LinoValue::object([("id", LinoValue::Int(2))]),
1115 ]),
1116 ),
1117 ]);
1118 assert_eq!(
1119 encode_line(&value),
1120 "(o: (empty_array ()) (empty_object (o:)) (records ((o: (id 1)) (o: (id 2)))))"
1121 );
1122 assert_eq!(line_roundtrip(&value), value);
1123 }
1124
1125 #[test]
1126 fn line_form_keeps_the_empty_key() {
1127 let value = LinoValue::object([("", LinoValue::Int(2))]);
1128 assert_eq!(encode_line(&value), "(o: (\"\" 2))");
1129 assert_eq!(line_roundtrip(&value), value);
1130 }
1131
1132 #[test]
1133 fn line_form_keeps_strings_on_one_line() {
1134 let value = LinoValue::object([
1135 (
1136 "quotes",
1137 LinoValue::String("both \"kinds\" of 'quotes'".into()),
1138 ),
1139 ("unicode", LinoValue::String("héllo 世界 🌍".into())),
1140 ("multiline", LinoValue::String("line1\nline2".into())),
1141 ]);
1142 let text = encode_line(&value);
1143 assert!(!text.contains('\n'), "{}", text);
1144 assert_eq!(line_roundtrip(&value), value);
1145 }
1146
1147 #[test]
1148 fn a_line_form_object_holds_pairs_only() {
1149 assert!(decode_line("(o: 1 2)").is_err());
1150 assert!(decode_line("(o: (a 1 2))").is_err());
1151 assert!(decode_line("(o: (a))").is_err());
1152 }
1153
1154 #[test]
1155 fn decode_line_rejects_a_document_of_several_lines() {
1156 assert!(decode_line("(o: (a 1))\n(o: (a 2))").is_err());
1157 assert_eq!(
1160 decode_line("(o: (a 1))\n").unwrap(),
1161 LinoValue::object([("a", LinoValue::Int(1))])
1162 );
1163 }
1164
1165 #[test]
1166 fn unterminated_input_is_an_error() {
1167 assert!(decode("(\n a 1\n").is_err());
1168 assert!(decode("(\n a \"unterminated\n").is_err());
1169 assert!(decode("(\n a \"\"\"unterminated\n").is_err());
1170 assert!(decode("a 1)").is_err());
1171 }
1172
1173 #[test]
1176 fn a_run_of_delimiters_says_how_the_value_is_read() {
1177 assert_eq!(
1179 decode("\"both \"\"kinds\"\" of 'quotes'\"").unwrap(),
1180 LinoValue::String("both \"kinds\" of 'quotes'".to_string())
1181 );
1182 assert_eq!(decode("\"\"").unwrap(), LinoValue::String(String::new()));
1184 assert_eq!(
1186 decode("\"\"\"say \"hi\"\"\"\"").unwrap(),
1187 LinoValue::String("say \"hi\"".to_string())
1188 );
1189 }
1190}