diff options
author | Nadrieril | 2020-04-05 17:57:07 +0100 |
---|---|---|
committer | GitHub | 2020-04-05 17:57:07 +0100 |
commit | 7e977f282fb6a0eff0ef45738b9b5c98dc4c6fee (patch) | |
tree | ad4249609707fd8720a44469152105c2f6a67c79 /serde_dhall | |
parent | 5a5aa49e64197899006751db72e404f4b2292d4e (diff) | |
parent | 820214615547101f8f2b5de209b5189968bddfee (diff) |
Merge pull request #154 from Nadrieril/cleanup-api
Rewrite serde_dhall API
Diffstat (limited to 'serde_dhall')
-rw-r--r-- | serde_dhall/Cargo.toml | 10 | ||||
-rw-r--r-- | serde_dhall/src/deserialize.rs | 143 | ||||
-rw-r--r-- | serde_dhall/src/error.rs | 40 | ||||
-rw-r--r-- | serde_dhall/src/lib.rs | 316 | ||||
-rw-r--r-- | serde_dhall/src/options.rs | 368 | ||||
-rw-r--r-- | serde_dhall/src/serde.rs | 144 | ||||
-rw-r--r-- | serde_dhall/src/static_type.rs | 119 | ||||
-rw-r--r-- | serde_dhall/src/value.rs | 287 | ||||
-rw-r--r-- | serde_dhall/tests/de.rs | 28 | ||||
-rw-r--r-- | serde_dhall/tests/traits.rs | 6 | ||||
-rw-r--r-- | serde_dhall/tests/version_numbers.rs | 17 |
11 files changed, 1061 insertions, 417 deletions
diff --git a/serde_dhall/Cargo.toml b/serde_dhall/Cargo.toml index b0030e9..6af883a 100644 --- a/serde_dhall/Cargo.toml +++ b/serde_dhall/Cargo.toml @@ -11,5 +11,11 @@ edition = "2018" [dependencies] serde = { version = "1.0", features = ["derive"] } -dhall = { version = "0.4.0", path = "../dhall" } -dhall_proc_macros = { version = "0.4.0", path = "../dhall_proc_macros" } +dhall = { version = "=0.4.0", path = "../dhall" } +dhall_proc_macros = { version = "=0.4.0", path = "../dhall_proc_macros" } +doc-comment = "0.3" +reqwest = { version = "0.10", features = ["blocking"] } +url = "2.1" + +[dev-dependencies] +version-sync = "0.8" diff --git a/serde_dhall/src/deserialize.rs b/serde_dhall/src/deserialize.rs new file mode 100644 index 0000000..92be2e9 --- /dev/null +++ b/serde_dhall/src/deserialize.rs @@ -0,0 +1,143 @@ +use serde::de::value::{ + MapAccessDeserializer, MapDeserializer, SeqDeserializer, +}; +use std::borrow::Cow; + +use dhall::syntax::NumKind; + +use crate::value::SimpleValue; +use crate::{Error, ErrorKind, Result, Value}; + +pub trait Sealed {} + +/// A data structure that can be deserialized from a Dhall expression. +/// +/// This is automatically implemented for any type that [serde] can deserialize. +/// In fact, this trait cannot be implemented manually. To implement it for your type, +/// use serde's derive mechanism. +/// +/// # Example +/// +/// ```rust +/// # fn main() -> serde_dhall::Result<()> { +/// use serde::Deserialize; +/// +/// // Use serde's derive +/// #[derive(Deserialize)] +/// struct Point { +/// x: u64, +/// y: u64, +/// } +/// +/// // Convert a Dhall string to a Point. +/// let point: Point = serde_dhall::from_str("{ x = 1, y = 1 + 1 }").parse()?; +/// # Ok(()) +/// # } +/// ``` +/// +/// [serde]: https://serde.rs +pub trait FromDhall: Sealed + Sized { + #[doc(hidden)] + fn from_dhall(v: &Value) -> Result<Self>; +} + +impl<T> Sealed for T where T: serde::de::DeserializeOwned {} + +struct Deserializer<'a>(Cow<'a, SimpleValue>); + +impl<T> FromDhall for T +where + T: serde::de::DeserializeOwned, +{ + fn from_dhall(v: &Value) -> Result<Self> { + let sval = v.to_simple_value().ok_or_else(|| { + Error(ErrorKind::Deserialize(format!( + "this cannot be deserialized into the serde data model: {}", + v + ))) + })?; + T::deserialize(Deserializer(Cow::Owned(sval))) + } +} + +impl<'de: 'a, 'a> serde::de::IntoDeserializer<'de, Error> for Deserializer<'a> { + type Deserializer = Deserializer<'a>; + fn into_deserializer(self) -> Self::Deserializer { + self + } +} + +impl<'de: 'a, 'a> serde::Deserializer<'de> for Deserializer<'a> { + type Error = Error; + + fn deserialize_any<V>(self, visitor: V) -> Result<V::Value> + where + V: serde::de::Visitor<'de>, + { + use std::convert::TryInto; + use NumKind::*; + use SimpleValue::*; + + let val = |x| Deserializer(Cow::Borrowed(x)); + match self.0.as_ref() { + Num(Bool(x)) => visitor.visit_bool(*x), + Num(Natural(x)) => { + if let Ok(x64) = (*x).try_into() { + visitor.visit_u64(x64) + } else if let Ok(x32) = (*x).try_into() { + visitor.visit_u32(x32) + } else { + unimplemented!() + } + } + Num(Integer(x)) => { + if let Ok(x64) = (*x).try_into() { + visitor.visit_i64(x64) + } else if let Ok(x32) = (*x).try_into() { + visitor.visit_i32(x32) + } else { + unimplemented!() + } + } + Num(Double(x)) => visitor.visit_f64((*x).into()), + Text(x) => visitor.visit_str(x), + List(xs) => { + visitor.visit_seq(SeqDeserializer::new(xs.iter().map(val))) + } + Optional(None) => visitor.visit_none(), + Optional(Some(x)) => visitor.visit_some(val(x)), + Record(m) => visitor.visit_map(MapDeserializer::new( + m.iter().map(|(k, v)| (k.as_ref(), val(v))), + )), + Union(field_name, Some(x)) => visitor.visit_enum( + MapAccessDeserializer::new(MapDeserializer::new( + Some((field_name.as_str(), val(x))).into_iter(), + )), + ), + Union(field_name, None) => visitor.visit_enum( + MapAccessDeserializer::new(MapDeserializer::new( + Some((field_name.as_str(), ())).into_iter(), + )), + ), + } + } + + fn deserialize_tuple<V>(self, _: usize, visitor: V) -> Result<V::Value> + where + V: serde::de::Visitor<'de>, + { + let val = |x| Deserializer(Cow::Borrowed(x)); + match self.0.as_ref() { + // Blindly takes keys in sorted order. + SimpleValue::Record(m) => visitor + .visit_seq(SeqDeserializer::new(m.iter().map(|(_, v)| val(v)))), + _ => self.deserialize_any(visitor), + } + } + + serde::forward_to_deserialize_any! { + bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string + bytes byte_buf option unit unit_struct newtype_struct seq + tuple_struct map struct enum identifier ignored_any + } +} diff --git a/serde_dhall/src/error.rs b/serde_dhall/src/error.rs new file mode 100644 index 0000000..896e8b9 --- /dev/null +++ b/serde_dhall/src/error.rs @@ -0,0 +1,40 @@ +use dhall::error::Error as DhallError; + +/// Alias for a `Result` with the error type `serde_dhall::Error`. +pub type Result<T> = std::result::Result<T, Error>; + +/// Errors that can occur when deserializing Dhall data. +#[derive(Debug)] +pub struct Error(pub(crate) ErrorKind); + +#[derive(Debug)] +pub(crate) enum ErrorKind { + Dhall(DhallError), + Deserialize(String), +} + +impl From<ErrorKind> for Error { + fn from(kind: ErrorKind) -> Error { + Error(kind) + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match &self.0 { + ErrorKind::Dhall(err) => write!(f, "{}", err), + ErrorKind::Deserialize(err) => write!(f, "{}", err), + } + } +} + +impl std::error::Error for Error {} + +impl serde::de::Error for Error { + fn custom<T>(msg: T) -> Self + where + T: std::fmt::Display, + { + ErrorKind::Deserialize(msg.to_string()).into() + } +} diff --git a/serde_dhall/src/lib.rs b/serde_dhall/src/lib.rs index 0a53420..c478b2a 100644 --- a/serde_dhall/src/lib.rs +++ b/serde_dhall/src/lib.rs @@ -1,4 +1,5 @@ #![doc(html_root_url = "https://docs.rs/serde_dhall/0.4.0")] +#![warn(missing_docs, missing_doc_code_examples)] //! [Dhall][dhall] is a programmable configuration language that provides a non-repetitive //! alternative to JSON and YAML. //! @@ -16,20 +17,20 @@ //! //! # Basic usage //! -//! The main entrypoint of this library is the [`from_str`][from_str] function. It reads a string +//! The main entrypoint of this library is the [`from_str`](fn.from_str.html) function. It reads a string //! containing a Dhall expression and deserializes it into any serde-compatible type. //! //! This could mean a common Rust type like `HashMap`: //! //! ```rust -//! # fn main() -> serde_dhall::de::Result<()> { +//! # fn main() -> serde_dhall::Result<()> { //! use std::collections::HashMap; //! //! // Some Dhall data //! let data = "{ x = 1, y = 1 + 1 } : { x: Natural, y: Natural }"; //! //! // Deserialize it to a Rust type. -//! let deserialized_map: HashMap<String, usize> = serde_dhall::from_str(data)?; +//! let deserialized_map: HashMap<String, usize> = serde_dhall::from_str(data).parse()?; //! //! let mut expected_map = HashMap::new(); //! expected_map.insert("x".to_string(), 1); @@ -43,10 +44,10 @@ //! or a custom datatype, using serde's `derive` mechanism: //! //! ```rust -//! # fn main() -> serde_dhall::de::Result<()> { +//! # fn main() -> serde_dhall::Result<()> { //! use serde::Deserialize; //! -//! #[derive(Debug, Deserialize)] +//! #[derive(Deserialize)] //! struct Point { //! x: u64, //! y: u64, @@ -56,7 +57,7 @@ //! let data = "{ x = 1, y = 1 + 1 } : { x: Natural, y: Natural }"; //! //! // Convert the Dhall string to a Point. -//! let point: Point = serde_dhall::from_str(data)?; +//! let point: Point = serde_dhall::from_str(data).parse()?; //! assert_eq!(point.x, 1); //! assert_eq!(point.y, 2); //! @@ -64,67 +65,82 @@ //! # } //! ``` //! -//! # Type correspondence -//! -//! The following Dhall types correspond to the following Rust types: -//! -//! Dhall | Rust -//! -------|------ -//! `Bool` | `bool` -//! `Natural` | `u64`, `u32`, ... -//! `Integer` | `i64`, `i32`, ... -//! `Double` | `f64`, `f32`, ... -//! `Text` | `String` -//! `List T` | `Vec<T>` -//! `Optional T` | `Option<T>` -//! `{ x: T, y: U }` | structs -//! `{ _1: T, _2: U }` | `(T, U)`, structs -//! `{ x: T, y: T }` | `HashMap<String, T>`, structs -//! `< x: T \| y: U >` | enums -//! `T -> U` | unsupported -//! `Prelude.JSON.Type` | unsupported -//! `Prelude.Map.Type T U` | unsupported -//! -//! //! # Replacing `serde_json` or `serde_yaml` //! -//! If you used to consume JSON or YAML, you only need to replace [serde_json::from_str] or -//! [serde_yaml::from_str] with [serde_dhall::from_str][from_str]. +//! If you used to consume JSON or YAML, you only need to replace [`serde_json::from_str`] or +//! [`serde_yaml::from_str`] with [`serde_dhall::from_str(…).parse()`](fn.from_str.html). //! -//! [serde_json::from_str]: https://docs.serde.rs/serde_json/de/fn.from_str.html -//! [serde_yaml::from_str]: https://docs.serde.rs/serde_yaml/fn.from_str.html +//! [`serde_json::from_str`]: https://docs.serde.rs/serde_json/fn.from_str.html +//! [`serde_yaml::from_str`]: https://docs.serde.rs/serde_yaml/fn.from_str.html //! //! //! # Additional Dhall typechecking //! //! When deserializing, normal type checking is done to ensure that the returned value is a valid -//! Dhall value, and that it can be deserialized into the required Rust type. However types are -//! first-class in Dhall, and this library allows you to additionally check that some input data +//! Dhall value. However types are +//! first-class in Dhall, and this library allows you to additionally check that the input data //! matches a given Dhall type. That way, a type error will be caught on the Dhall side, and have //! pretty and explicit errors that point to the source file. //! -//! There are two ways to typecheck a Dhall value: you can provide the type as Dhall text or you -//! can let Rust infer it for you. +//! There are two ways to typecheck a Dhall value in this way: you can provide the type manually or +//! you can let Rust infer it for you. //! -//! To provide a type written in Dhall, first parse it into a [`serde_dhall::Value`][Value], then -//! pass it to [`from_str_check_type`][from_str_check_type]. +//! To let Rust infer the appropriate Dhall type, use the [StaticType](trait.StaticType.html) +//! trait. //! //! ```rust -//! # fn main() -> serde_dhall::de::Result<()> { -//! use serde_dhall::Value; +//! # fn main() -> serde_dhall::Result<()> { +//! use serde::Deserialize; +//! use serde_dhall::StaticType; +//! +//! #[derive(Deserialize, StaticType)] +//! struct Point { +//! x: u64, +//! y: u64, +//! } +//! +//! // Some Dhall data +//! let data = "{ x = 1, y = 1 + 1 }"; +//! +//! // Convert the Dhall string to a Point. +//! let point = serde_dhall::from_str(data) +//! .static_type_annotation() +//! .parse::<Point>()?; +//! assert_eq!(point.x, 1); +//! assert_eq!(point.y, 2); +//! +//! // Invalid data fails the type validation +//! let invalid_data = "{ x = 1, z = 0.3 }"; +//! assert!( +//! serde_dhall::from_str(invalid_data) +//! .static_type_annotation() +//! .parse::<Point>() +//! .is_err() +//! ); +//! # Ok(()) +//! # } +//! ``` +//! +//! To provide a type manually, you need a [`SimpleType`](enum.SimpleType.html) value. You +//! can parse it from some Dhall text like you would parse any other value. +//! +//! ```rust +//! # fn main() -> serde_dhall::Result<()> { +//! use serde_dhall::SimpleType; //! use std::collections::HashMap; //! //! // Parse a Dhall type //! let point_type_str = "{ x: Natural, y: Natural }"; -//! let point_type: Value = serde_dhall::from_str(point_type_str)?; +//! let point_type = serde_dhall::from_str(point_type_str).parse::<SimpleType>()?; //! //! // Some Dhall data //! let point_data = "{ x = 1, y = 1 + 1 }"; //! //! // Deserialize the data to a Rust type. This checks that //! // the data matches the provided type. -//! let deserialized_map: HashMap<String, usize> = -//! serde_dhall::from_str_check_type(point_data, &point_type)?; +//! let deserialized_map = serde_dhall::from_str(point_data) +//! .type_annotation(&point_type) +//! .parse::<HashMap<String, usize>>()?; //! //! let mut expected_map = HashMap::new(); //! expected_map.insert("x".to_string(), 1); @@ -135,207 +151,35 @@ //! # } //! ``` //! -//! You can also let Rust infer the appropriate Dhall type, using the [StaticType] trait. -//! -//! ```rust -//! # fn main() -> serde_dhall::de::Result<()> { -//! use serde::Deserialize; -//! use serde_dhall::StaticType; +//! # Controlling deserialization //! -//! #[derive(Debug, Deserialize, StaticType)] -//! struct Point { -//! x: u64, -//! y: u64, -//! } -//! -//! // Some Dhall data -//! let data = "{ x = 1, y = 1 + 1 }"; -//! -//! // Convert the Dhall string to a Point. -//! let point: Point = serde_dhall::from_str_auto_type(data)?; -//! assert_eq!(point.x, 1); -//! assert_eq!(point.y, 2); -//! -//! // Invalid data fails the type validation -//! let invalid_data = "{ x = 1, z = 0.3 }"; -//! assert!(serde_dhall::from_str_auto_type::<Point>(invalid_data).is_err()); -//! # Ok(()) -//! # } -//! ``` +//! If you need more control over the process of reading Dhall values, e.g. disabling +//! imports, see the [`Deserializer`] methods. //! +//! [`Deserializer`]: struct.Deserializer.html //! [dhall]: https://dhall-lang.org/ //! [serde]: https://docs.serde.rs/serde/ //! [serde::Deserialize]: https://docs.serde.rs/serde/trait.Deserialize.html -mod serde; +#[cfg(doctest)] +mod test_readme { + doc_comment::doctest!("../../README.md"); +} + +mod deserialize; +mod error; +mod options; mod static_type; +/// Dhall values +mod value; -#[doc(inline)] -pub use de::{from_str, from_str_auto_type, from_str_check_type}; #[doc(hidden)] pub use dhall_proc_macros::StaticType; -pub use static_type::StaticType; -#[doc(inline)] -pub use value::Value; - -// A Dhall value. -#[doc(hidden)] -pub mod value { - use dhall::syntax::Builtin; - use dhall::{Normalized, NormalizedExpr, Parsed}; - - use super::de::{Error, Result}; - - /// A Dhall value - #[derive(Debug, Clone, PartialEq, Eq)] - pub struct Value(Normalized); - impl Value { - pub fn from_str(s: &str, ty: Option<&Value>) -> Result<Self> { - Value::from_str_using_dhall_error_type(s, ty).map_err(Error::Dhall) - } - fn from_str_using_dhall_error_type( - s: &str, - ty: Option<&Value>, - ) -> dhall::error::Result<Self> { - let resolved = Parsed::parse_str(s)?.resolve()?; - let typed = match ty { - None => resolved.typecheck()?, - Some(t) => resolved.typecheck_with(t.as_normalized())?, - }; - Ok(Value(typed.normalize())) - } - pub(crate) fn to_expr(&self) -> NormalizedExpr { - self.0.to_expr() - } - pub(crate) fn as_normalized(&self) -> &Normalized { - &self.0 - } - - pub(crate) fn make_builtin_type(b: Builtin) -> Self { - Value(Normalized::make_builtin_type(b)) - } - pub(crate) fn make_optional_type(t: Value) -> Self { - Value(Normalized::make_optional_type(t.0)) - } - pub(crate) fn make_list_type(t: Value) -> Self { - Value(Normalized::make_list_type(t.0)) - } - // Made public for the StaticType derive macro - #[doc(hidden)] - pub fn make_record_type( - kts: impl Iterator<Item = (String, Value)>, - ) -> Self { - Value(Normalized::make_record_type(kts.map(|(k, t)| (k, t.0)))) - } - #[doc(hidden)] - pub fn make_union_type( - kts: impl Iterator<Item = (String, Option<Value>)>, - ) -> Self { - Value(Normalized::make_union_type( - kts.map(|(k, t)| (k, t.map(|t| t.0))), - )) - } - } - - impl super::de::sealed::Sealed for Value {} - - impl super::de::Deserialize for Value { - fn from_dhall(v: &Value) -> Result<Self> { - Ok(v.clone()) - } - } -} - -/// Deserialize Dhall data to a Rust data structure. -pub mod de { - use super::StaticType; - use super::Value; - pub use error::{Error, Result}; - - mod error { - use dhall::error::Error as DhallError; - - pub type Result<T> = std::result::Result<T, Error>; - - #[derive(Debug)] - #[non_exhaustive] - pub enum Error { - Dhall(DhallError), - Deserialize(String), - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Error::Dhall(err) => write!(f, "{}", err), - Error::Deserialize(err) => write!(f, "{}", err), - } - } - } - - impl std::error::Error for Error {} - - impl serde::de::Error for Error { - fn custom<T>(msg: T) -> Self - where - T: std::fmt::Display, - { - Error::Deserialize(msg.to_string()) - } - } - } - - pub(crate) mod sealed { - pub trait Sealed {} - } - - /// A data structure that can be deserialized from a Dhall expression - /// - /// This is automatically implemented for any type that [serde][serde] - /// can deserialize. - /// - /// This trait cannot be implemented manually. - pub trait Deserialize: sealed::Sealed + Sized { - /// See [serde_dhall::from_str][crate::from_str] - fn from_dhall(v: &Value) -> Result<Self>; - } - - /// Deserialize an instance of type `T` from a string of Dhall text. - /// - /// This will recursively resolve all imports in the expression, and - /// typecheck it before deserialization. Relative imports will be resolved relative to the - /// provided file. More control over this process is not yet available - /// but will be in a coming version of this crate. - pub fn from_str<T>(s: &str) -> Result<T> - where - T: Deserialize, - { - T::from_dhall(&Value::from_str(s, None)?) - } - - /// Deserialize an instance of type `T` from a string of Dhall text, - /// additionally checking that it matches the supplied type. - /// - /// Like [from_str], but this additionally checks that - /// the type of the provided expression matches the supplied type. - pub fn from_str_check_type<T>(s: &str, ty: &Value) -> Result<T> - where - T: Deserialize, - { - T::from_dhall(&Value::from_str(s, Some(ty))?) - } - - /// Deserialize an instance of type `T` from a string of Dhall text, - /// additionally checking that it matches the type of `T`. - /// - /// Like [from_str], but this additionally checks that - /// the type of the provided expression matches the output type `T`. The [StaticType] trait - /// captures Rust types that are valid Dhall types. - pub fn from_str_auto_type<T>(s: &str) -> Result<T> - where - T: Deserialize + StaticType, - { - from_str_check_type(s, &<T as StaticType>::static_type()) - } -} +pub use deserialize::FromDhall; +pub(crate) use deserialize::Sealed; +pub(crate) use error::ErrorKind; +pub use error::{Error, Result}; +pub use options::{from_file, from_str, Deserializer}; +pub use static_type::StaticType; +pub use value::{SimpleType, Value}; diff --git a/serde_dhall/src/options.rs b/serde_dhall/src/options.rs new file mode 100644 index 0000000..06a4368 --- /dev/null +++ b/serde_dhall/src/options.rs @@ -0,0 +1,368 @@ +use std::path::{Path, PathBuf}; + +use dhall::Parsed; + +use crate::SimpleType; +use crate::{Error, ErrorKind, FromDhall, Result, StaticType, Value}; + +#[derive(Debug, Clone)] +enum Source<'a> { + Str(&'a str), + File(PathBuf), + // Url(&'a str), +} + +#[derive(Debug, Clone, Copy)] +pub struct NoAnnot; +#[derive(Debug, Clone, Copy)] +pub struct ManualAnnot<'ty>(&'ty SimpleType); +#[derive(Debug, Clone, Copy)] +pub struct StaticAnnot; + +pub trait HasAnnot<A> { + fn get_annot(a: &A) -> Option<SimpleType>; +} +impl<T> HasAnnot<NoAnnot> for T { + fn get_annot(_: &NoAnnot) -> Option<SimpleType> { + None + } +} +impl<'ty, T> HasAnnot<ManualAnnot<'ty>> for T { + fn get_annot(a: &ManualAnnot<'ty>) -> Option<SimpleType> { + Some(a.0.clone()) + } +} +impl<T: StaticType> HasAnnot<StaticAnnot> for T { + fn get_annot(_: &StaticAnnot) -> Option<SimpleType> { + Some(T::static_type()) + } +} + +/// Controls how a Dhall value is read. +/// +/// This builder exposes the ability to configure how a value is deserialized and what operations +/// are permitted during evaluation. +/// +/// Generally speaking, when using [`Deserializer`], you'll create it with [`from_str`] or [`from_file`], then +/// chain calls to methods to set each option, then call [`parse`]. This will give you a +/// [`Result<T>`] where `T` is a deserializable type of your choice. +/// +/// [`Deserializer`]: struct.Deserializer.html +/// [`from_str`]: fn.from_str.html +/// [`from_file`]: fn.from_file.html +/// [`parse`]: struct.Deserializer.html#method.parse +/// [`Result<T>`]: type.Result.html +/// +/// # Examples +/// +/// Reading from a file: +/// +/// ```no_run +/// # fn main() -> serde_dhall::Result<()> { +/// use serde_dhall::from_file; +/// +/// let data = from_file("foo.dhall").parse::<u64>()?; +/// # Ok(()) +/// # } +/// ``` +/// +/// Reading from a file and checking the value against a provided type: +/// +/// ```no_run +/// # fn main() -> serde_dhall::Result<()> { +/// use std::collections::HashMap; +/// use serde_dhall::{from_file, from_str}; +/// +/// let ty = from_str("{ x: Natural, y: Natural }").parse()?; +/// let data = from_file("foo.dhall") +/// .type_annotation(&ty) +/// .parse::<HashMap<String, usize>>()?; +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Clone)] +pub struct Deserializer<'a, A> { + source: Source<'a>, + annot: A, + allow_imports: bool, + // allow_remote_imports: bool, + // use_cache: bool, +} + +impl<'a> Deserializer<'a, NoAnnot> { + fn default_with_source(source: Source<'a>) -> Self { + Deserializer { + source, + annot: NoAnnot, + allow_imports: true, + // allow_remote_imports: true, + // use_cache: true, + } + } + fn from_str(s: &'a str) -> Self { + Self::default_with_source(Source::Str(s)) + } + fn from_file<P: AsRef<Path>>(path: P) -> Self { + Self::default_with_source(Source::File(path.as_ref().to_owned())) + } + // fn from_url(url: &'a str) -> Self { + // Self::default_with_source(Source::Url(url)) + // } + + /// Ensures that the parsed value matches the provided type. + /// + /// In many cases the Dhall type that corresponds to a Rust type can be inferred automatically. + /// See the [`StaticType`] trait and the [`static_type_annotation`] method for that. + /// + /// # Example + /// + /// ``` + /// # fn main() -> serde_dhall::Result<()> { + /// use std::collections::HashMap; + /// use serde::Deserialize; + /// use serde_dhall::{from_str, SimpleType}; + /// + /// // Parse a Dhall type + /// let type_str = "{ x: Natural, y: Natural }"; + /// let ty = from_str(type_str).parse::<SimpleType>()?; + /// + /// // Parse some Dhall data. + /// let data = "{ x = 1, y = 1 + 1 }"; + /// let point = from_str(data) + /// .type_annotation(&ty) + /// .parse::<HashMap<String, usize>>()?; + /// assert_eq!(point.get("y"), Some(&2)); + /// + /// // Invalid data fails the type validation; deserialization would have succeeded otherwise. + /// let invalid_data = "{ x = 1, z = 3 }"; + /// assert!( + /// from_str(invalid_data) + /// .type_annotation(&ty) + /// .parse::<HashMap<String, usize>>() + /// .is_err() + /// ); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`static_type_annotation`]: struct.Deserializer.html#method.static_type_annotation + /// [`StaticType`]: trait.StaticType.html + pub fn type_annotation<'ty>( + self, + ty: &'ty SimpleType, + ) -> Deserializer<'a, ManualAnnot<'ty>> { + Deserializer { + annot: ManualAnnot(ty), + source: self.source, + allow_imports: self.allow_imports, + } + } + + /// Ensures that the parsed value matches the type of `T`. + /// + /// `T` must implement the [`StaticType`] trait. If it doesn't, you can use [`type_annotation`] + /// to provide a type manually. + /// + /// # Example + /// + /// ``` + /// # fn main() -> serde_dhall::Result<()> { + /// use serde::Deserialize; + /// use serde_dhall::StaticType; + /// + /// #[derive(Deserialize, StaticType)] + /// struct Point { + /// x: u64, + /// y: Option<u64>, + /// } + /// + /// // Some Dhall data + /// let data = "{ x = 1, y = Some (1 + 1) }"; + /// + /// // Convert the Dhall string to a Point. + /// let point = serde_dhall::from_str(data) + /// .static_type_annotation() + /// .parse::<Point>()?; + /// assert_eq!(point.x, 1); + /// assert_eq!(point.y, Some(2)); + /// + /// // Invalid data fails the type validation; deserialization would have succeeded otherwise. + /// let invalid_data = "{ x = 1 }"; + /// assert!( + /// serde_dhall::from_str(invalid_data) + /// .static_type_annotation() + /// .parse::<Point>() + /// .is_err() + /// ); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`type_annotation`]: struct.Deserializer.html#method.type_annotation + /// [`StaticType`]: trait.StaticType.html + pub fn static_type_annotation(self) -> Deserializer<'a, StaticAnnot> { + Deserializer { + annot: StaticAnnot, + source: self.source, + allow_imports: self.allow_imports, + } + } +} + +impl<'a, A> Deserializer<'a, A> { + /// Sets whether to enable imports. + /// + /// By default, imports are enabled. + /// + /// # Example + /// + /// ``` + /// # fn main() -> serde_dhall::Result<()> { + /// use serde::Deserialize; + /// use serde_dhall::SimpleType; + /// + /// let data = "12 + ./other_file.dhall : Natural"; + /// assert!( + /// serde_dhall::from_str(data) + /// .imports(false) + /// .parse::<u64>() + /// .is_err() + /// ); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`static_type_annotation`]: struct.Deserializer.html#method.static_type_annotation + /// [`StaticType`]: trait.StaticType.html + pub fn imports(self, imports: bool) -> Self { + Deserializer { + allow_imports: imports, + ..self + } + } + + // /// TODO + // pub fn remote_imports(&mut self, imports: bool) -> &mut Self { + // self.allow_remote_imports = imports; + // if imports { + // self.allow_imports = true; + // } + // self + // } + + fn _parse<T>(&self) -> dhall::error::Result<Value> + where + T: HasAnnot<A>, + { + let parsed = match &self.source { + Source::Str(s) => Parsed::parse_str(s)?, + Source::File(p) => Parsed::parse_file(p.as_ref())?, + }; + let resolved = if self.allow_imports { + parsed.resolve()? + } else { + parsed.skip_resolve()? + }; + let typed = match &T::get_annot(&self.annot) { + None => resolved.typecheck()?, + Some(ty) => resolved.typecheck_with(ty.to_value().as_hir())?, + }; + Ok(Value::from_nir(typed.normalize().as_nir())) + } + + /// Parses the chosen dhall value with the options provided. + /// + /// If you enabled static annotations, `T` is required to implement [`StaticType`]. + /// + /// + /// # Example + /// + /// ``` + /// # fn main() -> serde_dhall::Result<()> { + /// let data = serde_dhall::from_str("6 * 7").parse::<u64>()?; + /// assert_eq!(data, 42); + /// # Ok(()) + /// # } + /// ``` + /// [`StaticType`]: trait.StaticType.html + pub fn parse<T>(&self) -> Result<T> + where + T: FromDhall + HasAnnot<A>, + { + let val = self + ._parse::<T>() + .map_err(ErrorKind::Dhall) + .map_err(Error)?; + T::from_dhall(&val) + } +} + +/// Deserialize a value from a string of Dhall text. +/// +/// This returns a [`Deserializer`] object. Call the [`parse`] method to get the deserialized +/// value, or use other [`Deserializer`] methods to control the deserialization process. +/// +/// # Example +/// +/// ```rust +/// # fn main() -> serde_dhall::Result<()> { +/// use serde::Deserialize; +/// +/// // We use serde's derive feature +/// #[derive(Deserialize)] +/// struct Point { +/// x: u64, +/// y: u64, +/// } +/// +/// // Some Dhall data +/// let data = "{ x = 1, y = 1 + 1 } : { x: Natural, y: Natural }"; +/// +/// // Parse the Dhall string as a Point. +/// let point: Point = serde_dhall::from_str(data).parse()?; +/// +/// assert_eq!(point.x, 1); +/// assert_eq!(point.y, 2); +/// # Ok(()) +/// # } +/// ``` +/// +/// [`Deserializer`]: struct.Deserializer.html +/// [`parse`]: struct.Deserializer.html#method.parse +pub fn from_str(s: &str) -> Deserializer<'_, NoAnnot> { + Deserializer::from_str(s) +} + +/// Deserialize a value from a Dhall file. +/// +/// This returns a [`Deserializer`] object. Call the [`parse`] method to get the deserialized +/// value, or use other [`Deserializer`] methods to control the deserialization process. +/// +/// # Example +/// +/// ```no_run +/// # fn main() -> serde_dhall::Result<()> { +/// use serde::Deserialize; +/// +/// // We use serde's derive feature +/// #[derive(Deserialize)] +/// struct Point { +/// x: u64, +/// y: u64, +/// } +/// +/// // Parse the Dhall file as a Point. +/// let point: Point = serde_dhall::from_file("foo.dhall").parse()?; +/// # Ok(()) +/// # } +/// ``` +/// +/// [`Deserializer`]: struct.Deserializer.html +/// [`parse`]: struct.Deserializer.html#method.parse +pub fn from_file<'a, P: AsRef<Path>>(path: P) -> Deserializer<'a, NoAnnot> { + Deserializer::from_file(path) +} + +// pub fn from_url(url: &str) -> Deserializer<'_, NoAnnot> { +// Deserializer::from_url(url) +// } diff --git a/serde_dhall/src/serde.rs b/serde_dhall/src/serde.rs deleted file mode 100644 index 4fd7815..0000000 --- a/serde_dhall/src/serde.rs +++ /dev/null @@ -1,144 +0,0 @@ -use std::borrow::Cow; - -use serde::de::value::{ - MapAccessDeserializer, MapDeserializer, SeqDeserializer, -}; - -use dhall::syntax::{ExprKind, LitKind}; -use dhall::NormalizedExpr; - -use crate::de::{Deserialize, Error, Result}; -use crate::Value; - -impl<'a, T> crate::de::sealed::Sealed for T where T: serde::Deserialize<'a> {} - -impl<'a, T> Deserialize for T -where - T: serde::Deserialize<'a>, -{ - fn from_dhall(v: &Value) -> Result<Self> { - T::deserialize(Deserializer(Cow::Owned(v.to_expr()))) - } -} - -struct Deserializer<'a>(Cow<'a, NormalizedExpr>); - -impl<'de: 'a, 'a> serde::de::IntoDeserializer<'de, Error> for Deserializer<'a> { - type Deserializer = Deserializer<'a>; - fn into_deserializer(self) -> Self::Deserializer { - self - } -} - -impl<'de: 'a, 'a> serde::Deserializer<'de> for Deserializer<'a> { - type Error = Error; - - fn deserialize_any<V>(self, visitor: V) -> Result<V::Value> - where - V: serde::de::Visitor<'de>, - { - use std::convert::TryInto; - use ExprKind::*; - use LitKind::*; - let expr = self.0.as_ref(); - let not_serde_compatible = || { - Err(Error::Deserialize(format!( - "this cannot be deserialized into the serde data model: {}", - expr - ))) - }; - - match expr.kind() { - Lit(Bool(x)) => visitor.visit_bool(*x), - Lit(Natural(x)) => { - if let Ok(x64) = (*x).try_into() { - visitor.visit_u64(x64) - } else if let Ok(x32) = (*x).try_into() { - visitor.visit_u32(x32) - } else { - unimplemented!() - } - } - Lit(Integer(x)) => { - if let Ok(x64) = (*x).try_into() { - visitor.visit_i64(x64) - } else if let Ok(x32) = (*x).try_into() { - visitor.visit_i32(x32) - } else { - unimplemented!() - } - } - Lit(Double(x)) => visitor.visit_f64((*x).into()), - TextLit(x) => { - // Normal form ensures that the tail is empty. - assert!(x.tail().is_empty()); - visitor.visit_str(x.head()) - } - EmptyListLit(..) => { - visitor.visit_seq(SeqDeserializer::new(None::<()>.into_iter())) - } - NEListLit(xs) => visitor.visit_seq(SeqDeserializer::new( - xs.iter().map(|x| Deserializer(Cow::Borrowed(x))), - )), - SomeLit(x) => visitor.visit_some(Deserializer(Cow::Borrowed(x))), - App(f, x) => match f.kind() { - Builtin(dhall::syntax::Builtin::OptionalNone) => { - visitor.visit_none() - } - Field(y, name) => match y.kind() { - UnionType(..) => { - let name: String = name.into(); - visitor.visit_enum(MapAccessDeserializer::new( - MapDeserializer::new( - Some((name, Deserializer(Cow::Borrowed(x)))) - .into_iter(), - ), - )) - } - _ => not_serde_compatible(), - }, - _ => not_serde_compatible(), - }, - RecordLit(m) => visitor - .visit_map(MapDeserializer::new(m.iter().map(|(k, v)| { - (k.as_ref(), Deserializer(Cow::Borrowed(v))) - }))), - Field(y, name) => match y.kind() { - UnionType(..) => { - let name: String = name.into(); - visitor.visit_enum(MapAccessDeserializer::new( - MapDeserializer::new(Some((name, ())).into_iter()), - )) - } - _ => not_serde_compatible(), - }, - Const(..) | Var(..) | Lam(..) | Pi(..) | Let(..) | Annot(..) - | Assert(..) | Builtin(..) | BinOp(..) | BoolIf(..) - | RecordType(..) | UnionType(..) | Merge(..) | ToMap(..) - | Projection(..) | ProjectionByExpr(..) | Completion(..) - | Import(..) => not_serde_compatible(), - } - } - - fn deserialize_tuple<V>(self, _: usize, visitor: V) -> Result<V::Value> - where - V: serde::de::Visitor<'de>, - { - use ExprKind::*; - let expr = self.0.as_ref(); - - match expr.kind() { - // Blindly takes keys in sorted order. - RecordLit(m) => visitor.visit_seq(SeqDeserializer::new( - m.iter().map(|(_, v)| Deserializer(Cow::Borrowed(v))), - )), - _ => self.deserialize_any(visitor), - } - } - - serde::forward_to_deserialize_any! { - bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string - bytes byte_buf option unit unit_struct newtype_struct seq - tuple_struct map struct enum identifier ignored_any - } -} diff --git a/serde_dhall/src/static_type.rs b/serde_dhall/src/static_type.rs index 1eb9150..26c70cd 100644 --- a/serde_dhall/src/static_type.rs +++ b/serde_dhall/src/static_type.rs @@ -1,27 +1,92 @@ -use dhall::syntax::Builtin; - -use crate::Value; +use crate::SimpleType; /// A Rust type that can be represented as a Dhall type. /// -/// A typical example is `Option<bool>`, -/// represented by the dhall expression `Optional Bool`. +/// A typical example is `Option<bool>`, represented by the Dhall expression `Optional Bool`. +/// +/// This trait can be automatically derived, and this is the recommended way of implementing it. +/// +/// Some Rust types cannot implement this trait, because there isn't a single Dhall type that +/// corresponds to them. For example, `HashMap<String, u64>` could correspond to multiple different +/// Dhall types, e.g. `{ foo: Natural, bar: Natural }` and `{ baz: Natural }`. +/// +/// # Example +/// +/// ```rust +/// # fn main() -> serde_dhall::Result<()> { +/// use serde_dhall::{SimpleType, StaticType}; +/// +/// #[derive(StaticType)] +/// struct Foo { +/// x: bool, +/// y: Vec<u64>, +/// } +/// +/// let ty: SimpleType = +/// serde_dhall::from_str("{ x: Bool, y: List Natural }").parse()?; +/// +/// assert_eq!(Foo::static_type(), ty); +/// # Ok(()) +/// # } +/// ``` +/// +/// # Type correspondence /// -/// This trait can and should be automatically derived. +/// The following Dhall types correspond to the following Rust types: /// -/// The representation needs to be independent of the value. -/// For this reason, something like `HashMap<String, bool>` cannot implement -/// [StaticType] because each different value would -/// have a different Dhall record type. +/// Dhall | Rust +/// -------|------ +/// `Bool` | `bool` +/// `Natural` | `u64`, `u32`, ... +/// `Integer` | `i64`, `i32`, ... +/// `Double` | `f64`, `f32`, ... +/// `Text` | `String` +/// `List T` | `Vec<T>` +/// `Optional T` | `Option<T>` +/// `{ x: T, y: U }` | structs +/// `{ _1: T, _2: U }` | `(T, U)`, structs +/// `{ x: T, y: T }` | `HashMap<String, T>`, structs +/// `< x: T \| y: U >` | enums +/// `T -> U` | unsupported +/// `Prelude.JSON.Type` | unsupported +/// `Prelude.Map.Type T U` | unsupported pub trait StaticType { - fn static_type() -> Value; + /// Return the Dhall type that represents this type. + /// + /// # Example + /// + /// ```rust + /// # fn main() -> serde_dhall::Result<()> { + /// use serde::Deserialize; + /// use serde_dhall::{SimpleType, StaticType}; + /// + /// // Using `derive(StaticType)` here would give it the type `{ _1: List Natural }`. + /// #[derive(Deserialize)] + /// #[serde(transparent)] + /// struct Foo(Vec<u64>); + /// + /// impl StaticType for Foo { + /// fn static_type() -> SimpleType { + /// SimpleType::List(Box::new(SimpleType::Natural)) + /// } + /// } + /// + /// let foo = serde_dhall::from_str("[ 1, 2 ]") + /// .static_type_annotation() + /// .parse::<Foo>()?; + /// + /// assert_eq!(foo.0, vec![1, 2]); + /// # Ok(()) + /// # } + /// ``` + fn static_type() -> SimpleType; } macro_rules! derive_builtin { - ($ty:ty, $builtin:ident) => { - impl StaticType for $ty { - fn static_type() -> Value { - Value::make_builtin_type(Builtin::$builtin) + ($rust_ty:ty, $dhall_ty:ident) => { + impl StaticType for $rust_ty { + fn static_type() -> SimpleType { + SimpleType::$dhall_ty } } }; @@ -43,13 +108,14 @@ where A: StaticType, B: StaticType, { - fn static_type() -> Value { - Value::make_record_type( + fn static_type() -> SimpleType { + SimpleType::Record( vec![ ("_1".to_owned(), A::static_type()), ("_2".to_owned(), B::static_type()), ] - .into_iter(), + .into_iter() + .collect(), ) } } @@ -59,13 +125,14 @@ where T: StaticType, E: StaticType, { - fn static_type() -> Value { - Value::make_union_type( + fn static_type() -> SimpleType { + SimpleType::Union( vec![ ("Ok".to_owned(), Some(T::static_type())), ("Err".to_owned(), Some(E::static_type())), ] - .into_iter(), + .into_iter() + .collect(), ) } } @@ -74,8 +141,8 @@ impl<T> StaticType for Option<T> where T: StaticType, { - fn static_type() -> Value { - Value::make_optional_type(T::static_type()) + fn static_type() -> SimpleType { + SimpleType::Optional(Box::new(T::static_type())) } } @@ -83,8 +150,8 @@ impl<T> StaticType for Vec<T> where T: StaticType, { - fn static_type() -> Value { - Value::make_list_type(T::static_type()) + fn static_type() -> SimpleType { + SimpleType::List(Box::new(T::static_type())) } } @@ -92,7 +159,7 @@ impl<'a, T> StaticType for &'a T where T: StaticType, { - fn static_type() -> Value { + fn static_type() -> SimpleType { T::static_type() } } diff --git a/serde_dhall/src/value.rs b/serde_dhall/src/value.rs new file mode 100644 index 0000000..d6631da --- /dev/null +++ b/serde_dhall/src/value.rs @@ -0,0 +1,287 @@ +use std::collections::{BTreeMap, HashMap}; + +use dhall::semantics::{Hir, HirKind, Nir, NirKind}; +use dhall::syntax::{Builtin, Expr, ExprKind, NumKind, Span}; + +use crate::{Error, ErrorKind, FromDhall, Result, Sealed}; + +#[doc(hidden)] +/// An arbitrary Dhall value. +#[derive(Debug, Clone)] +pub struct Value { + /// Invariant: in normal form + hir: Hir, + /// Cached conversions because they are annoying to construct from Hir. + /// At most one of them will be `Some`. + as_simple_val: Option<SimpleValue>, + as_simple_ty: Option<SimpleType>, +} + +/// A simple value of the kind that can be decoded with serde +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum SimpleValue { + Num(NumKind), + Text(String), + Optional(Option<Box<SimpleValue>>), + List(Vec<SimpleValue>), + Record(BTreeMap<String, SimpleValue>), + Union(String, Option<Box<SimpleValue>>), +} + +/// The type of a value that can be decoded by `serde_dhall`, e.g. `{ x: Bool, y: List Natural }`. +/// +/// A `SimpleType` is used when deserializing values to ensure they are of the expected type. +/// Rather than letting `serde` handle potential type mismatches, this uses the type-checking +/// capabilities of Dhall to catch errors early and cleanly indicate in the user's code where the +/// mismatch happened. +/// +/// You would typically not manipulate `SimpleType`s by hand but rather let Rust infer it for your +/// datatype by deriving the [`StaticType`] trait, and using +/// [`Deserializer::static_type_annotation`]. If you need to supply a `SimpleType` manually, you +/// can either deserialize it like any other Dhall value, or construct it manually. +/// +/// [`StaticType`]: trait.StaticType.html +/// [`Deserializer::static_type_annotation`]: options/struct.Deserializer.html#method.static_type_annotation +/// +/// # Examples +/// +/// ```rust +/// # fn main() -> serde_dhall::Result<()> { +/// use serde_dhall::{SimpleType, StaticType}; +/// +/// #[derive(StaticType)] +/// struct Foo { +/// x: bool, +/// y: Vec<u64>, +/// } +/// +/// let ty: SimpleType = +/// serde_dhall::from_str("{ x: Bool, y: List Natural }").parse()?; +/// +/// assert_eq!(Foo::static_type(), ty); +/// # Ok(()) +/// # } +/// ``` +/// +/// ```rust +/// # fn main() -> serde_dhall::Result<()> { +/// use std::collections::HashMap; +/// use serde_dhall::SimpleType; +/// +/// let ty: SimpleType = +/// serde_dhall::from_str("{ x: Natural, y: Natural }").parse()?; +/// +/// let mut map = HashMap::new(); +/// map.insert("x".to_string(), SimpleType::Natural); +/// map.insert("y".to_string(), SimpleType::Natural); +/// assert_eq!(ty, SimpleType::Record(map)); +/// # Ok(()) +/// # } +/// ``` +/// +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SimpleType { + /// Corresponds to the Dhall type `Bool` + Bool, + /// Corresponds to the Dhall type `Natural` + Natural, + /// Corresponds to the Dhall type `Integer` + Integer, + /// Corresponds to the Dhall type `Double` + Double, + /// Corresponds to the Dhall type `Text` + Text, + /// Corresponds to the Dhall type `Optional T` + Optional(Box<SimpleType>), + /// Corresponds to the Dhall type `List T` + List(Box<SimpleType>), + /// Corresponds to the Dhall type `{ x : T, y : U }` + Record(HashMap<String, SimpleType>), + /// Corresponds to the Dhall type `< x : T | y : U >` + Union(HashMap<String, Option<SimpleType>>), +} + +impl Value { + pub(crate) fn from_nir(x: &Nir) -> Self { + Value { + hir: x.to_hir_noenv(), + as_simple_val: SimpleValue::from_nir(x), + as_simple_ty: SimpleType::from_nir(x), + } + } + + pub(crate) fn as_hir(&self) -> &Hir { + &self.hir + } + + /// Converts a Value into a SimpleValue. + pub(crate) fn to_simple_value(&self) -> Option<SimpleValue> { + self.as_simple_val.clone() + } + + /// Converts a Value into a SimpleType. + pub(crate) fn to_simple_type(&self) -> Option<SimpleType> { + self.as_simple_ty.clone() + } + + /// Converts a value back to the corresponding AST expression. + pub(crate) fn to_expr(&self) -> Expr { + self.hir.to_expr(Default::default()) + } +} + +impl SimpleValue { + pub(crate) fn from_nir(nir: &Nir) -> Option<Self> { + Some(match nir.kind() { + NirKind::Num(lit) => SimpleValue::Num(lit.clone()), + NirKind::TextLit(x) => SimpleValue::Text( + x.as_text() + .expect("Normal form should ensure the text is a string"), + ), + NirKind::EmptyOptionalLit(_) => SimpleValue::Optional(None), + NirKind::NEOptionalLit(x) => { + SimpleValue::Optional(Some(Box::new(Self::from_nir(x)?))) + } + NirKind::EmptyListLit(_) => SimpleValue::List(vec![]), + NirKind::NEListLit(xs) => SimpleValue::List( + xs.iter().map(Self::from_nir).collect::<Option<_>>()?, + ), + NirKind::RecordLit(kvs) => SimpleValue::Record( + kvs.iter() + .map(|(k, v)| Some((k.into(), Self::from_nir(v)?))) + .collect::<Option<_>>()?, + ), + NirKind::UnionLit(field, x, _) => SimpleValue::Union( + field.into(), + Some(Box::new(Self::from_nir(x)?)), + ), + NirKind::UnionConstructor(field, ty) + if ty.get(field).map(|f| f.is_some()) == Some(false) => + { + SimpleValue::Union(field.into(), None) + } + _ => return None, + }) + } +} + +impl SimpleType { + pub(crate) fn from_nir(nir: &Nir) -> Option<Self> { + Some(match nir.kind() { + NirKind::BuiltinType(b) => match b { + Builtin::Bool => SimpleType::Bool, + Builtin::Natural => SimpleType::Natural, + Builtin::Integer => SimpleType::Integer, + Builtin::Double => SimpleType::Double, + Builtin::Text => SimpleType::Text, + _ => unreachable!(), + }, + NirKind::OptionalType(t) => { + SimpleType::Optional(Box::new(Self::from_nir(t)?)) + } + NirKind::ListType(t) => { + SimpleType::List(Box::new(Self::from_nir(t)?)) + } + NirKind::RecordType(kts) => SimpleType::Record( + kts.iter() + .map(|(k, v)| Some((k.into(), Self::from_nir(v)?))) + .collect::<Option<_>>()?, + ), + NirKind::UnionType(kts) => SimpleType::Union( + kts.iter() + .map(|(k, v)| { + Some(( + k.into(), + v.as_ref() + .map(|v| Ok(Self::from_nir(v)?)) + .transpose()?, + )) + }) + .collect::<Option<_>>()?, + ), + _ => return None, + }) + } + + pub(crate) fn to_value(&self) -> Value { + Value { + hir: self.to_hir(), + as_simple_val: None, + as_simple_ty: Some(self.clone()), + } + } + pub(crate) fn to_hir(&self) -> Hir { + let hir = |k| Hir::new(HirKind::Expr(k), Span::Artificial); + hir(match self { + SimpleType::Bool => ExprKind::Builtin(Builtin::Bool), + SimpleType::Natural => ExprKind::Builtin(Builtin::Natural), + SimpleType::Integer => ExprKind::Builtin(Builtin::Integer), + SimpleType::Double => ExprKind::Builtin(Builtin::Double), + SimpleType::Text => ExprKind::Builtin(Builtin::Text), + SimpleType::Optional(t) => ExprKind::App( + hir(ExprKind::Builtin(Builtin::Optional)), + t.to_hir(), + ), + SimpleType::List(t) => { + ExprKind::App(hir(ExprKind::Builtin(Builtin::List)), t.to_hir()) + } + SimpleType::Record(kts) => ExprKind::RecordType( + kts.iter() + .map(|(k, t)| (k.as_str().into(), t.to_hir())) + .collect(), + ), + SimpleType::Union(kts) => ExprKind::UnionType( + kts.iter() + .map(|(k, t)| { + (k.as_str().into(), t.as_ref().map(|t| t.to_hir())) + }) + .collect(), + ), + }) + } +} + +impl Sealed for Value {} +impl Sealed for SimpleValue {} +impl Sealed for SimpleType {} + +impl FromDhall for Value { + fn from_dhall(v: &Value) -> Result<Self> { + Ok(v.clone()) + } +} +impl FromDhall for SimpleValue { + fn from_dhall(v: &Value) -> Result<Self> { + v.to_simple_value().ok_or_else(|| { + Error(ErrorKind::Deserialize(format!( + "this cannot be deserialized into a simple type: {}", + v + ))) + }) + } +} +impl FromDhall for SimpleType { + fn from_dhall(v: &Value) -> Result<Self> { + v.to_simple_type().ok_or_else(|| { + Error(ErrorKind::Deserialize(format!( + "this cannot be deserialized into a simple type: {}", + v + ))) + }) + } +} + +impl Eq for Value {} +impl PartialEq for Value { + fn eq(&self, other: &Self) -> bool { + self.hir == other.hir + } +} +impl std::fmt::Display for Value { + fn fmt( + &self, + f: &mut std::fmt::Formatter, + ) -> std::result::Result<(), std::fmt::Error> { + self.to_expr().fmt(f) + } +} diff --git a/serde_dhall/tests/de.rs b/serde_dhall/tests/de.rs index 74912dd..a5c42fd 100644 --- a/serde_dhall/tests/de.rs +++ b/serde_dhall/tests/de.rs @@ -1,10 +1,10 @@ use serde::Deserialize; -use serde_dhall::{from_str, from_str_auto_type, StaticType}; +use serde_dhall::{from_str, FromDhall, StaticType}; #[test] fn test_de_typed() { - fn parse<T: serde_dhall::de::Deserialize + StaticType>(s: &str) -> T { - from_str_auto_type(s).unwrap() + fn parse<T: FromDhall + StaticType>(s: &str) -> T { + from_str(s).static_type_annotation().parse().unwrap() } assert_eq!(parse::<bool>("True"), true); @@ -51,12 +51,17 @@ fn test_de_typed() { Y(i64), } assert_eq!(parse::<Baz>("< X | Y: Integer >.X"), Baz::X); + + assert!(from_str("< X | Y: Integer >.Y") + .static_type_annotation() + .parse::<Baz>() + .is_err()); } #[test] fn test_de_untyped() { - fn parse<T: serde_dhall::de::Deserialize>(s: &str) -> T { - from_str(s).unwrap() + fn parse<T: FromDhall>(s: &str) -> T { + from_str(s).parse().unwrap() } // Test tuples on record of wrong type @@ -83,6 +88,17 @@ fn test_de_untyped() { expected_map ); + #[derive(Debug, PartialEq, Eq, Deserialize)] + struct Foo { + x: u64, + y: Option<u64>, + } + // Omit optional field + assert_eq!(parse::<Foo>("{ x = 1 }"), Foo { x: 1, y: None }); + // https://github.com/Nadrieril/dhall-rust/issues/155 - assert!(from_str::<bool>("List/length [True, 42]").is_err()); + assert!(from_str("List/length [True, 42]").parse::<bool>().is_err()); } + +// TODO: test various builder configurations +// In particular test cloning and reusing builder diff --git a/serde_dhall/tests/traits.rs b/serde_dhall/tests/traits.rs index 15a91ed..3c6fbfe 100644 --- a/serde_dhall/tests/traits.rs +++ b/serde_dhall/tests/traits.rs @@ -1,9 +1,9 @@ -use serde_dhall::{from_str, StaticType, Value}; +use serde_dhall::{from_str, SimpleType, StaticType}; #[test] fn test_static_type() { - fn parse(s: &str) -> Value { - from_str(s).unwrap() + fn parse(s: &str) -> SimpleType { + from_str(s).parse().unwrap() } assert_eq!(bool::static_type(), parse("Bool")); diff --git a/serde_dhall/tests/version_numbers.rs b/serde_dhall/tests/version_numbers.rs new file mode 100644 index 0000000..8307e47 --- /dev/null +++ b/serde_dhall/tests/version_numbers.rs @@ -0,0 +1,17 @@ +#[test] +fn test_readme_deps() { + version_sync::assert_markdown_deps_updated!("../README.md"); +} + +#[test] +fn test_html_root_url() { + version_sync::assert_html_root_url_updated!("src/lib.rs"); +} + +#[test] +fn test_readme_mentions_version() { + version_sync::assert_contains_regex!( + "../README.md", + "^#### \\[{version}\\]" + ); +} |