use crate::error::{Error, Result};
use crate::parser::{self};
use crate::prelude::*;
#[cfg(feature = "std")]
use crate::span_context;
use crate::value::Value;
#[cfg(feature = "std")]
use std::io;
mod config;
mod deserializer;
pub use config::{
DuplicateKeyPolicy, MergeKeyPolicy, NonScalarKeyPolicy, ParserConfig, RequireIndent,
YamlVersion,
};
pub use deserializer::Deserializer;
pub(crate) use deserializer::{EmptyMapAccess, SpannedMapAccess, is_binary_tag};
pub fn from_str<T>(s: &str) -> Result<T>
where
T: for<'de> serde_core::Deserialize<'de> + 'static,
{
from_str_with_config(s, &ParserConfig::default())
}
pub fn from_str_borrowing<'de, T>(s: &'de str) -> Result<T>
where
T: serde_core::Deserialize<'de>,
{
from_str_borrowing_with_config(s, &ParserConfig::default())
}
pub fn from_str_borrowing_with_config<'de, T>(s: &'de str, config: &ParserConfig) -> Result<T>
where
T: serde_core::Deserialize<'de>,
{
let parse_config = parser::ParseConfig::from(config);
if s.len() > parse_config.max_document_length {
return Err(Error::Parse(format!(
"document exceeds maximum length of {} bytes",
parse_config.max_document_length
)));
}
let mut de = crate::streaming::StreamingDeserializer::with_config(s, parse_config);
if let Some(registry) = config.tag_registry.as_ref() {
de = de.with_tag_registry(Arc::clone(registry));
}
T::deserialize(&mut de)
}
fn is_value_target<T: 'static + ?Sized>() -> bool {
use core::any::TypeId;
TypeId::of::<T>() == TypeId::of::<Value>()
}
#[cfg(all(feature = "std", feature = "figment"))]
pub(crate) fn from_str_typed_no_tag_preserve<T>(s: &str, config: &ParserConfig) -> Result<T>
where
T: for<'de> serde_core::Deserialize<'de>,
{
let stream_eligible = config.merge_key_policy == MergeKeyPolicy::Auto
&& !config.ignore_binary_tag_for_string
&& config.policies.is_empty();
let mut streaming_err = None;
if stream_eligible {
if let Some(res) = crate::streaming::from_str_streaming(s, config) {
match res {
Err(err) if err.location().is_none() => streaming_err = Some(err),
other => return other,
}
}
}
let parse_config = parser::ParseConfig::from(config);
let (value, span_tree) = parser::parse_one(s, &parse_config)?;
for p in &config.policies {
p.check_value(&value)?;
}
let spans = span_context::build_span_map(&value, &span_tree);
let ctx = span_context::SpanContext {
spans,
source: s.into(),
};
let _guard = span_context::set_span_context(ctx);
let de = Deserializer::with_options(
&value,
Some(_guard.as_ref()),
config.ignore_binary_tag_for_string,
config.plain_scalar_strings,
);
attach_field_path(
locate_streaming_error(T::deserialize(de), streaming_err),
&value,
)
}
#[cfg(all(feature = "std", feature = "strict-deserialise"))]
pub fn from_str_strict<T>(s: &str) -> Result<T>
where
T: for<'de> serde_core::Deserialize<'de> + 'static,
{
let unknown = std::sync::Mutex::new(Vec::<String>::new());
let value: Value = from_str_with_config(s, &ParserConfig::default())?;
let result: Result<T> = serde_ignored::deserialize(&value, |path| {
unknown
.lock()
.expect("from_str_strict: ignored-paths lock poisoned")
.push(path.to_string());
});
let extras = unknown
.into_inner()
.expect("from_str_strict: ignored-paths lock poisoned");
let typed = result?;
if !extras.is_empty() {
let msg = if extras.len() == 1 {
format!("unknown field at `{}`", extras[0])
} else {
let joined = extras
.iter()
.map(|p| format!("`{p}`"))
.collect::<Vec<_>>()
.join(", ");
format!("unknown fields: {joined}")
};
return Err(Error::UnknownField(msg));
}
Ok(typed)
}
#[cfg(all(feature = "std", feature = "strict-deserialise"))]
pub fn from_slice_strict<T>(b: &[u8]) -> Result<T>
where
T: for<'de> serde_core::Deserialize<'de> + 'static,
{
let s = core::str::from_utf8(b).map_err(|e| Error::Deserialize(e.to_string()))?;
from_str_strict(s)
}
#[cfg(all(feature = "std", feature = "strict-deserialise"))]
pub fn from_reader_strict<R, T>(mut reader: R) -> Result<T>
where
R: io::Read,
T: for<'de> serde_core::Deserialize<'de> + 'static,
{
let mut s = String::new();
let _ = reader.read_to_string(&mut s).map_err(Error::Io)?;
from_str_strict(&s)
}
pub fn from_str_with_config<T>(s: &str, config: &ParserConfig) -> Result<T>
where
T: for<'de> serde_core::Deserialize<'de> + 'static,
{
let value_target_bypass = is_value_target::<T>();
let stream_eligible = config.merge_key_policy == MergeKeyPolicy::Auto
&& !config.ignore_binary_tag_for_string
&& config.policies.is_empty()
&& properties_inactive(config)
&& includes_inactive(config)
&& !value_target_bypass;
let mut streaming_err = None;
if stream_eligible {
if let Some(res) = crate::streaming::from_str_streaming(s, config) {
match res {
Err(err) if err.location().is_none() => streaming_err = Some(err),
other => return other,
}
}
}
let parse_config = parser::ParseConfig::from(config);
if s.len() > parse_config.max_document_length {
return Err(Error::Parse(format!(
"document exceeds maximum length of {} bytes",
parse_config.max_document_length
)));
}
if is_value_target::<T>() {
let mut value = parser::parse_exactly_one_value(s, &parse_config)?;
apply_includes(&mut value, config)?;
apply_properties(&mut value, config)?;
for p in &config.policies {
p.check_value(&value)?;
}
let boxed: Box<dyn core::any::Any> = Box::new(value);
let downcast: Box<T> = boxed
.downcast::<T>()
.expect("is_value_target proved T == Value");
return Ok(*downcast);
}
#[cfg(feature = "std")]
{
let (mut value, span_tree) = parser::parse_exactly_one(s, &parse_config)?;
apply_includes(&mut value, config)?;
apply_properties(&mut value, config)?;
for p in &config.policies {
p.check_value(&value)?;
}
let spans = span_context::build_span_map(&value, &span_tree);
let ctx = span_context::SpanContext {
spans,
source: s.into(),
};
let _guard = span_context::set_span_context(ctx);
let de = Deserializer::with_options(
&value,
Some(_guard.as_ref()),
config.ignore_binary_tag_for_string,
config.plain_scalar_strings,
);
attach_field_path(
locate_streaming_error(T::deserialize(de), streaming_err),
&value,
)
}
#[cfg(not(feature = "std"))]
{
let value = parser::parse_exactly_one_value(s, &parse_config)?;
let de = Deserializer::with_options(
&value,
None,
config.ignore_binary_tag_for_string,
config.plain_scalar_strings,
);
locate_streaming_error(T::deserialize(de), streaming_err)
}
}
fn locate_streaming_error<T>(ast_result: Result<T>, streaming_err: Option<Error>) -> Result<T> {
match (ast_result, streaming_err) {
(Err(ast_err), Some(_))
if matches!(
ast_err.kind(),
crate::error::ErrorKind::DuplicateKey | crate::error::ErrorKind::KeyCollision
) && ast_err.location().is_some() =>
{
Err(ast_err)
}
(Err(ast_err), Some(streaming_err)) => Err(match ast_err.location() {
Some(location) => {
let message = match streaming_err {
Error::Custom(message) | Error::Deserialize(message) => message,
other => other.to_string(),
};
Error::DeserializeWithLocation { message, location }
}
None => streaming_err,
}),
(result, _) => result,
}
}
#[cfg(feature = "std")]
fn attach_field_path<T>(result: Result<T>, root: &Value) -> Result<T> {
let recorded = span_context::take_error_node();
let err = match result {
Ok(v) => return Ok(v),
Err(err) => err,
};
let Some(addr) = recorded else {
return Err(err);
};
let mut segments = Vec::new();
if !find_value_path(root, addr, &mut segments) || segments.is_empty() {
return Err(err);
}
let path = format_value_path(&segments);
Err(match err {
Error::DeserializeWithLocation { message, location } => Error::DeserializeWithLocation {
message: format!("{path}: {message}"),
location,
},
Error::Deserialize(message) => Error::Deserialize(format!("{path}: {message}")),
Error::Custom(message) => Error::Custom(format!("{path}: {message}")),
other => other,
})
}
#[cfg(feature = "std")]
enum PathSegment<'a> {
Key(&'a str),
Index(usize),
}
#[cfg(feature = "std")]
fn find_value_path<'a>(value: &'a Value, addr: usize, segments: &mut Vec<PathSegment<'a>>) -> bool {
if core::ptr::from_ref(value) as usize == addr {
return true;
}
match value {
Value::Mapping(map) => {
for (key, child) in map {
segments.push(PathSegment::Key(key));
if find_value_path(child, addr, segments) {
return true;
}
let _ = segments.pop();
}
}
Value::Sequence(seq) => {
for (index, child) in seq.iter().enumerate() {
segments.push(PathSegment::Index(index));
if find_value_path(child, addr, segments) {
return true;
}
let _ = segments.pop();
}
}
Value::Tagged(tagged) => return find_value_path(tagged.value(), addr, segments),
_ => {}
}
false
}
#[cfg(feature = "std")]
fn format_value_path(segments: &[PathSegment<'_>]) -> String {
use core::fmt::Write as _;
let mut out = String::new();
for segment in segments {
match segment {
PathSegment::Key(key) => {
if !out.is_empty() {
out.push('.');
}
out.push_str(key);
}
PathSegment::Index(index) => {
let _ = write!(out, "[{index}]");
}
}
}
out
}
#[cfg(feature = "std")]
#[inline]
fn properties_inactive(config: &ParserConfig) -> bool {
config.properties.is_none()
}
#[cfg(not(feature = "std"))]
#[inline]
fn properties_inactive(_config: &ParserConfig) -> bool {
true
}
#[cfg(feature = "std")]
fn apply_properties(value: &mut Value, config: &ParserConfig) -> Result<()> {
if let Some(props) = config.properties.as_ref() {
let action = if config.strict_properties {
crate::value::MissingAction::Error(false)
} else {
crate::value::MissingAction::Empty
};
value.interpolate_inner(
&|name| match props.get(name) {
Some(v) => crate::value::ResolveOutcome::Found(v.clone()),
None => crate::value::ResolveOutcome::Missing,
},
action,
)?;
}
Ok(())
}
#[cfg(not(feature = "std"))]
#[inline]
fn apply_properties(_value: &mut Value, _config: &ParserConfig) -> Result<()> {
Ok(())
}
#[cfg(feature = "include")]
#[inline]
fn includes_inactive(config: &ParserConfig) -> bool {
config.include_resolver.is_none()
}
#[cfg(not(feature = "include"))]
#[inline]
fn includes_inactive(_config: &ParserConfig) -> bool {
true
}
#[cfg(feature = "include")]
fn apply_includes(value: &mut Value, config: &ParserConfig) -> Result<()> {
if let Some(resolver) = config.include_resolver.as_ref() {
let parse_config = parser::ParseConfig::from(config);
let mut visited: FxHashSet<String> = FxHashSet::default();
let mut next_id: usize = 1;
resolve_includes_recursive(
value,
resolver,
&parse_config,
config.max_include_depth,
0,
0,
&mut visited,
&mut next_id,
)?;
}
Ok(())
}
#[cfg(not(feature = "include"))]
#[inline]
fn apply_includes(_value: &mut Value, _config: &ParserConfig) -> Result<()> {
Ok(())
}
#[cfg(feature = "include")]
#[allow(clippy::too_many_arguments)]
fn resolve_includes_recursive(
value: &mut Value,
resolver: &crate::include::IncludeResolver,
parse_config: &parser::ParseConfig,
max_depth: usize,
depth: usize,
from_id: usize,
visited: &mut FxHashSet<String>,
next_id: &mut usize,
) -> Result<()> {
if depth > max_depth {
return Err(Error::RecursionLimitExceeded { depth });
}
match value {
Value::Tagged(boxed) => {
if boxed.tag().as_str() == "!include" {
let spec = match boxed.value().as_str() {
Some(s) => s.to_string(),
None => {
return Err(Error::Custom(
"!include directive expects a scalar string spec".into(),
));
}
};
if !visited.insert(spec.clone()) {
return Err(Error::Custom(format!(
"!include cycle detected: `{spec}` already in resolution chain"
)));
}
let (path, fragment) = crate::include::split_fragment(&spec);
let req = crate::include::IncludeRequest {
spec: &spec,
from_id,
depth,
};
let source = resolver.resolve(req)?;
let id = *next_id;
*next_id += 1;
let mut included = parser::parse_exactly_one_value(&source.bytes, parse_config)?;
resolve_includes_recursive(
&mut included,
resolver,
parse_config,
max_depth,
depth + 1,
id,
visited,
next_id,
)?;
if let Some(frag) = fragment {
if let Some(map) = included.as_mapping() {
match map.get(frag) {
Some(v) => *value = v.clone(),
None => {
return Err(Error::Custom(format!(
"!include fragment `#{frag}` not found in `{path}`"
)));
}
}
} else {
return Err(Error::Custom(format!(
"!include fragment `#{frag}` requires a mapping-shaped \
included document; `{path}` is not a mapping"
)));
}
} else {
*value = included;
}
let _ = visited.remove(&spec);
} else {
resolve_includes_recursive(
boxed.value_mut(),
resolver,
parse_config,
max_depth,
depth,
from_id,
visited,
next_id,
)?;
}
}
Value::Sequence(seq) => {
for v in seq {
resolve_includes_recursive(
v,
resolver,
parse_config,
max_depth,
depth,
from_id,
visited,
next_id,
)?;
}
}
Value::Mapping(map) => {
for v in map.values_mut() {
resolve_includes_recursive(
v,
resolver,
parse_config,
max_depth,
depth,
from_id,
visited,
next_id,
)?;
}
}
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
}
Ok(())
}
pub fn from_slice<T>(b: &[u8]) -> Result<T>
where
T: for<'de> serde_core::Deserialize<'de> + 'static,
{
let s = core::str::from_utf8(b).map_err(|e| Error::Deserialize(e.to_string()))?;
from_str(s)
}
pub fn from_slice_with_config<T>(b: &[u8], config: &ParserConfig) -> Result<T>
where
T: for<'de> serde_core::Deserialize<'de> + 'static,
{
let s = core::str::from_utf8(b).map_err(|e| Error::Deserialize(e.to_string()))?;
from_str_with_config(s, config)
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
pub fn from_reader<R, T>(reader: R) -> Result<T>
where
R: io::Read,
T: for<'de> serde_core::Deserialize<'de> + 'static,
{
from_reader_with_config(reader, &ParserConfig::default())
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
pub fn from_reader_with_config<R, T>(mut reader: R, config: &ParserConfig) -> Result<T>
where
R: io::Read,
T: for<'de> serde_core::Deserialize<'de> + 'static,
{
let mut s = String::new();
let _ = reader.read_to_string(&mut s).map_err(Error::Io)?;
from_str_with_config(&s, config)
}
pub fn from_value<T>(value: &Value) -> Result<T>
where
T: for<'de> serde_core::Deserialize<'de> + 'static,
{
if is_value_target::<T>() {
let cloned = value.clone();
let boxed: Box<dyn core::any::Any> = Box::new(cloned);
let downcast: Box<T> = boxed
.downcast::<T>()
.expect("is_value_target proved T == Value");
return Ok(*downcast);
}
T::deserialize(Deserializer::new(value))
}
#[cfg(all(test, feature = "std", feature = "figment"))]
mod figment_entry_tests {
use super::*;
#[derive(Debug, serde::Deserialize, PartialEq)]
struct Endpoint {
name: String,
port: u16,
}
#[test]
fn no_tag_preserve_takes_streaming_fast_path() {
let cfg = ParserConfig::default();
let got: Endpoint = from_str_typed_no_tag_preserve("name: alpha\nport: 7\n", &cfg).unwrap();
assert_eq!(
got,
Endpoint {
name: "alpha".into(),
port: 7
}
);
}
#[test]
fn no_tag_preserve_falls_back_to_ast_on_merge_key() {
let cfg = ParserConfig::default();
let yaml = "\
_defaults: &d\n name: beta\n port: 9\n<<: *d\n";
let got: Endpoint = from_str_typed_no_tag_preserve(yaml, &cfg).unwrap();
assert_eq!(
got,
Endpoint {
name: "beta".into(),
port: 9
}
);
}
#[test]
fn no_tag_preserve_surfaces_parse_error() {
let cfg = ParserConfig::default().max_document_length(4);
let res: Result<Endpoint> = from_str_typed_no_tag_preserve("name: alpha\n", &cfg);
assert!(matches!(res, Err(Error::Parse(_))), "got {res:?}");
}
#[test]
fn no_tag_preserve_runs_value_policies() {
#[derive(Debug)]
struct RejectAll;
impl crate::policy::Policy for RejectAll {
fn check_value(&self, _v: &Value) -> Result<()> {
Err(Error::Deserialize("rejected by policy".into()))
}
}
let cfg = ParserConfig::default().with_policy(RejectAll);
let res: Result<Endpoint> = from_str_typed_no_tag_preserve("name: alpha\nport: 7\n", &cfg);
assert!(
matches!(res, Err(Error::Deserialize(_))),
"policy rejection must surface: {res:?}"
);
}
}