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