mod value;
use value::ValueDeserializer;
pub use value::from_value;
pub const MAX_SERDE_NESTING: usize = 128;
pub(crate) fn enter(depth: usize) -> Result<usize, UclError> {
if depth < MAX_SERDE_NESTING {
Ok(depth + 1)
} else {
Err(UclError::Serde(crate::error::SerdeError::TooDeep {
limit: MAX_SERDE_NESTING,
}))
}
}
use crate::error::UclError;
use crate::parse::{Parser, ParserBuilder, Source};
use serde::de::{self, Deserialize, DeserializeOwned, Visitor};
use std::collections::HashMap;
use std::io;
#[cfg(feature = "fs")]
use std::path::Path;
pub struct UclDeserializer<'a> {
parser: Parser,
input: &'a [u8],
}
impl<'a> UclDeserializer<'a> {
pub fn new(input: &'a str) -> Self {
Self::from_slice(input.as_bytes())
}
pub fn from_slice(input: &'a [u8]) -> Self {
Self::from_parser(Parser::new(), input)
}
pub fn from_parser(parser: Parser, input: &'a [u8]) -> Self {
Self { parser, input }
}
pub fn parser(&self) -> &Parser {
&self.parser
}
pub fn parser_mut(&mut self) -> &mut Parser {
&mut self.parser
}
}
#[cold]
#[inline(never)]
fn locate(parser: &mut Parser, source: &Source<'_>, error: UclError) -> UclError {
let mut error = error.in_document();
if let UclError::Deserialize(e) = &mut error
&& let Some((_, facts)) = parser.parse_located(source)
{
let (path, key) = e.locate_request();
if let Some((position, file)) = facts.position_of(path, key, source.input()) {
e.set_position(position, file);
}
}
error
}
fn deserialize_bytes<'de, T: Deserialize<'de>>(
mut parser: Parser,
input: &[u8],
) -> Result<T, UclError> {
let value = parser.parse(input)?;
deserialize_parsed(&mut parser, &Source::Bytes(input), value)
}
fn deserialize_parsed<'de, T: Deserialize<'de>>(
parser: &mut Parser,
source: &Source<'_>,
value: crate::value::UclValue,
) -> Result<T, UclError> {
T::deserialize(ValueDeserializer::<false>::new(value))
.map_err(|e| explain::<T>(parser, source, e))
}
#[cold]
#[inline(never)]
fn explain<'de, T: Deserialize<'de>>(
parser: &mut Parser,
source: &Source<'_>,
error: UclError,
) -> UclError {
let error = error.in_document();
let UclError::Deserialize(first) = &error else {
return error;
};
let message = first.error().to_string();
let Some((value, facts)) = parser.parse_located(source) else {
return error;
};
match T::deserialize(ValueDeserializer::<true>::new(value)).map(drop) {
Err(again) => match again.in_document() {
UclError::Deserialize(mut e) if e.error().to_string() == message => {
let (path, key) = e.locate_request();
if let Some((position, file)) = facts.position_of(path, key, source.input()) {
e.set_position(position, file);
}
UclError::Deserialize(e)
}
_ => error,
},
Ok(()) => error,
}
}
macro_rules! forward_to_value {
($($method:ident($($arg:ident: $ty:ty),*))*) => {
$(
fn $method<V>(mut self, $($arg: $ty,)* visitor: V) -> Result<V::Value, UclError>
where
V: Visitor<'de>,
{
let value = self.parser.parse(self.input)?;
ValueDeserializer::<true>::new(value)
.$method($($arg,)* visitor)
.map_err(|e| locate(&mut self.parser, &Source::Bytes(self.input), e))
}
)*
};
}
impl<'de> de::Deserializer<'de> for UclDeserializer<'de> {
type Error = UclError;
forward_to_value! {
deserialize_any()
deserialize_bool()
deserialize_i8()
deserialize_i16()
deserialize_i32()
deserialize_i64()
deserialize_i128()
deserialize_u8()
deserialize_u16()
deserialize_u32()
deserialize_u64()
deserialize_u128()
deserialize_f32()
deserialize_f64()
deserialize_char()
deserialize_str()
deserialize_string()
deserialize_bytes()
deserialize_byte_buf()
deserialize_option()
deserialize_unit()
deserialize_unit_struct(name: &'static str)
deserialize_newtype_struct(name: &'static str)
deserialize_seq()
deserialize_tuple(len: usize)
deserialize_tuple_struct(name: &'static str, len: usize)
deserialize_map()
deserialize_struct(name: &'static str, fields: &'static [&'static str])
deserialize_enum(name: &'static str, variants: &'static [&'static str])
deserialize_identifier()
deserialize_ignored_any()
}
}
pub fn from_str<'a, T>(s: &'a str) -> Result<T, UclError>
where
T: Deserialize<'a>,
{
from_slice(s.as_bytes())
}
pub fn from_slice<'a, T>(v: &'a [u8]) -> Result<T, UclError>
where
T: Deserialize<'a>,
{
deserialize_bytes(Parser::new(), v)
}
pub fn from_reader<T>(mut reader: impl io::Read) -> Result<T, UclError>
where
T: DeserializeOwned,
{
let mut input = Vec::new();
reader.read_to_end(&mut input)?;
from_slice(&input)
}
#[cfg(feature = "fs")]
pub fn from_file<T>(path: impl AsRef<Path>) -> Result<T, UclError>
where
T: DeserializeOwned,
{
let mut parser = Parser::new();
parser.set_loader(crate::parse::FsLoader::new());
let (canonical, input) = parser
.read_file(path.as_ref())
.map_err(|(path, e)| io::Error::new(e.kind(), format!("{}: {e}", path.display())))?;
let value = parser.parse_read_file(canonical.clone(), &input)?;
let source = Source::File {
canonical,
input: &input,
};
deserialize_parsed(&mut parser, &source, value)
}
pub fn from_str_with_variables<'a, T, I, K, V>(s: &'a str, variables: I) -> Result<T, UclError>
where
T: Deserialize<'a>,
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
let parser = ParserBuilder::new().with_variables(variables).build();
deserialize_bytes(parser, s.as_bytes())
}
pub fn from_str_with_map<'a, T>(
s: &'a str,
variables: HashMap<String, String>,
) -> Result<T, UclError>
where
T: Deserialize<'a>,
{
let mut variables: Vec<(String, String)> = variables.into_iter().collect();
variables.sort_unstable_by(|a, b| b.0.cmp(&a.0));
from_str_with_variables(s, variables)
}
pub fn from_str_with_env<'a, T>(s: &'a str) -> Result<T, UclError>
where
T: Deserialize<'a>,
{
let parser = ParserBuilder::new()
.with_variable_handler(environment_variable)
.build();
deserialize_bytes(parser, s.as_bytes())
}
fn environment_variable(name: &str) -> Option<String> {
if name.is_empty() || name.contains(['=', '\0']) {
return None;
}
std::env::var(name).ok()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse::ErrorKind;
use crate::value::UclValue;
use serde::Deserialize;
use std::collections::HashMap;
fn with_value(value: UclValue) -> ValueDeserializer<true> {
ValueDeserializer::new(value)
}
fn parse_error(input: &str) -> (ErrorKind, usize, usize) {
match from_str::<serde_json::Value>(input).unwrap_err() {
UclError::Syntax(e) => (e.kind().clone(), e.position().line, e.position().column),
other => panic!("expected a parse error, got {other:?}"),
}
}
#[test]
fn test_parse_errors_keep_kind_and_position() {
assert_eq!(
parse_error("\nkey = \"unterminated"),
(ErrorKind::UnterminatedString, 2, 7)
);
assert_eq!(parse_error("a = [1, 2").0, ErrorKind::UnterminatedArray);
assert_eq!(
parse_error("a = 1\nb = 2\n}"),
(ErrorKind::UnmatchedClose { found: '}' }, 3, 1)
);
}
#[derive(Debug, Deserialize, PartialEq)]
struct UnsignedField {
v: u64,
}
#[derive(Debug, Deserialize, PartialEq)]
struct SignedField {
v: i64,
}
#[derive(Debug, Deserialize, PartialEq)]
struct ByteField {
v: u8,
}
#[test]
fn test_float_to_integer_only_when_integral_and_in_range() {
assert_eq!(from_str::<UnsignedField>("v = 30.0").unwrap().v, 30);
assert_eq!(from_str::<UnsignedField>("v = 30s").unwrap().v, 30);
assert_eq!(from_str::<SignedField>("v = -30.0").unwrap().v, -30);
assert_eq!(from_str::<SignedField>("v = 30").unwrap().v, 30);
for input in [
"v = 30.7",
"v = 1e300",
"v = -1.0",
"v = nan",
"v = inf",
"v = -1",
] {
assert!(
from_str::<UnsignedField>(input).is_err(),
"u64 from {input:?}"
);
}
for input in ["v = 30.7", "v = 1e300", "v = -1e300", "v = nan"] {
assert!(
from_str::<SignedField>(input).is_err(),
"i64 from {input:?}"
);
}
assert!(from_str::<ByteField>("v = 300.0").is_err());
assert_eq!(from_str::<ByteField>("v = 255.0").unwrap().v, 255);
}
#[test]
fn test_document_and_nested_values_follow_the_same_rules() {
assert_eq!(
u64::deserialize(with_value(UclValue::Float(30.0))).unwrap(),
30
);
assert_eq!(
i64::deserialize(with_value(UclValue::Time(-2.0))).unwrap(),
-2
);
assert!(u64::deserialize(with_value(UclValue::Float(30.7))).is_err());
assert!(u64::deserialize(with_value(UclValue::Float(1e300))).is_err());
assert!(i64::deserialize(with_value(UclValue::Float(f64::NAN))).is_err());
assert_eq!(
Option::<u32>::deserialize(with_value(UclValue::Integer(7))).unwrap(),
Some(7)
);
assert_eq!(
Option::<u32>::deserialize(with_value(UclValue::Null)).unwrap(),
None
);
}
#[derive(Debug, Deserialize, PartialEq)]
struct Port(u16);
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
enum Level {
Debug,
Info,
}
#[derive(Debug, Deserialize, PartialEq)]
struct Fields {
maybe: Option<u32>,
#[serde(default)]
absent: Option<u32>,
nothing: Option<u32>,
level: Level,
port: Port,
}
#[test]
fn test_option_enum_and_newtype_fields() {
let parsed: Fields =
from_str("maybe = 30; nothing = null; level = \"info\"; port = 8080").unwrap();
assert_eq!(
parsed,
Fields {
maybe: Some(30),
absent: None,
nothing: None,
level: Level::Info,
port: Port(8080),
}
);
assert!(from_str::<Fields>("maybe = 1; nothing = null; level = trace; port = 1").is_err());
let _ = Level::Debug;
}
#[test]
fn test_time_reads_as_seconds() {
#[derive(Deserialize)]
struct Timeouts {
connect: f64,
read: f32,
}
let parsed: Timeouts = from_str("connect = 1.5min; read = 250ms").unwrap();
assert_eq!(parsed.connect, 90.0);
assert_eq!(parsed.read, 0.25);
}
#[test]
fn test_repeated_key_reads_as_sequence() {
#[derive(Deserialize)]
struct Servers {
server: Vec<String>,
}
let parsed: Servers = from_str("server = a; server = b; server = c").unwrap();
assert_eq!(parsed.server, vec!["a", "b", "c"]);
let value: serde_json::Value = from_str("k = [1, 2]\nk = 3").unwrap();
assert_eq!(value["k"], serde_json::json!([[1, 2], 3]));
}
#[derive(Debug, Deserialize, PartialEq)]
struct Repos {
#[serde(default)]
repo: Vec<String>,
mirror: Option<Vec<String>>,
}
#[test]
fn test_one_or_many_sequences() {
let parse = |input: &str| from_str::<Repos>(input).unwrap();
let strings = |items: &[&str]| items.iter().map(|s| s.to_string()).collect::<Vec<_>>();
assert_eq!(
parse(""),
Repos {
repo: vec![],
mirror: None
}
);
assert_eq!(
parse("repo = a\nmirror = m1"),
Repos {
repo: strings(&["a"]),
mirror: Some(strings(&["m1"]))
}
);
assert_eq!(
parse("repo = a\nrepo = b\nmirror = m1\nmirror = m2"),
Repos {
repo: strings(&["a", "b"]),
mirror: Some(strings(&["m1", "m2"]))
}
);
assert_eq!(parse(r#"repo = ["a", "b"]"#).repo, strings(&["a", "b"]));
assert_eq!(parse("mirror = null").mirror, None);
assert_eq!(
<(u32,)>::deserialize(with_value(UclValue::Integer(5))).unwrap(),
(5,)
);
}
#[test]
fn test_object_is_not_a_sequence() {
#[derive(Debug, Deserialize)]
struct Sections {
#[allow(
dead_code,
reason = "a deserialization target; the test checks the error"
)]
section: Vec<u32>,
}
let err = from_str::<Sections>("section { a = 1; b = 2 }").unwrap_err();
assert!(err.to_string().contains("invalid type: map"), "{err}");
#[derive(Debug, Deserialize)]
struct Named {
section: HashMap<String, u32>,
}
let named: Named = from_str("section { a = 1; b = 2 }").unwrap();
assert_eq!(named.section.len(), 2);
let repeated: Vec<HashMap<String, u32>> =
<Vec<HashMap<String, u32>>>::deserialize(with_value(UclValue::Array(vec![
UclValue::Object([("a", UclValue::Integer(1))].into_iter().collect()),
UclValue::Object([("b", UclValue::Integer(2))].into_iter().collect()),
])))
.unwrap();
assert_eq!(repeated.len(), 2);
}
#[test]
fn test_type_errors_name_what_was_found() {
let err = from_str::<UnsignedField>("v = \"30\"").unwrap_err();
assert!(
err.to_string().contains("invalid type: string \"30\""),
"{err}"
);
let err = from_str::<SignedField>("v = 30s").map(|_| ()).err();
assert!(err.is_none(), "an integral time converts");
#[derive(Debug, Deserialize)]
struct Flag {
#[allow(
dead_code,
reason = "a deserialization target; the test checks the error"
)]
f: bool,
}
let err = from_str::<Flag>("f = 1").unwrap_err();
assert!(
err.to_string().contains("invalid type: integer `1`"),
"{err}"
);
}
#[derive(Debug, Deserialize, PartialEq)]
struct TestStruct {
name: String,
age: u32,
}
#[derive(Debug, Deserialize, PartialEq)]
struct NestedStruct {
user: TestStruct,
active: bool,
}
#[derive(Debug, Deserialize, PartialEq)]
struct OptionalFields {
required: String,
#[serde(default)]
optional: Option<String>,
#[serde(default = "default_number")]
number: i32,
}
fn default_number() -> i32 {
42
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
struct RenamedFields {
first_name: String,
last_name: String,
}
#[derive(Debug, Deserialize, PartialEq)]
enum TestEnum {
Unit,
Newtype(String),
Tuple(String, i32),
Struct { field: String },
}
#[derive(Debug, Deserialize, PartialEq)]
struct EnumField {
e: TestEnum,
}
#[test]
fn test_basic_struct_deserialization() {
let parsed: TestStruct = from_str(r#"{ name = "Alice", age = 30 }"#).unwrap();
assert_eq!(
parsed,
TestStruct {
name: "Alice".into(),
age: 30
}
);
}
#[test]
fn test_nested_struct_deserialization() {
let ucl = r#"{
user = { name = "Bob", age = 25 },
active = true
}"#;
let parsed: NestedStruct = from_str(ucl).unwrap();
assert_eq!(parsed.user.name, "Bob");
assert_eq!(parsed.user.age, 25);
assert!(parsed.active);
}
#[test]
fn test_array_deserialization() {
let parsed: Vec<i32> = from_str("[1, 2, 3, 4, 5]").unwrap();
assert_eq!(parsed, vec![1, 2, 3, 4, 5]);
}
#[test]
fn test_map_deserialization() {
let parsed: HashMap<String, String> =
from_str(r#"{ key1 = "value1", key2 = "value2" }"#).unwrap();
assert_eq!(parsed.get("key1"), Some(&"value1".to_string()));
assert_eq!(parsed.get("key2"), Some(&"value2".to_string()));
}
#[test]
fn test_optional_fields() {
let parsed: OptionalFields = from_str(r#"{ required = "test" }"#).unwrap();
assert_eq!(parsed.required, "test");
assert_eq!(parsed.optional, None);
assert_eq!(parsed.number, 42);
let parsed: OptionalFields = from_str(r#"{ required = "a", optional = "b" }"#).unwrap();
assert_eq!(parsed.optional.as_deref(), Some("b"));
}
#[test]
fn test_renamed_fields() {
let parsed: RenamedFields =
from_str(r#"{ first_name = "John", last_name = "Doe" }"#).unwrap();
assert_eq!(parsed.first_name, "John");
assert_eq!(parsed.last_name, "Doe");
}
#[test]
fn test_enum_variants() {
let parse = |input: &str| from_str::<EnumField>(input).map(|f| f.e);
assert_eq!(parse(r#"e = "Unit""#).unwrap(), TestEnum::Unit);
assert_eq!(
parse(r#"e = { Newtype = "test_value" }"#).unwrap(),
TestEnum::Newtype("test_value".into())
);
assert_eq!(
parse(r#"e = { Tuple = ["a", 1] }"#).unwrap(),
TestEnum::Tuple("a".into(), 1)
);
assert_eq!(
parse(r#"e = { Struct = { field = "test" } }"#).unwrap(),
TestEnum::Struct {
field: "test".into()
}
);
assert_eq!(
from_str::<TestEnum>(r#"{ Newtype = "x" }"#).unwrap(),
TestEnum::Newtype("x".into())
);
assert!(parse(r#"e = { Unit = 1, Newtype = "x" }"#).is_err());
assert!(parse("e = 1").is_err());
}
#[test]
fn test_variable_expansion() {
let mut variables = HashMap::new();
variables.insert("name".to_string(), "World".to_string());
let parsed: HashMap<String, String> =
from_str_with_map(r#"{ greeting = "Hello ${name}! $name." }"#, variables).unwrap();
assert_eq!(parsed["greeting"], "Hello World! World.");
let parse = |variables: Vec<(&str, &str)>| {
from_str_with_variables::<HashMap<String, String>, _, _, _>("v = \"$ABC\"", variables)
.unwrap()
.remove("v")
.unwrap()
};
assert_eq!(parse(vec![("AB", "1"), ("ABC", "2")]), "1C");
assert_eq!(parse(vec![("ABC", "2"), ("AB", "1")]), "2");
let map: HashMap<String, String> = [("AB", "1"), ("ABC", "2"), ("A", "0")]
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
let parsed: HashMap<String, String> =
from_str_with_map("v = \"$ABC $ABD $AX\"", map).unwrap();
assert_eq!(parsed["v"], "2 1D 0X");
}
#[test]
fn test_environment_variables() {
unsafe {
std::env::set_var("TEST_UCL_VAR", "test_value");
}
let result: Result<HashMap<String, String>, _> = from_str_with_env(
r#"{ braced = "${TEST_UCL_VAR}", unbraced = "$TEST_UCL_VAR", unset = "${TEST_UCL_UNSET_VAR}", odd = "${} ${A=B}" }"#,
);
unsafe {
std::env::remove_var("TEST_UCL_VAR");
}
let result = result.unwrap();
assert_eq!(result["braced"], "test_value");
assert_eq!(result["unbraced"], "$TEST_UCL_VAR");
assert_eq!(result["unset"], "${TEST_UCL_UNSET_VAR}");
assert_eq!(result["odd"], "${} ${A=B}");
let result: HashMap<String, String> = from_str_with_env("f = \"${FILENAME}\"").unwrap();
assert_eq!(result["f"], "undef");
}
#[test]
fn test_type_coercion() {
#[derive(Debug, Deserialize)]
struct TypeCoercion {
as_i32: i32,
as_f64: f64,
as_string: String,
as_bool: bool,
}
let ucl = r#"{
as_i32 = 42,
as_f64 = 42,
as_string = "42",
as_bool = true
}"#;
let parsed: TypeCoercion = from_str(ucl).unwrap();
assert_eq!(parsed.as_i32, 42);
assert_eq!(parsed.as_f64, 42.0);
assert_eq!(parsed.as_string, "42");
assert!(parsed.as_bool);
}
#[test]
fn test_error_propagation() {
let result: Result<TestStruct, _> = from_str(r#"{ invalid syntax }"#);
let error = result.unwrap_err();
assert!(!error.to_string().is_empty());
}
#[test]
fn test_convenience_functions() {
let ucl = r#"{ name = "test", age = 25 }"#;
let expected = TestStruct {
name: "test".into(),
age: 25,
};
assert_eq!(from_str::<TestStruct>(ucl).unwrap(), expected);
assert_eq!(from_slice::<TestStruct>(ucl.as_bytes()).unwrap(), expected);
assert_eq!(from_reader::<TestStruct>(ucl.as_bytes()).unwrap(), expected);
assert_eq!(
from_str_with_variables::<TestStruct, _, &str, &str>(ucl, []).unwrap(),
expected
);
assert_eq!(
from_str_with_map::<TestStruct>(ucl, HashMap::new()).unwrap(),
expected
);
assert_eq!(from_str_with_env::<TestStruct>(ucl).unwrap(), expected);
}
#[test]
fn test_parser_settings_are_honoured() {
let parse = |parser: Parser| {
serde_json::Value::deserialize(UclDeserializer::from_parser(
parser,
b"t = 10s\nv = \"${N}\"",
))
.unwrap()
};
let with_default = parse(ParserBuilder::new().with_variable("N", "x").build());
assert_eq!(with_default["t"], 10.0);
assert_eq!(with_default["v"], "x");
let with_flags = parse(
ParserBuilder::new()
.with_flags(crate::ParserFlags::NO_TIME)
.with_variable("N", "x")
.build(),
);
assert_eq!(with_flags["t"], "10s");
assert_eq!(with_flags["v"], "x");
}
#[test]
fn test_deserializer_methods() {
let mut deserializer = UclDeserializer::new("test");
assert!(deserializer.parser().flags().is_empty());
deserializer
.parser_mut()
.set_flags(crate::ParserFlags::KEY_LOWERCASE);
assert_eq!(
deserializer.parser().flags(),
crate::ParserFlags::KEY_LOWERCASE
);
}
}