mod error;
mod resolve;
pub use error::{Expected, LocatedError, YamlError, YamlErrorKind};
#[cfg(feature = "test")]
pub use test_util::*;
use indexmap::{IndexMap, IndexSet};
use saphyr::{
AnnotatedMapping, AnnotatedNode, LoadableYamlNode, MarkedYaml, Marker,
Scalar, YamlData,
};
use std::{
fs,
hash::{Hash, Hasher},
path::{Path, PathBuf},
};
pub type Result<T> = std::result::Result<T, LocatedError<YamlErrorKind>>;
pub fn deserialize_file<T>(path: &Path) -> std::result::Result<T, YamlError>
where
T: DeserializeYaml,
{
let mut source_map = SourceMap::default();
SourcedYaml::load(path, source_map.add_source(path.to_owned()))
.and_then(|yaml| {
yaml.resolve_references(&mut source_map).map_err(|error| {
LocatedError {
error: YamlErrorKind::Reference(error.error),
location: error.location,
}
})
})
.and_then(|yaml| T::deserialize(yaml, &source_map))
.map_err(|error| error.resolve(&source_map))
}
pub fn deserialize_str<T>(yaml: &str) -> std::result::Result<T, YamlError>
where
T: DeserializeYaml,
{
deserialize(
SourcedYaml::load_from_str(yaml, SourceId::Memory),
SourceMap::default(),
)
}
fn deserialize<T>(
parse_result: Result<SourcedYaml>,
mut source_map: SourceMap,
) -> std::result::Result<T, YamlError>
where
T: DeserializeYaml,
{
parse_result
.and_then(|yaml| {
yaml.resolve_references(&mut source_map).map_err(|error| {
LocatedError {
error: YamlErrorKind::Reference(error.error),
location: error.location,
}
})
})
.and_then(|yaml| T::deserialize(yaml, &source_map))
.map_err(|error| error.resolve(&source_map))
}
pub trait DeserializeYaml: Sized {
fn expected() -> Expected;
fn deserialize(yaml: SourcedYaml, source_map: &SourceMap) -> Result<Self>;
}
#[macro_export]
macro_rules! impl_deserialize_from {
($t:ty, $u:ty) => {
impl DeserializeYaml for $t {
fn expected() -> Expected {
<$u as DeserializeYaml>::expected()
}
fn deserialize(
yaml: slumber_util::yaml::SourcedYaml,
source_map: &slumber_util::yaml::SourceMap,
) -> slumber_util::yaml::Result<Self> {
<$u as DeserializeYaml>::deserialize(yaml, source_map)
.map(<$t>::from)
}
}
};
}
#[macro_export]
macro_rules! deserialize_enum {
($yaml:expr, $($tag:literal => $f:expr),* $(,)?) => {
use $crate::yaml::{LocatedError, YamlErrorKind};
const TYPE_FIELD: &str = "type";
const EXPECTED: Expected =
Expected::OneOf(&[$(&Expected::Literal($tag),)*]);
let location = $yaml.location;
let mut mapping = $yaml.try_into_mapping()?;
let kind_yaml = mapping
.remove(&SourcedYaml::value_from_str(TYPE_FIELD))
.ok_or(LocatedError {
error: YamlErrorKind::MissingField {
field: TYPE_FIELD,
expected: EXPECTED,
},
location,
})?;
let kind_location = kind_yaml.location;
let kind = kind_yaml.try_into_string()?;
let yaml = SourcedYaml {
data: YamlData::Mapping(mapping),
location,
};
match kind.as_str() {
$($tag => $f(yaml),)*
_ => Err(LocatedError {
error: YamlErrorKind::Unexpected {
expected: EXPECTED,
actual: format!("{kind:?}"),
},
location: kind_location,
}),
}
};
}
impl DeserializeYaml for bool {
fn expected() -> Expected {
Expected::Boolean
}
fn deserialize(yaml: SourcedYaml, _source_map: &SourceMap) -> Result<Self> {
yaml.try_into_bool()
}
}
impl DeserializeYaml for usize {
fn expected() -> Expected {
Expected::Number
}
fn deserialize(yaml: SourcedYaml, _source_map: &SourceMap) -> Result<Self> {
yaml.try_into_usize()
}
}
impl DeserializeYaml for String {
fn expected() -> Expected {
Expected::String
}
fn deserialize(yaml: SourcedYaml, _source_map: &SourceMap) -> Result<Self> {
yaml.try_into_string()
}
}
impl<T: DeserializeYaml> DeserializeYaml for Option<T> {
fn expected() -> Expected {
T::expected()
}
fn deserialize(yaml: SourcedYaml, source_map: &SourceMap) -> Result<Self> {
if yaml.data.is_null() {
Ok(None)
} else {
T::deserialize(yaml, source_map).map(Some)
}
}
}
impl<T> DeserializeYaml for Vec<T>
where
T: DeserializeYaml,
{
fn expected() -> Expected {
Expected::Sequence
}
fn deserialize(yaml: SourcedYaml, source_map: &SourceMap) -> Result<Self> {
let sequence = yaml.try_into_sequence()?;
sequence
.into_iter()
.map(|yaml| T::deserialize(yaml, source_map))
.collect()
}
}
impl<K, V> DeserializeYaml for IndexMap<K, V>
where
K: Eq + Hash + DeserializeYaml,
V: DeserializeYaml,
{
fn expected() -> Expected {
Expected::Mapping
}
fn deserialize(yaml: SourcedYaml, source_map: &SourceMap) -> Result<Self> {
yaml.try_into_mapping()?
.into_iter()
.map(|(k, v)| {
Ok((
K::deserialize(k, source_map)?,
V::deserialize(v, source_map)?,
))
})
.collect()
}
}
#[derive(Clone, Debug, Eq)]
pub struct SourcedYaml<'input> {
pub location: SourceIdLocation,
pub data: YamlData<'input, Self>,
}
impl<'input> SourcedYaml<'input> {
fn load(path: &Path, source: SourceId) -> Result<Self> {
let content =
fs::read_to_string(path).map_err(|error| LocatedError {
error: YamlErrorKind::Io {
error,
source: path.display().to_string(),
},
location: SourceIdLocation::default(),
})?;
Self::load_from_str(&content, source)
}
fn load_from_str(input: &str, source: SourceId) -> Result<Self> {
let mut documents = MarkedYaml::load_from_str(input)
.map_err(|error| LocatedError::scan(error, source))?;
let yaml = documents
.pop()
.unwrap_or(YamlData::Mapping(Default::default()).into());
let yaml = Self::from_marked_yaml(yaml, source);
Ok(yaml)
}
fn from_marked_yaml(yaml: MarkedYaml<'input>, source_id: SourceId) -> Self {
let location =
SourceIdLocation::from_marker(source_id, yaml.span.start);
let data = match yaml.data {
YamlData::Value(scalar) => YamlData::Value(scalar),
YamlData::Sequence(sequence) => YamlData::Sequence(
sequence
.into_iter()
.map(|item| Self::from_marked_yaml(item, source_id))
.collect(),
),
YamlData::Mapping(mapping) => YamlData::Mapping(
mapping
.into_iter()
.map(|(key, value)| {
(
Self::from_marked_yaml(key, source_id),
Self::from_marked_yaml(value, source_id),
)
})
.collect(),
),
YamlData::Tagged(tag, value) => YamlData::Tagged(
tag,
Box::new(Self::from_marked_yaml(*value, source_id)),
),
YamlData::Alias(alias) => YamlData::Alias(alias),
YamlData::BadValue => YamlData::BadValue,
YamlData::Representation(a, b, c) => {
YamlData::Representation(a, b, c)
}
};
Self { location, data }
}
pub fn try_into_bool(self) -> Result<bool> {
if let YamlData::Value(Scalar::Boolean(b)) = self.data {
Ok(b)
} else {
Err(LocatedError::unexpected(Expected::Boolean, self))
}
}
pub fn try_into_usize(self) -> Result<usize> {
if let YamlData::Value(Scalar::Integer(i)) = self.data {
i.try_into()
.map_err(|error| LocatedError::other(error, self.location))
} else {
Err(LocatedError::unexpected(Expected::Number, self))
}
}
pub fn try_into_string(self) -> Result<String> {
if let YamlData::Value(Scalar::String(s)) = self.data {
Ok(s.into_owned())
} else {
Err(LocatedError::unexpected(Expected::String, self))
}
}
pub fn try_into_sequence(self) -> Result<Vec<Self>> {
if let YamlData::Sequence(sequence) = self.data {
Ok(sequence)
} else {
Err(LocatedError::unexpected(Expected::Sequence, self))
}
}
pub fn try_into_mapping(self) -> Result<AnnotatedMapping<'input, Self>> {
if let YamlData::Mapping(mapping) = self.data {
if mapping.contains_key(&SourcedYaml::value_from_str("<<")) {
Err(LocatedError {
error: YamlErrorKind::UnsupportedMerge,
location: self.location,
})
} else {
Ok(mapping)
}
} else {
Err(LocatedError::unexpected(Expected::Mapping, self))
}
}
pub fn value_from_str(value: &'input str) -> Self {
Self {
data: YamlData::Value(Scalar::parse_from_cow(value.into())),
location: SourceIdLocation::default(),
}
}
fn value_from_string(value: String) -> Self {
Self {
data: YamlData::Value(Scalar::parse_from_cow(value.into())),
location: SourceIdLocation::default(),
}
}
pub fn drop_dot_fields(&mut self) {
if let YamlData::Mapping(mapping) = &mut self.data {
mapping.retain(|key, _| {
!key.data.as_str().is_some_and(|s| s.starts_with('.'))
});
}
}
}
impl<'a> From<YamlData<'a, SourcedYaml<'a>>> for SourcedYaml<'a> {
fn from(value: YamlData<'a, SourcedYaml<'a>>) -> Self {
Self {
data: value,
location: SourceIdLocation::default(),
}
}
}
impl<'b> PartialEq<SourcedYaml<'b>> for SourcedYaml<'_> {
fn eq(&self, other: &SourcedYaml<'b>) -> bool {
self.data == other.data
}
}
impl Hash for SourcedYaml<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.data.hash(state);
}
}
impl AnnotatedNode for SourcedYaml<'_> {
type HashKey<'a> = SourcedYaml<'a>;
fn parse_representation_recursive(&mut self) -> bool {
self.data.parse_representation_recursive()
}
}
#[derive(Debug, Default)]
pub struct SourceMap {
sources: IndexSet<PathBuf>,
}
impl SourceMap {
fn add_source(&mut self, path: PathBuf) -> SourceId {
assert!(
path.is_absolute(),
"Source path must be absolute but got {}",
path.display()
);
assert!(
!self.sources.contains(&path),
"Source {} already in map",
path.display()
);
let index = self.sources.len() as u8;
self.sources.insert(path);
SourceId::File(index)
}
fn get_path(&self, source_id: SourceId) -> Option<&Path> {
match source_id {
SourceId::File(index) => {
self.sources.get_index(index as usize).map(PathBuf::as_path)
}
SourceId::Memory => None,
}
}
fn get_source_id(&self, path: &Path) -> Option<SourceId> {
self.sources
.get_index_of(path)
.map(|index| SourceId::File(index as u8))
}
}
#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq)]
enum SourceId {
File(u8),
#[default]
Memory,
}
#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq)]
pub struct SourceIdLocation {
source: SourceId,
line: u32,
column: u32,
}
impl SourceIdLocation {
fn from_marker(source_id: SourceId, marker: Marker) -> Self {
Self {
source: source_id,
line: marker.line() as u32,
column: marker.col() as u32,
}
}
pub fn resolve(&self, source_map: &SourceMap) -> SourceLocation {
let source = source_map
.get_path(self.source)
.map(|path| path.display().to_string())
.unwrap_or_default();
SourceLocation {
source,
line: self.line,
column: self.column,
}
}
}
#[derive(Clone, Debug, Default, derive_more::Display)]
#[display("{source}:{line}:{column}")]
pub struct SourceLocation {
pub source: String,
pub line: u32,
pub column: u32,
}
#[cfg(feature = "test")]
impl PartialEq for SourceLocation {
fn eq(&self, _: &Self) -> bool {
true
}
}
pub struct StructDeserializer<'a> {
pub mapping: AnnotatedMapping<'a, SourcedYaml<'a>>,
pub location: SourceIdLocation,
}
impl<'a> StructDeserializer<'a> {
pub fn new(yaml: SourcedYaml<'a>) -> Result<Self> {
let location = yaml.location;
let mapping = yaml.try_into_mapping()?;
Ok(Self { mapping, location })
}
pub fn get<T: DeserializeYaml>(
&mut self,
field: Field<T>,
source_map: &SourceMap,
) -> Result<T> {
if let Some(value) = self
.mapping
.remove(&SourcedYaml::value_from_str(field.name))
{
T::deserialize(value, source_map)
} else if let Some(default) = field.default {
Ok(default)
} else {
Err(LocatedError {
error: YamlErrorKind::MissingField {
field: field.name,
expected: T::expected(),
},
location: self.location,
})
}
}
pub fn done(mut self) -> Result<()> {
if let Some((key, _)) = self.mapping.pop_front() {
let key_location = key.location;
let key = key.try_into_string()?;
Err(LocatedError {
error: YamlErrorKind::UnexpectedField(key),
location: key_location,
})
} else {
Ok(())
}
}
}
pub struct Field<T> {
name: &'static str,
default: Option<T>,
}
impl<T> Field<T> {
pub fn new(name: &'static str) -> Self {
Self {
name,
default: None,
}
}
#[must_use]
pub fn opt(mut self) -> Self
where
T: Default,
{
self.default = Some(T::default());
self
}
#[must_use]
pub fn or(mut self, value: T) -> Self {
self.default = Some(value);
self
}
}
#[track_caller]
pub fn yaml_parse_panic() -> ! {
unreachable!("Invalid or incomplete YAML data")
}
#[cfg(feature = "test")]
mod test_util {
use super::{DeserializeYaml, Result, SourcedYaml};
use crate::yaml::{SourceId, SourceMap};
use std::iter;
pub fn deserialize_yaml<T: DeserializeYaml>(
yaml: serde_yaml::Value,
) -> Result<T> {
let yaml_input = serde_yaml::to_string(&yaml).unwrap();
let yaml = SourcedYaml::load_from_str(&yaml_input, SourceId::Memory)?;
let source_map = SourceMap::default();
T::deserialize(yaml, &source_map)
}
pub fn yaml_mapping(
fields: impl IntoIterator<
Item = (&'static str, impl Into<serde_yaml::Value>),
>,
) -> serde_yaml::Value {
fields
.into_iter()
.map(|(k, v)| (serde_yaml::Value::from(k), v.into()))
.collect::<serde_yaml::Mapping>()
.into()
}
pub fn yaml_enum(
type_: &'static str,
fields: impl IntoIterator<
Item = (&'static str, impl Into<serde_yaml::Value>),
>,
) -> serde_yaml::Value {
yaml_mapping(
iter::once(("type", serde_yaml::Value::from(type_)))
.chain(fields.into_iter().map(|(k, v)| (k, v.into()))),
)
}
}