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 mut tokens = Vec::new();
440 let mut i = 0;
441
442 while i < text.len() {
445 let c = text[i..].chars().next().expect("in bounds");
446
447 if c == '\n' {
448 tokens.push(Token::Newline);
449 i += 1;
450 } else if c.is_whitespace() {
451 i += c.len_utf8();
452 } else if c == '(' {
453 tokens.push(Token::Open);
454 i += 1;
455 } else if c == ')' {
456 tokens.push(Token::Close);
457 i += 1;
458 } else if matches!(c, '"' | '\'' | '`') {
459 let (value, next) = read_quoted(text, i, c)?;
460 tokens.push(Token::Ref {
461 value,
462 quoted: true,
463 });
464 i = next;
465 } else {
466 let start = i;
467 for c in text[start..].chars() {
468 if c.is_whitespace() || matches!(c, '(' | ')' | '"' | '\'' | '`') {
469 break;
470 }
471 i += c.len_utf8();
472 }
473 tokens.push(Token::Ref {
474 value: text[start..i].to_owned(),
475 quoted: false,
476 });
477 }
478 }
479
480 Ok(tokens)
481}
482
483fn read_quoted(text: &str, start: usize, quote_char: char) -> Result<(String, usize), CodecError> {
493 let quote_byte = text.as_bytes()[start];
494 let opening = run_length(text, start, quote_byte);
495
496 if opening == 2 {
497 return Ok((String::new(), start + 2));
498 }
499
500 if opening == 1 {
501 let mut i = start + 1;
502
503 while let Some(next) = text[i..].find(quote_char) {
504 let next = i + next;
505 if text.as_bytes().get(next + 1) == Some("e_byte) {
506 i = next + 2;
507 continue;
508 }
509 let doubled = format!("{quote_char}{quote_char}");
510 return Ok((
511 text[start + 1..next].replace(&doubled, "e_char.to_string()),
512 next + 1,
513 ));
514 }
515
516 return Err(unterminated_quote(text[..start].chars().count()));
517 }
518
519 let mut i = start + opening;
520 while let Some(next) = text[i..].find(quote_char) {
521 i += next;
522 let run = run_length(text, i, quote_byte);
523 if run >= opening {
524 let value = text[start + opening..i + run - opening].to_owned();
525 return Ok((value, i + run));
526 }
527 i += run;
528 }
529
530 Err(unterminated_quote(text[..start].chars().count()))
531}
532
533fn run_length(text: &str, start: usize, c: u8) -> usize {
535 text.as_bytes()[start..]
536 .iter()
537 .take_while(|&&x| x == c)
538 .count()
539}
540
541fn unterminated_quote(start: usize) -> CodecError {
542 CodecError::ParseError(format!(
543 "unterminated quoted value starting at character {}",
544 start
545 ))
546}
547
548struct Cursor {
549 tokens: Vec<Token>,
550 pos: usize,
551}
552
553impl Cursor {
554 fn parse_rows(&mut self, top_level: bool) -> Result<Vec<Vec<Node>>, CodecError> {
557 let mut rows: Vec<Vec<Node>> = Vec::new();
558 let mut row: Vec<Node> = Vec::new();
559
560 while self.pos < self.tokens.len() {
561 match &self.tokens[self.pos] {
562 Token::Close => {
563 if top_level {
564 break;
565 }
566 self.pos += 1;
567 if !row.is_empty() {
568 rows.push(row);
569 }
570 return Ok(rows);
571 }
572 Token::Newline => {
573 self.pos += 1;
574 if !row.is_empty() {
575 rows.push(std::mem::take(&mut row));
576 }
577 }
578 _ => row.push(self.parse_node()?),
579 }
580 }
581
582 if !top_level {
583 return Err(CodecError::ParseError(
584 "unterminated '(' in readable notation".to_string(),
585 ));
586 }
587
588 if !row.is_empty() {
589 rows.push(row);
590 }
591 Ok(rows)
592 }
593
594 fn parse_node(&mut self) -> Result<Node, CodecError> {
595 match self.tokens[self.pos].clone() {
596 Token::Ref { value, quoted } => {
597 self.pos += 1;
598 Ok(Node::Ref { value, quoted })
599 }
600 Token::Open => {
601 self.pos += 1;
602 let object = self.take_object_marker();
603 let multiline = self.link_is_multiline();
604 let rows = self.parse_rows(false)?;
605 Ok(Node::Link {
606 rows,
607 multiline,
608 object,
609 })
610 }
611 Token::Close | Token::Newline => Err(CodecError::ParseError(
612 "unexpected token in readable notation".to_string(),
613 )),
614 }
615 }
616
617 fn take_object_marker(&mut self) -> bool {
620 let marker = format!("{}:", OBJECT_MARKER);
621 let is_marker = matches!(
622 self.tokens.get(self.pos),
623 Some(Token::Ref { value, quoted: false }) if *value == marker
624 );
625 let array_marker = matches!(self.tokens.get(self.pos),
626 Some(Token::Ref { value, quoted: false }) if value == "a:");
627 if is_marker || array_marker {
628 self.pos += 1;
629 }
630 is_marker
631 }
632
633 fn link_is_multiline(&self) -> bool {
636 self.tokens[self.pos..]
637 .iter()
638 .take_while(|t| **t != Token::Close)
639 .any(|t| *t == Token::Newline)
640 }
641}
642
643fn node_to_value(node: &Node) -> Result<LinoValue, CodecError> {
644 match node {
645 Node::Ref { value, quoted } => Ok(ref_to_value(value, *quoted)),
646 Node::Link {
647 rows,
648 multiline,
649 object,
650 } => rows_to_value(rows, *multiline, *object),
651 }
652}
653
654fn rows_to_value(
655 rows: &[Vec<Node>],
656 multiline: bool,
657 object_marker: bool,
658) -> Result<LinoValue, CodecError> {
659 if object_marker {
660 return marked_object_to_value(rows);
661 }
662
663 if rows.is_empty() {
664 return Ok(if multiline {
665 LinoValue::Object(vec![])
666 } else {
667 LinoValue::Array(vec![])
668 });
669 }
670
671 if let Some(marked) = decode_marked_value(rows) {
672 return marked;
673 }
674
675 if !multiline {
678 let mut items = Vec::new();
679 for row in rows {
680 for node in row {
681 items.push(node_to_value(node)?);
682 }
683 }
684 return Ok(LinoValue::Array(items));
685 }
686
687 let is_object = rows
689 .iter()
690 .all(|row| row.len() == 2 && node_to_key(&row[0]).is_some());
691
692 if is_object {
693 let mut pairs = Vec::with_capacity(rows.len());
694 for row in rows {
695 let Some(key) = node_to_key(&row[0]) else {
696 unreachable!("checked by is_object")
697 };
698 pairs.push((key, node_to_value(&row[1])?));
699 }
700 return Ok(LinoValue::Object(pairs));
701 }
702
703 let mut items = Vec::new();
704 for row in rows {
705 for node in row {
706 items.push(node_to_value(node)?);
707 }
708 }
709 Ok(LinoValue::Array(items))
710}
711
712fn marked_object_to_value(rows: &[Vec<Node>]) -> Result<LinoValue, CodecError> {
715 let mut pairs = Vec::new();
716
717 for node in rows.iter().flatten() {
718 let Node::Link {
719 rows: pair,
720 object: false,
721 ..
722 } = node
723 else {
724 return Err(CodecError::ParseError(format!(
725 "an object marked '{}:' holds (key value) pairs, found a value that is not a pair",
726 OBJECT_MARKER
727 )));
728 };
729
730 let [row] = pair.as_slice() else {
731 return Err(CodecError::ParseError(format!(
732 "an object marked '{}:' holds (key value) pairs, found a link of {} lines",
733 OBJECT_MARKER,
734 pair.len()
735 )));
736 };
737
738 let [key_node, value] = row.as_slice() else {
739 return Err(CodecError::ParseError(format!(
740 "an object marked '{}:' holds (key value) pairs, found a link of {} values",
741 OBJECT_MARKER,
742 row.len()
743 )));
744 };
745
746 let Some(key) = node_to_key(key_node) else {
747 return Err(CodecError::ParseError(format!(
748 "an object marked '{}:' holds (key value) pairs, found a pair whose key is not text",
749 OBJECT_MARKER
750 )));
751 };
752
753 pairs.push((key, node_to_value(value)?));
754 }
755
756 Ok(LinoValue::Object(pairs))
757}
758
759fn node_to_key(node: &Node) -> Option<String> {
764 match node {
765 Node::Ref { value, .. } => Some(value.clone()),
766 Node::Link {
767 rows,
768 object: false,
769 ..
770 } => match decode_marked_value(rows)? {
771 Ok(LinoValue::String(key)) => Some(key),
772 _ => None,
773 },
774 Node::Link { .. } => None,
775 }
776}
777
778fn decode_marked_value(rows: &[Vec<Node>]) -> Option<Result<LinoValue, CodecError>> {
783 if rows.len() != 1 || rows[0].len() != 2 {
784 return None;
785 }
786
787 let Node::Ref {
788 value: marker,
789 quoted: false,
790 } = &rows[0][0]
791 else {
792 return None;
793 };
794
795 let Node::Ref {
796 value: payload,
797 quoted: true,
798 } = &rows[0][1]
799 else {
800 return None;
801 };
802
803 #[cfg(feature = "serde_json")]
804 if marker == "json-number" {
805 return Some(
806 payload
807 .parse::<serde_json::Number>()
808 .map(LinoValue::JsonNumber)
809 .map_err(|e| CodecError::DecodeError(format!("invalid JSON number: {e}"))),
810 );
811 }
812
813 if marker == ESCAPED_MARKER {
814 return Some(unescape(payload).map(LinoValue::String));
815 }
816
817 if marker == BASE64_MARKER {
818 return Some(
819 BASE64
820 .decode(payload)
821 .map_err(|e| CodecError::DecodeError(format!("invalid base64 value: {}", e)))
822 .and_then(|bytes| {
823 String::from_utf8(bytes)
824 .map(LinoValue::String)
825 .map_err(|e| CodecError::DecodeError(format!("invalid UTF-8 value: {}", e)))
826 }),
827 );
828 }
829
830 None
831}
832
833pub fn unescape(payload: &str) -> Result<String, CodecError> {
837 let bytes = payload.as_bytes();
838 let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
839 let mut i = 0;
840
841 while i < bytes.len() {
842 if bytes[i] != b'%' {
843 out.push(bytes[i]);
844 i += 1;
845 continue;
846 }
847
848 let escape = payload.get(i + 1..i + 3).ok_or_else(|| {
849 CodecError::DecodeError(format!(
850 "truncated escape at character {} of an escaped value",
851 i
852 ))
853 })?;
854 let byte = u8::from_str_radix(escape, 16).map_err(|_| {
855 CodecError::DecodeError(format!("invalid escape '%{}' in an escaped value", escape))
856 })?;
857 out.push(byte);
858 i += 3;
859 }
860
861 String::from_utf8(out)
862 .map_err(|e| CodecError::DecodeError(format!("invalid UTF-8 escaped value: {}", e)))
863}
864
865fn ref_to_value(value: &str, quoted: bool) -> LinoValue {
868 if quoted {
869 return LinoValue::String(value.to_string());
870 }
871
872 match value {
873 "null" => return LinoValue::Null,
874 "true" => return LinoValue::Bool(true),
875 "false" => return LinoValue::Bool(false),
876 "NaN" => return LinoValue::Float(f64::NAN),
877 "Infinity" => return LinoValue::Float(f64::INFINITY),
878 "-Infinity" => return LinoValue::Float(f64::NEG_INFINITY),
879 _ => {}
880 }
881
882 if let Ok(i) = value.parse::<i64>() {
883 return LinoValue::Int(i);
884 }
885 if value.contains(['.', 'e', 'E']) {
886 if let Ok(f) = value.parse::<f64>() {
887 return LinoValue::Float(f);
888 }
889 }
890
891 LinoValue::String(value.to_string())
892}
893
894#[cfg(test)]
895mod tests {
896 use super::*;
897
898 fn roundtrip(value: &LinoValue) -> LinoValue {
899 let text = encode(value, DEFAULT_INDENT);
900 decode(&text).unwrap_or_else(|e| panic!("failed to decode {:?}: {}", text, e))
901 }
902
903 #[test]
904 fn empty_containers_keep_their_type() {
905 assert_eq!(encode(&LinoValue::Array(vec![]), DEFAULT_INDENT), "()");
906 assert_eq!(encode(&LinoValue::Object(vec![]), DEFAULT_INDENT), "(\n)");
907 assert_eq!(
908 roundtrip(&LinoValue::Array(vec![])),
909 LinoValue::Array(vec![])
910 );
911 assert_eq!(
912 roundtrip(&LinoValue::Object(vec![])),
913 LinoValue::Object(vec![])
914 );
915 }
916
917 #[test]
918 fn single_pair_object_is_not_an_array() {
919 let value = LinoValue::object([("a", LinoValue::Int(1))]);
920 assert_eq!(encode(&value, DEFAULT_INDENT), "(\n a 1\n)");
921 assert_eq!(roundtrip(&value), value);
922 }
923
924 #[test]
925 fn numeric_looking_strings_stay_strings() {
926 let value = LinoValue::object([
927 ("count", LinoValue::Int(42)),
928 ("zip", LinoValue::String("10001".to_string())),
929 ("flag", LinoValue::String("true".to_string())),
930 ]);
931 assert_eq!(roundtrip(&value), value);
932 }
933
934 #[test]
935 fn whole_floats_stay_floats() {
936 let value = LinoValue::Float(1.0);
937 assert_eq!(encode(&value, DEFAULT_INDENT), "1.0");
938 assert!(matches!(roundtrip(&value), LinoValue::Float(f) if (f - 1.0).abs() < f64::EPSILON));
939 }
940
941 #[test]
942 fn base64_key_is_quoted_so_it_is_not_a_marker() {
943 let value = LinoValue::object([("base64", LinoValue::String("plain".to_string()))]);
944 assert_eq!(
945 encode(&value, DEFAULT_INDENT),
946 "(\n \"base64\" \"plain\"\n)"
947 );
948 assert_eq!(roundtrip(&value), value);
949 }
950
951 #[test]
952 fn text_is_written_as_text_and_only_the_rest_is_escaped() {
953 let value = LinoValue::object([
954 ("plain", LinoValue::String("visible".to_string())),
955 ("raw", LinoValue::String("line1\nline2".to_string())),
956 ("returned", LinoValue::String("line1\r\nline2".to_string())),
957 ]);
958 let text = encode(&value, DEFAULT_INDENT);
959 assert!(text.contains("plain \"visible\""), "{}", text);
960 assert!(text.contains("raw \"line1\nline2\""), "{}", text);
961 assert!(
962 text.contains("returned (escaped \"line1%0D\nline2\")"),
963 "{}",
964 text
965 );
966 assert!(!text.contains("base64"), "{}", text);
967 assert_eq!(roundtrip(&value), value);
968 }
969
970 #[test]
973 fn every_written_document_parses_as_links_notation() {
974 let texts = [
975 "plain",
976 "it's",
977 "he said \"hello\"",
978 "both \"kinds\" of 'quotes'",
979 "a\"\"b'c",
980 "a\"\"\"b'c",
981 "trailing quote\"'",
982 "'\"",
983 "line one\nline two",
984 "a\tb",
985 "null\u{0}byte",
986 "unicode: 你好世界 🌍",
987 ];
988
989 for text in texts {
990 let value = LinoValue::object([
991 ("message", LinoValue::String(text.to_string())),
992 ("level", LinoValue::String("info".to_string())),
993 ]);
994
995 for document in [encode(&value, DEFAULT_INDENT), encode_line(&value)] {
996 assert!(
997 links_notation::parse_lino(&document).is_ok(),
998 "links-notation rejected {:?}",
999 document
1000 );
1001 }
1002 }
1003 }
1004
1005 #[test]
1008 fn links_notation_reads_back_the_text_that_was_written() {
1009 for text in [
1010 "he said \"hello\"",
1011 "both \"kinds\" of 'quotes'",
1012 "a\"\"b'c",
1013 "line one\nline two",
1014 ] {
1015 let document = encode(
1016 &LinoValue::object([("message", LinoValue::String(text.to_string()))]),
1017 DEFAULT_INDENT,
1018 );
1019 let parsed = links_notation::parse_lino(&document)
1020 .unwrap_or_else(|e| panic!("links-notation rejected {:?}: {}", document, e));
1021
1022 let mut refs = Vec::new();
1023 collect_refs(&parsed, &mut refs);
1024 assert!(
1025 refs.contains(&text.to_string()),
1026 "links-notation read {:?} out of {:?}",
1027 refs,
1028 document
1029 );
1030 }
1031 }
1032
1033 fn collect_refs(node: &links_notation::LiNo<String>, out: &mut Vec<String>) {
1034 match node {
1035 links_notation::LiNo::Ref(value) => out.push(value.clone()),
1036 links_notation::LiNo::Link { values, .. } => {
1037 for value in values {
1038 collect_refs(value, out);
1039 }
1040 }
1041 }
1042 }
1043
1044 #[test]
1045 fn custom_indent_is_used() {
1046 let value = LinoValue::object([("a", LinoValue::Int(1))]);
1047 assert_eq!(encode(&value, " "), "(\n a 1\n)");
1048 }
1049
1050 #[test]
1051 fn handwritten_document_without_root_parentheses_decodes() {
1052 let decoded = decode("a 1\nb \"two\"").unwrap();
1053 assert_eq!(
1054 decoded,
1055 LinoValue::object([
1056 ("a", LinoValue::Int(1)),
1057 ("b", LinoValue::String("two".into()))
1058 ])
1059 );
1060 }
1061
1062 fn line_roundtrip(value: &LinoValue) -> LinoValue {
1063 let text = encode_line(value);
1064 assert!(
1065 !text.contains('\n'),
1066 "line form must hold no newline: {:?}",
1067 text
1068 );
1069 decode_line(&text).unwrap_or_else(|e| panic!("failed to decode {:?}: {}", text, e))
1070 }
1071
1072 #[test]
1073 fn line_form_marks_objects_and_leaves_arrays_bare() {
1074 assert_eq!(encode_line(&LinoValue::Array(vec![])), "()");
1075 assert_eq!(encode_line(&LinoValue::Object(vec![])), "(o:)");
1076 assert_eq!(
1077 encode_line(&LinoValue::object([("a", LinoValue::Int(1))])),
1078 "(o: (a 1))"
1079 );
1080 assert_eq!(
1081 encode_line(&LinoValue::array([
1082 LinoValue::String("key".into()),
1083 LinoValue::String("value".into()),
1084 ])),
1085 "(\"key\" \"value\")"
1086 );
1087 }
1088
1089 #[test]
1090 fn line_form_tells_a_one_pair_object_from_a_two_element_array() {
1091 let object = LinoValue::object([("key", LinoValue::String("value".into()))]);
1092 let array = LinoValue::array([
1093 LinoValue::String("key".into()),
1094 LinoValue::String("value".into()),
1095 ]);
1096 assert_ne!(encode_line(&object), encode_line(&array));
1097 assert_eq!(line_roundtrip(&object), object);
1098 assert_eq!(line_roundtrip(&array), array);
1099 }
1100
1101 #[test]
1102 fn line_form_roundtrips_nested_and_empty_containers() {
1103 let value = LinoValue::object([
1104 ("empty_array", LinoValue::Array(vec![])),
1105 ("empty_object", LinoValue::Object(vec![])),
1106 (
1107 "records",
1108 LinoValue::array([
1109 LinoValue::object([("id", LinoValue::Int(1))]),
1110 LinoValue::object([("id", LinoValue::Int(2))]),
1111 ]),
1112 ),
1113 ]);
1114 assert_eq!(
1115 encode_line(&value),
1116 "(o: (empty_array ()) (empty_object (o:)) (records ((o: (id 1)) (o: (id 2)))))"
1117 );
1118 assert_eq!(line_roundtrip(&value), value);
1119 }
1120
1121 #[test]
1122 fn line_form_keeps_the_empty_key() {
1123 let value = LinoValue::object([("", LinoValue::Int(2))]);
1124 assert_eq!(encode_line(&value), "(o: (\"\" 2))");
1125 assert_eq!(line_roundtrip(&value), value);
1126 }
1127
1128 #[test]
1129 fn line_form_keeps_strings_on_one_line() {
1130 let value = LinoValue::object([
1131 (
1132 "quotes",
1133 LinoValue::String("both \"kinds\" of 'quotes'".into()),
1134 ),
1135 ("unicode", LinoValue::String("héllo 世界 🌍".into())),
1136 ("multiline", LinoValue::String("line1\nline2".into())),
1137 ]);
1138 let text = encode_line(&value);
1139 assert!(!text.contains('\n'), "{}", text);
1140 assert_eq!(line_roundtrip(&value), value);
1141 }
1142
1143 #[test]
1144 fn a_line_form_object_holds_pairs_only() {
1145 assert!(decode_line("(o: 1 2)").is_err());
1146 assert!(decode_line("(o: (a 1 2))").is_err());
1147 assert!(decode_line("(o: (a))").is_err());
1148 }
1149
1150 #[test]
1151 fn decode_line_rejects_a_document_of_several_lines() {
1152 assert!(decode_line("(o: (a 1))\n(o: (a 2))").is_err());
1153 assert_eq!(
1156 decode_line("(o: (a 1))\n").unwrap(),
1157 LinoValue::object([("a", LinoValue::Int(1))])
1158 );
1159 }
1160
1161 #[test]
1162 fn unterminated_input_is_an_error() {
1163 assert!(decode("(\n a 1\n").is_err());
1164 assert!(decode("(\n a \"unterminated\n").is_err());
1165 assert!(decode("(\n a \"\"\"unterminated\n").is_err());
1166 assert!(decode("a 1)").is_err());
1167 }
1168
1169 #[test]
1172 fn a_run_of_delimiters_says_how_the_value_is_read() {
1173 assert_eq!(
1175 decode("\"both \"\"kinds\"\" of 'quotes'\"").unwrap(),
1176 LinoValue::String("both \"kinds\" of 'quotes'".to_string())
1177 );
1178 assert_eq!(decode("\"\"").unwrap(), LinoValue::String(String::new()));
1180 assert_eq!(
1182 decode("\"\"\"say \"hi\"\"\"\"").unwrap(),
1183 LinoValue::String("say \"hi\"".to_string())
1184 );
1185 }
1186}