diff options
Diffstat (limited to 'serde_dhall')
-rw-r--r-- | serde_dhall/src/lib.rs | 158 | ||||
-rw-r--r-- | serde_dhall/src/serde.rs | 109 | ||||
-rw-r--r-- | serde_dhall/src/static_type.rs | 11 | ||||
-rw-r--r-- | serde_dhall/tests/de.rs | 85 |
4 files changed, 292 insertions, 71 deletions
diff --git a/serde_dhall/src/lib.rs b/serde_dhall/src/lib.rs index 33193dc..0878471 100644 --- a/serde_dhall/src/lib.rs +++ b/serde_dhall/src/lib.rs @@ -12,65 +12,121 @@ //! for any datatype that supports serde (and that's a lot of them !). //! //! This library is limited to deserializing (reading) Dhall values; serializing (writing) -//! values to Dhall is not supported for now. +//! values to Dhall is not supported. //! -//! # Examples +//! # Basic usage //! -//! ### Custom datatype +//! The main entrypoint of this library is the [`from_str`][from_str] function. It reads a string +//! containing a Dhall expression and deserializes it into any serde-compatible type. //! -//! If you have a custom datatype for which you derived [serde::Deserialize], chances are -//! you will be able to derive [StaticType] for it as well. -//! This allows easy type-safe deserializing. +//! This could mean a common Rust type like `HashMap`: //! -//! ```edition2018 +//! ```rust +//! # fn main() -> serde_dhall::de::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 mut expected_map = HashMap::new(); +//! expected_map.insert("x".to_string(), 1); +//! expected_map.insert("y".to_string(), 2); +//! +//! assert_eq!(deserialized_map, expected_map); +//! # Ok(()) +//! # } +//! ``` +//! +//! or a custom datatype, using serde's `derive` mechanism: +//! +//! ```rust +//! # fn main() -> serde_dhall::de::Result<()> { //! use serde::Deserialize; -//! use serde_dhall::{de::Error, StaticType}; //! -//! #[derive(Debug, Deserialize, StaticType)] +//! #[derive(Debug, Deserialize)] //! struct Point { //! x: u64, //! y: u64, //! } //! -//! fn main() -> Result<(), Error> { -//! // 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); +//! // Some Dhall data +//! let data = "{ x = 1, y = 1 + 1 } : { x: Natural, y: Natural }"; //! -//! // 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()); +//! // Convert the Dhall string to a Point. +//! let point: Point = serde_dhall::from_str(data)?; +//! assert_eq!(point.x, 1); +//! assert_eq!(point.y, 2); //! -//! Ok(()) -//! } +//! # Ok(()) +//! # } //! ``` //! -//! ### Loosely typed +//! # 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 in a loosely typed way, you can continue to do so -//! with Dhall. You only need to replace [serde_json::from_str] or [serde_yaml::from_str] -//! with [serde_dhall::from_str][from_str]. -//! More generally, if the [StaticType] derive doesn't suit your -//! needs, you can still deserialize any valid Dhall file that serde can handle. +//! 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]. //! //! [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 //! -//! ```edition2018 +//! +//! # 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 +//! 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. +//! +//! 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]. +//! +//! ```rust //! # fn main() -> serde_dhall::de::Result<()> { -//! use std::collections::BTreeMap; +//! use serde_dhall::Value; +//! 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)?; //! //! // Some Dhall data -//! let data = "{ x = 1, y = 1 + 1 } : { x: Natural, y: Natural }"; +//! let point_data = "{ x = 1, y = 1 + 1 }"; //! -//! // Deserialize it to a Rust type. -//! let deserialized_map: BTreeMap<String, usize> = serde_dhall::from_str(data)?; +//! // 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 mut expected_map = BTreeMap::new(); +//! let mut expected_map = HashMap::new(); //! expected_map.insert("x".to_string(), 1); //! expected_map.insert("y".to_string(), 2); //! @@ -79,29 +135,30 @@ //! # } //! ``` //! -//! You can alternatively specify a Dhall type that the input should match. +//! You can also let Rust infer the appropriate Dhall type, using the [StaticType] trait. //! -//! ```edition2018 +//! ```rust //! # fn main() -> serde_dhall::de::Result<()> { -//! use std::collections::BTreeMap; +//! use serde::Deserialize; +//! use serde_dhall::StaticType; //! -//! // Parse a Dhall type -//! let point_type_str = "{ x: Natural, y: Natural }"; -//! let point_type = serde_dhall::from_str(point_type_str)?; +//! #[derive(Debug, Deserialize, StaticType)] +//! struct Point { +//! x: u64, +//! y: u64, +//! } //! //! // Some Dhall data -//! let point_data = "{ x = 1, y = 1 + 1 }"; +//! let data = "{ x = 1, y = 1 + 1 }"; //! -//! // Deserialize the data to a Rust type. This ensures that -//! // the data matches the point type. -//! let deserialized_map: BTreeMap<String, usize> = -//! serde_dhall::from_str_check_type(point_data, &point_type)?; -//! -//! let mut expected_map = BTreeMap::new(); -//! expected_map.insert("x".to_string(), 1); -//! expected_map.insert("y".to_string(), 2); +//! // 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); //! -//! assert_eq!(deserialized_map, expected_map); +//! // 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(()) //! # } //! ``` @@ -122,6 +179,7 @@ pub use static_type::StaticType; pub use value::Value; // A Dhall value. +#[doc(hidden)] pub mod value { use dhall::semantics::phase::{NormalizedExpr, Parsed, Typed}; use dhall::syntax::Builtin; diff --git a/serde_dhall/src/serde.rs b/serde_dhall/src/serde.rs index 99b9014..2e449df 100644 --- a/serde_dhall/src/serde.rs +++ b/serde_dhall/src/serde.rs @@ -1,5 +1,9 @@ use std::borrow::Cow; +use serde::de::value::{ + MapAccessDeserializer, MapDeserializer, SeqDeserializer, +}; + use dhall::semantics::phase::NormalizedExpr; use dhall::syntax::ExprKind; @@ -28,43 +32,112 @@ impl<'de: 'a, 'a> serde::de::IntoDeserializer<'de, Error> for Deserializer<'a> { 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::*; - match self.0.as_ref().as_ref() { - NaturalLit(n) => { - if let Ok(n64) = (*n).try_into() { - visitor.visit_u64(n64) - } else if let Ok(n32) = (*n).try_into() { - visitor.visit_u32(n32) + 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.as_ref() { + BoolLit(x) => visitor.visit_bool(*x), + NaturalLit(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!() } } - IntegerLit(n) => { - if let Ok(n64) = (*n).try_into() { - visitor.visit_i64(n64) - } else if let Ok(n32) = (*n).try_into() { - visitor.visit_i32(n32) + IntegerLit(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!() } } - RecordLit(m) => visitor.visit_map( - serde::de::value::MapDeserializer::new(m.iter().map( - |(k, v)| (k.as_ref(), Deserializer(Cow::Borrowed(v))), - )), - ), - _ => unimplemented!(), + DoubleLit(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.as_ref() { + Builtin(dhall::syntax::Builtin::OptionalNone) => { + visitor.visit_none() + } + Field(y, name) => match y.as_ref() { + 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.as_ref() { + 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(..) | Embed(..) => 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.as_ref() { + // 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 + 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 1323aa3..1eb9150 100644 --- a/serde_dhall/src/static_type.rs +++ b/serde_dhall/src/static_type.rs @@ -1,4 +1,4 @@ -use dhall::syntax::{Builtin, Integer, Natural}; +use dhall::syntax::Builtin; use crate::Value; @@ -28,9 +28,14 @@ macro_rules! derive_builtin { } derive_builtin!(bool, Bool); -derive_builtin!(Natural, Natural); +derive_builtin!(usize, Natural); derive_builtin!(u64, Natural); -derive_builtin!(Integer, Integer); +derive_builtin!(u32, Natural); +derive_builtin!(isize, Integer); +derive_builtin!(i64, Integer); +derive_builtin!(i32, Integer); +derive_builtin!(f64, Double); +derive_builtin!(f32, Double); derive_builtin!(String, Text); impl<A, B> StaticType for (A, B) diff --git a/serde_dhall/tests/de.rs b/serde_dhall/tests/de.rs new file mode 100644 index 0000000..41a44bd --- /dev/null +++ b/serde_dhall/tests/de.rs @@ -0,0 +1,85 @@ +use serde::Deserialize; +use serde_dhall::{from_str, from_str_auto_type, StaticType}; + +#[test] +fn test_de_typed() { + fn parse<T: serde_dhall::de::Deserialize + StaticType>(s: &str) -> T { + from_str_auto_type(s).unwrap() + } + + assert_eq!(parse::<bool>("True"), true); + + assert_eq!(parse::<u64>("1"), 1); + assert_eq!(parse::<u32>("1"), 1); + assert_eq!(parse::<usize>("1"), 1); + + assert_eq!(parse::<i64>("+1"), 1); + assert_eq!(parse::<i32>("+1"), 1); + assert_eq!(parse::<isize>("+1"), 1); + + assert_eq!(parse::<f64>("1.0"), 1.0); + assert_eq!(parse::<f32>("1.0"), 1.0); + + assert_eq!(parse::<String>(r#""foo""#), "foo".to_owned()); + assert_eq!(parse::<Vec<u64>>("[] : List Natural"), vec![]); + assert_eq!(parse::<Vec<u64>>("[1, 2]"), vec![1, 2]); + assert_eq!(parse::<Option<u64>>("None Natural"), None); + assert_eq!(parse::<Option<u64>>("Some 1"), Some(1)); + + assert_eq!( + parse::<(u64, String)>(r#"{ _1 = 1, _2 = "foo" }"#), + (1, "foo".to_owned()) + ); + + #[derive(Debug, PartialEq, Eq, Deserialize, StaticType)] + struct Foo { + x: u64, + y: i64, + } + assert_eq!(parse::<Foo>("{ x = 1, y = -2 }"), Foo { x: 1, y: -2 }); + + #[derive(Debug, PartialEq, Eq, Deserialize, StaticType)] + enum Bar { + X(u64), + Y(i64), + } + assert_eq!(parse::<Bar>("< X: Natural | Y: Integer >.X 1"), Bar::X(1)); + + #[derive(Debug, PartialEq, Eq, Deserialize, StaticType)] + enum Baz { + X, + Y(i64), + } + assert_eq!(parse::<Baz>("< X | Y: Integer >.X"), Baz::X); +} + +#[test] +fn test_de_untyped() { + fn parse<T: serde_dhall::de::Deserialize>(s: &str) -> T { + from_str(s).unwrap() + } + + // Test tuples on record of wrong type + assert_eq!( + parse::<(u64, String, isize)>(r#"{ y = "foo", x = 1, z = +42 }"#), + (1, "foo".to_owned(), 42) + ); + + use std::collections::HashMap; + let mut expected_map = HashMap::new(); + expected_map.insert("x".to_string(), 1); + expected_map.insert("y".to_string(), 2); + assert_eq!( + parse::<HashMap<String, usize>>("{ x = 1, y = 2 }"), + expected_map + ); + + use std::collections::BTreeMap; + let mut expected_map = BTreeMap::new(); + expected_map.insert("x".to_string(), 1); + expected_map.insert("y".to_string(), 2); + assert_eq!( + parse::<BTreeMap<String, usize>>("{ x = 1, y = 2 }"), + expected_map + ); +} |