#![cfg(feature = "std")]
use noyalib::{Value, from_str, from_value};
#[derive(serde::Deserialize, Debug)]
#[allow(dead_code)]
struct Server {
port: u16,
}
#[derive(serde::Deserialize, Debug)]
#[allow(dead_code)]
struct Cfg {
server: Server,
}
#[test]
fn nested_field_error_names_the_dotted_path() {
let err = from_str::<Cfg>("server:\n port: \"x\"\n").unwrap_err();
let msg = err.to_string();
assert!(msg.contains("server.port: "), "path missing: {msg}");
assert!(msg.contains("invalid type"), "wording lost: {msg}");
let loc = err.location().expect("location survives the prefix");
assert_eq!((loc.line(), loc.column()), (2, 9), "got: {msg}");
}
#[test]
fn sequence_index_is_bracketed() {
#[derive(serde::Deserialize, Debug)]
#[allow(dead_code)]
struct Doc {
items: Vec<i32>,
}
let err = from_str::<Doc>("items:\n - 1\n - x\n").unwrap_err();
let msg = err.to_string();
assert!(msg.contains("items[1]: "), "path missing: {msg}");
}
#[test]
fn root_sequence_leads_with_the_index() {
let err = from_str::<Vec<i32>>("- 1\n- x\n").unwrap_err();
let msg = err.to_string();
assert!(msg.contains("[1]: "), "path missing: {msg}");
}
#[test]
fn deeply_nested_mixed_path() {
#[derive(serde::Deserialize, Debug)]
#[allow(dead_code)]
struct Inner {
name: String,
count: u8,
}
#[derive(serde::Deserialize, Debug)]
#[allow(dead_code)]
struct Outer {
groups: Vec<Inner>,
}
#[derive(serde::Deserialize, Debug)]
#[allow(dead_code)]
struct Doc {
a: Outer,
}
let yaml =
"a:\n groups:\n - name: ok\n count: 1\n - name: bad\n count: many\n";
let err = from_str::<Doc>(yaml).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("a.groups[1].count: "), "path missing: {msg}");
}
#[test]
fn root_error_stays_unprefixed() {
let err = from_str::<i32>("hello").unwrap_err();
let msg = err.to_string();
assert!(
msg.contains(": invalid type") || msg.contains(": type mismatch"),
"got: {msg}"
);
assert!(!msg.contains(": : "), "double prefix: {msg}");
}
#[test]
fn from_value_errors_are_untouched() {
let v: Value = from_str("port: \"x\"\n").unwrap();
let err = from_value::<Server>(&v).unwrap_err();
assert!(err.location().is_none());
assert!(
!err.to_string().contains("port: invalid"),
"unexpected prefix without a source: {err}"
);
}
#[test]
fn swallowed_probe_error_does_not_leak_into_the_next_parse() {
#[derive(serde::Deserialize, Debug)]
#[serde(untagged)]
#[allow(dead_code)]
enum Loose {
Num(u64),
Text(String),
}
#[derive(serde::Deserialize, Debug)]
#[allow(dead_code)]
struct Probe {
field: Loose,
}
let ok: Probe = from_str("field: definitely-text\n").unwrap();
drop(ok);
let err = from_str::<i32>("hello").unwrap_err();
assert!(
!err.to_string().contains("field"),
"leaked path from the previous parse: {err}"
);
}
#[test]
fn streaming_wording_keeps_the_path_prefix() {
let err = from_str::<Cfg>("server:\n port: \"x\"\n").unwrap_err();
let msg = err.to_string();
assert!(msg.contains("server.port: "), "path: {msg}");
assert!(msg.contains("expected u16"), "streaming wording: {msg}");
assert!(msg.contains("line 2"), "location: {msg}");
}