summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--dhall/src/syntax/ast/text.rs4
-rw-r--r--serde_dhall/src/lib.rs158
-rw-r--r--serde_dhall/src/serde.rs109
-rw-r--r--serde_dhall/src/static_type.rs11
-rw-r--r--serde_dhall/tests/de.rs85
5 files changed, 296 insertions, 71 deletions
diff --git a/dhall/src/syntax/ast/text.rs b/dhall/src/syntax/ast/text.rs
index fb390ee..83aaf9a 100644
--- a/dhall/src/syntax/ast/text.rs
+++ b/dhall/src/syntax/ast/text.rs
@@ -97,6 +97,10 @@ impl<SubExpr> InterpolatedText<SubExpr> {
&self.head
}
+ pub fn tail(&self) -> &Vec<(SubExpr, String)> {
+ &self.tail
+ }
+
pub fn head_mut(&mut self) -> &mut String {
&mut self.head
}
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
+ );
+}