diff --git a/src/error.rs b/src/error.rs index d217570..8995395 100644 --- a/src/error.rs +++ b/src/error.rs @@ -28,6 +28,13 @@ pub enum KdlErrorKind { Other, } +#[derive(Debug, Clone, Eq, PartialEq, Error)] +#[error("Failed to convert from KdlNodeValue::{variant} to {expected}.")] +pub struct TryFromKdlNodeValueError { + pub(crate) expected: &'static str, + pub(crate) variant: &'static str, +} + #[derive(Debug, Clone, Eq, PartialEq)] pub(crate) struct KdlParseError { pub(crate) input: I, diff --git a/src/lib.rs b/src/lib.rs index 83952fb..4d8dfd8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ use nom::combinator::all_consuming; use nom::Finish; -pub use crate::error::{KdlError, KdlErrorKind}; +pub use crate::error::{KdlError, KdlErrorKind, TryFromKdlNodeValueError}; pub use crate::node::{KdlNode, KdlNodeValue}; mod error; diff --git a/src/node.rs b/src/node.rs index 72860e7..6dfdf2a 100644 --- a/src/node.rs +++ b/src/node.rs @@ -1,4 +1,6 @@ -use std::collections::HashMap; +use std::{collections::HashMap, convert::TryFrom}; + +use crate::TryFromKdlNodeValueError; #[derive(Debug, Clone, PartialEq)] pub struct KdlNode { @@ -16,3 +18,161 @@ pub enum KdlNodeValue { Boolean(bool), Null, } + +// Support conversions from base types into KdlNodeValue + +impl From for KdlNodeValue { + fn from(v: i64) -> Self { + Self::Int(v) + } +} + +impl From for KdlNodeValue { + fn from(v: f64) -> Self { + Self::Float(v) + } +} + +impl From for KdlNodeValue { + fn from(v: String) -> Self { + Self::String(v) + } +} + +impl From<&str> for KdlNodeValue { + fn from(v: &str) -> Self { + Self::String(v.to_owned()) + } +} + +impl From for KdlNodeValue { + fn from(v: bool) -> Self { + Self::Boolean(v) + } +} + +impl From> for KdlNodeValue +where + T: Into, +{ + fn from(v: Option) -> Self { + v.map_or(KdlNodeValue::Null, |v| v.into()) + } +} + +// Support reverse conversions using TryFrom + +// Synthesizes a TryFrom impl for both the base type and an Option variant. +// +// We need the Option variant because we can't write a blanket impl due to the existing +// impl TryFrom for T where U: Into +// even though KdlNodeValue does not implement Into>. +macro_rules! impl_try_from { + (<$($lt:lifetime)?> $source:ty => $typ:ty, $($good:pat => $value:expr),+; $($bad:ident),+) => { + impl<$($lt)?> TryFrom<$source> for $typ { + type Error = TryFromKdlNodeValueError; + fn try_from(value: $source) -> Result { + match value { + $( $good => Ok($value), )+ + $( KdlNodeValue::$bad(_) => Err(TryFromKdlNodeValueError { + expected: stringify!($typ), + variant: stringify!($bad) + }), )+ + KdlNodeValue::Null => Err(TryFromKdlNodeValueError { + expected: stringify!($typ), + variant: "Null" + }), + } + } + } + impl<$($lt)?> TryFrom<$source> for Option<$typ> { + type Error = TryFromKdlNodeValueError; + fn try_from(value: $source) -> Result { + match value { + $( $good => Ok(Some($value)), )+ + $( KdlNodeValue::$bad(_) => Err(TryFromKdlNodeValueError { + expected: concat!("Option::<", stringify!($typ), ">"), + variant: stringify!($bad) + }), )+ + KdlNodeValue::Null => Ok(None), + } + } + } + }; + (& $($lt:lifetime)?, $typ:ty, $($tt:tt)*) => { + impl_try_from!(<$($lt)?> & $($lt)? KdlNodeValue => $typ, $($tt)*); + }; + ($typ:ty, $($tt:tt)*) => { + impl_try_from!(<> KdlNodeValue => $typ, $($tt)*); + }; +} + +impl_try_from!(i64, KdlNodeValue::Int(v) => v; Float, String, Boolean); +impl_try_from!(&, i64, KdlNodeValue::Int(v) => *v; Float, String, Boolean); +impl_try_from!(f64, KdlNodeValue::Float(v) => v; Int, String, Boolean); +impl_try_from!(&, f64, KdlNodeValue::Float(v) => *v; Int, String, Boolean); +impl_try_from!(String, KdlNodeValue::String(v) => v; Int, Float, Boolean); +impl_try_from!(&'a, &'a str, KdlNodeValue::String(v) => &v[..]; Int, Float, Boolean); +impl_try_from!(bool, KdlNodeValue::Boolean(v) => v; Int, Float, String); +impl_try_from!(&, bool, KdlNodeValue::Boolean(v) => *v; Int, Float, String); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from() { + assert_eq!(KdlNodeValue::from(1), KdlNodeValue::Int(1)); + assert_eq!(KdlNodeValue::from(1.5), KdlNodeValue::Float(1.5)); + assert_eq!( + KdlNodeValue::from("foo".to_owned()), + KdlNodeValue::String("foo".to_owned()) + ); + assert_eq!( + KdlNodeValue::from("bar"), + KdlNodeValue::String("bar".to_owned()) + ); + assert_eq!(KdlNodeValue::from(true), KdlNodeValue::Boolean(true)); + + assert_eq!(KdlNodeValue::from(None::), KdlNodeValue::Null); + assert_eq!(KdlNodeValue::from(Some(1)), KdlNodeValue::Int(1)); + } + + #[test] + fn try_from_success() { + assert_eq!(i64::try_from(KdlNodeValue::Int(1)), Ok(1)); + assert_eq!(i64::try_from(&KdlNodeValue::Int(1)), Ok(1)); + assert_eq!(f64::try_from(KdlNodeValue::Float(1.5)), Ok(1.5)); + assert_eq!(f64::try_from(&KdlNodeValue::Float(1.5)), Ok(1.5)); + assert_eq!( + String::try_from(KdlNodeValue::String("foo".to_owned())), + Ok("foo".to_owned()) + ); + assert_eq!( + <&str as TryFrom<_>>::try_from(&KdlNodeValue::String("foo".to_owned())), + Ok("foo") + ); + assert_eq!(bool::try_from(KdlNodeValue::Boolean(true)), Ok(true)); + assert_eq!(bool::try_from(&KdlNodeValue::Boolean(true)), Ok(true)); + + assert_eq!(Option::::try_from(KdlNodeValue::Int(1)), Ok(Some(1))); + assert_eq!(Option::::try_from(KdlNodeValue::Null), Ok(None)); + } + + #[test] + fn try_from_failure() { + // We don't expose the internal format of the error type, so let's just test the message + // for a couple of cases. + assert_eq!( + format!("{}", i64::try_from(KdlNodeValue::Float(1.5)).unwrap_err()), + "Failed to convert from KdlNodeValue::Float to i64." + ); + assert_eq!( + format!( + "{}", + Option::::try_from(KdlNodeValue::Float(1.5)).unwrap_err() + ), + "Failed to convert from KdlNodeValue::Float to Option::." + ); + } +}