#![cfg(feature = "strict-deserialise")]
#![allow(missing_docs)]
use noyalib::{Error, from_reader_strict, from_slice_strict, from_str, from_str_strict};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct ServerConfig {
port: u16,
host: String,
#[serde(default)]
tls: bool,
}
#[test]
fn lenient_from_str_silently_ignores_typos() {
let yaml = "port: 8080\nhost: api.example.com\nporrt: 9090\n";
let cfg: ServerConfig = from_str(yaml).unwrap();
assert_eq!(cfg.port, 8080);
assert_eq!(cfg.host, "api.example.com");
}
#[test]
fn strict_from_str_surfaces_typo_as_typed_error() {
let yaml = "port: 8080\nhost: api.example.com\nporrt: 9090\n";
let res: Result<ServerConfig, _> = from_str_strict(yaml);
let err = res.unwrap_err();
match &err {
Error::UnknownField(msg) => {
assert!(msg.contains("porrt"), "msg should name the typo: {msg}");
}
other => panic!("expected UnknownField, got {other:?}"),
}
}
#[test]
fn strict_passes_when_every_key_is_declared() {
let yaml = "port: 8080\nhost: api.example.com\ntls: true\n";
let cfg: ServerConfig = from_str_strict(yaml).unwrap();
assert_eq!(cfg.port, 8080);
assert!(cfg.tls);
}
#[test]
fn strict_lists_multiple_unknown_fields() {
let yaml = "
port: 8080
host: api.example.com
unknown_a: 1
unknown_b: 2
unknown_c: 3
";
let res: Result<ServerConfig, _> = from_str_strict(yaml);
let err = res.unwrap_err();
match &err {
Error::UnknownField(msg) => {
assert!(msg.contains("unknown_a"));
assert!(msg.contains("unknown_b"));
assert!(msg.contains("unknown_c"));
}
other => panic!("expected UnknownField, got {other:?}"),
}
}
#[test]
fn strict_walks_into_nested_structs() {
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Outer {
name: String,
server: ServerConfig,
}
let yaml = "
name: prod
server:
port: 8080
host: api
unknown: oops
";
let res: Result<Outer, _> = from_str_strict(yaml);
let err = res.unwrap_err();
match &err {
Error::UnknownField(msg) => {
assert!(msg.contains("unknown"));
}
other => panic!("expected UnknownField, got {other:?}"),
}
}
#[test]
fn strict_from_slice_surfaces_typo_as_typed_error() {
let yaml: &[u8] = b"port: 8080\nhost: api.example.com\nporrt: 9090\n";
let res: Result<ServerConfig, _> = from_slice_strict(yaml);
let err = res.unwrap_err();
match &err {
Error::UnknownField(msg) => assert!(msg.contains("porrt")),
other => panic!("expected UnknownField, got {other:?}"),
}
}
#[test]
fn strict_from_slice_passes_when_every_key_is_declared() {
let yaml: &[u8] = b"port: 8080\nhost: api.example.com\ntls: true\n";
let cfg: ServerConfig = from_slice_strict(yaml).unwrap();
assert_eq!(cfg.port, 8080);
assert!(cfg.tls);
}
#[test]
fn strict_from_slice_rejects_invalid_utf8() {
let yaml: &[u8] = b"port: \xff\n";
let res: Result<ServerConfig, _> = from_slice_strict(yaml);
assert!(res.is_err());
}
#[test]
fn strict_from_reader_surfaces_typo_as_typed_error() {
let yaml = b"port: 8080\nhost: api.example.com\nporrt: 9090\n".to_vec();
let res: Result<ServerConfig, _> = from_reader_strict(&yaml[..]);
let err = res.unwrap_err();
match &err {
Error::UnknownField(msg) => assert!(msg.contains("porrt")),
other => panic!("expected UnknownField, got {other:?}"),
}
}
#[test]
fn strict_from_reader_passes_when_every_key_is_declared() {
let yaml = b"port: 8080\nhost: api.example.com\ntls: true\n".to_vec();
let cfg: ServerConfig = from_reader_strict(&yaml[..]).unwrap();
assert_eq!(cfg.port, 8080);
assert!(cfg.tls);
}
#[test]
fn strict_from_reader_surfaces_io_error() {
struct FailingReader;
impl std::io::Read for FailingReader {
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
Err(std::io::Error::other("boom"))
}
}
let res: Result<ServerConfig, _> = from_reader_strict(FailingReader);
let err = res.unwrap_err();
assert!(
matches!(err, Error::Io(_)),
"expected Error::Io, got {err:?}"
);
}
#[test]
fn strict_from_reader_rejects_invalid_utf8() {
let bad: &[u8] = b"port: \xff\n";
let res: Result<ServerConfig, _> = from_reader_strict(bad);
assert!(res.is_err());
}
#[test]
fn parse_error_carries_line_and_column() {
let yaml = "ok: 1\nbad: [\n";
let err = from_str::<noyalib::Value>(yaml).unwrap_err();
let loc = err.location().expect("parse error must carry a location");
assert!(loc.line() >= 1);
assert!(loc.column() >= 1);
}
#[test]
fn format_with_source_renders_caret_under_offending_column() {
let yaml = "key: bad\nbad: [unclosed\nkey: ok\n";
let err = from_str::<noyalib::Value>(yaml).unwrap_err();
let snippet = err.format_with_source(yaml);
assert!(snippet.contains("error"));
}
#[test]
fn format_with_source_radius_includes_context_lines() {
let yaml = "\
header: ok
service:
nested: x
bad: y
trailer: ok
";
let err = from_str::<noyalib::Value>(yaml).unwrap_err();
let snippet = err.format_with_source_radius(yaml, 1);
assert!(snippet.contains('|'));
assert!(snippet.contains("bad: y") || snippet.contains("nested: x"));
}
#[test]
fn type_mismatch_surfaces_as_typed_error() {
let yaml = "port: not-an-integer\nhost: api\n";
let res: Result<ServerConfig, _> = from_str(yaml);
assert!(res.is_err(), "string-shaped port must not coerce into u16");
}
#[test]
fn missing_required_field_surfaces_specific_error() {
let yaml = "host: api.example.com\n";
let res: Result<ServerConfig, _> = from_str(yaml);
assert!(res.is_err());
}