use links_notation::parse_lino;
use lino_objects_codec::{LinoValue, decode, decode_line, encode, encode_line};
fn log_record() -> LinoValue {
LinoValue::object([
("bytes", LinoValue::Int(2827)),
("complete", LinoValue::Bool(true)),
(
"server",
LinoValue::object([
("host", LinoValue::String("127.0.0.1".to_string())),
("port", LinoValue::Int(18878)),
]),
),
(
"models",
LinoValue::array([
LinoValue::String("claude-haiku".to_string()),
LinoValue::String("claude-opus".to_string()),
]),
),
])
}
#[test]
fn a_record_is_written_on_one_line() {
let line = encode_line(&log_record());
assert!(
!line.contains('\n') && !line.contains('\r'),
"a record must stay on one line, got {line:?}"
);
assert_eq!(
line,
r#"(o: (bytes 2827) (complete true) (server (o: (host "127.0.0.1") (port 18878))) (models ("claude-haiku" "claude-opus")))"#
);
}
#[test]
fn a_line_is_valid_links_notation() {
let line = encode_line(&log_record());
parse_lino(&line).unwrap_or_else(|error| {
panic!("the notation's own parser rejected {line:?}: {error:?}");
});
}
#[test]
fn the_hand_rolled_dialect_is_the_one_the_parser_rejects() {
assert!(parse_lino(r#"((:"bytes" 2827) (:"complete" true))"#).is_err());
}
#[test]
fn both_forms_of_the_same_value_decode_alike() {
for value in [
log_record(),
LinoValue::Array(vec![]),
LinoValue::Object(vec![]),
LinoValue::array([LinoValue::Object(vec![]), LinoValue::Array(vec![])]),
LinoValue::object([("empty", LinoValue::Array(vec![]))]),
LinoValue::Int(42),
LinoValue::Null,
] {
assert_eq!(
decode(&encode_line(&value)),
decode(&encode(&value)),
"the two forms disagree about {value:?}"
);
assert_eq!(decode_line(&encode_line(&value)), Ok(value));
}
}
#[test]
fn a_string_keeps_its_own_characters_on_one_line() {
let value = LinoValue::object([(
"text",
LinoValue::String("quote \" backslash \\ ünïcödé".to_string()),
)]);
let line = encode_line(&value);
assert_eq!(line, r#"(o: (text 'quote " backslash \ ünïcödé'))"#);
assert_eq!(decode_line(&line), Ok(value));
}
#[test]
fn a_string_holding_a_newline_still_fits_on_one_line() {
let value = LinoValue::object([
("readable", LinoValue::String("still visible".to_string())),
("multiline", LinoValue::String("line1\nline2".to_string())),
]);
let line = encode_line(&value);
assert_eq!(
line,
r#"(o: (readable "still visible") (multiline (escaped "line1%0Aline2")))"#
);
assert!(line.contains("line1") && line.contains("line2"), "{line:?}");
assert!(
!line.contains('\n'),
"a record must stay on one line: {line:?}"
);
assert_eq!(decode_line(&line), Ok(value));
}
#[test]
fn a_one_pair_object_is_not_a_two_element_array() {
let object = LinoValue::object([("a", LinoValue::Int(1))]);
let array = LinoValue::array([LinoValue::String("a".to_string()), LinoValue::Int(1)]);
assert_eq!(encode_line(&object), "(o: (a 1))");
assert_eq!(encode_line(&array), r#"("a" 1)"#);
assert_eq!(decode_line("(o: (a 1))"), Ok(object));
assert_eq!(decode_line(r#"("a" 1)"#), Ok(array));
}
#[test]
fn the_empty_key_survives_the_round_trip() {
let value = LinoValue::object([("", LinoValue::Int(2))]);
assert_eq!(encode_line(&value), r#"(o: ("" 2))"#);
assert_eq!(decode_line(&encode_line(&value)), Ok(value));
}
#[test]
fn a_marked_object_holding_something_that_is_not_a_pair_is_rejected() {
let error = decode_line("(o: 1 2)").expect_err("a marked object holds pairs only");
assert!(
error.to_string().contains("pairs"),
"the error must say what a marked object holds, got {error}"
);
}
#[test]
fn several_lines_are_not_one_record() {
assert!(decode_line("(o: (a 1))\n(o: (b 2))").is_err());
}
#[test]
fn a_trailing_newline_is_not_a_second_record() {
assert_eq!(
decode_line("(o: (a 1))\n"),
Ok(LinoValue::object([("a", LinoValue::Int(1))]))
);
}
#[test]
fn a_line_starting_with_null_is_still_read_as_a_line() {
assert_eq!(
decode("(null 1)"),
Ok(LinoValue::array([LinoValue::Null, LinoValue::Int(1)]))
);
assert_eq!(
decode("(o: (a null))"),
Ok(LinoValue::object([("a", LinoValue::Null)]))
);
assert_eq!(decode("(null)"), Ok(LinoValue::Null));
}
#[test]
fn a_long_run_of_line_breaks_is_rejected_without_a_slowdown() {
let notation = format!("{}{}x", encode_line(&log_record()), "\n".repeat(200_000));
let started = std::time::Instant::now();
assert!(decode_line(¬ation).is_err());
assert!(started.elapsed() < std::time::Duration::from_secs(2));
}