summaryrefslogtreecommitdiff
path: root/serde_dhall
diff options
context:
space:
mode:
Diffstat (limited to 'serde_dhall')
-rw-r--r--serde_dhall/Cargo.toml12
-rw-r--r--serde_dhall/src/lib.rs264
-rw-r--r--serde_dhall/src/serde.rs70
-rw-r--r--serde_dhall/src/static_type.rs93
-rw-r--r--serde_dhall/tests/traits.rs68
5 files changed, 507 insertions, 0 deletions
diff --git a/serde_dhall/Cargo.toml b/serde_dhall/Cargo.toml
new file mode 100644
index 0000000..c61ddcd
--- /dev/null
+++ b/serde_dhall/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "serde_dhall"
+version = "0.1.0"
+authors = ["Nadrieril <nadrieril@users.noreply.github.com>"]
+license = "BSD-2-Clause"
+edition = "2018"
+
+[dependencies]
+serde = { version = "1.0", features = ["derive"] }
+dhall = { path = "../dhall" }
+dhall_syntax = { path = "../dhall_syntax" }
+dhall_proc_macros = { path = "../dhall_proc_macros" }
diff --git a/serde_dhall/src/lib.rs b/serde_dhall/src/lib.rs
new file mode 100644
index 0000000..1dbbf99
--- /dev/null
+++ b/serde_dhall/src/lib.rs
@@ -0,0 +1,264 @@
+//! [Dhall][dhall] is a programmable configuration language that provides a non-repetitive
+//! alternative to JSON and YAML.
+//!
+//! You can think of Dhall as: JSON + types + imports + functions
+//!
+//! For a description of the dhall language, examples, tutorials, and more, see the [language
+//! website][dhall].
+//!
+//! This crate provides support for consuming dhall files the same way you would consume JSON or
+//! YAML. It uses the [Serde][serde] serialization library to provide drop-in support for dhall
+//! 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.
+//!
+//! # Examples
+//!
+//! ### Custom datatype
+//!
+//! If you have a custom datatype for which you derived [serde::Deserialize], chances are
+//! you will be able to derive [StaticType][de::StaticType] for it as well.
+//! This gives you access to a dhall representation of your datatype that can be outputted
+//! to users, and allows easy type-safe deserializing.
+//!
+//! ```edition2018
+//! use serde::Deserialize;
+//! use serde_dhall::de::StaticType;
+//!
+//! #[derive(Debug, Deserialize, StaticType)]
+//! struct Point {
+//! x: u64,
+//! y: u64,
+//! }
+//!
+//! fn main() {
+//! // Some dhall data
+//! let data = "{ x = 1, y = 1 + 1 }";
+//!
+//! // Convert the dhall string to a Point.
+//! let point: Point =
+//! serde_dhall::de::from_str_auto_type(&data)
+//! .expect("An error ocurred !");
+//!
+//! // Prints "point = Point { x: 1, y: 2 }"
+//! println!("point = {:?}", point);
+//! }
+//! ```
+//!
+//! ### Loosely typed
+//!
+//! 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::de::from_str][de::from_str].
+//! More generally, if the [StaticType][de::StaticType] derive doesn't suit your
+//! needs, you can still deserialize any valid dhall file that serde can handle.
+//!
+//! [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
+//! use std::collections::BTreeMap;
+//!
+//! let mut map = BTreeMap::new();
+//! map.insert("x".to_string(), 1);
+//! map.insert("y".to_string(), 2);
+//!
+//! // Some dhall data
+//! let data = "{ x = 1, y = 1 + 1 } : { x: Natural, y: Natural }";
+//!
+//! // Deserialize it to a Rust type.
+//! let deserialized_map: BTreeMap<String, usize> =
+//! serde_dhall::de::from_str(&data, None)
+//! .expect("Failed reading the data !");
+//! assert_eq!(map, deserialized_map);
+//! ```
+//!
+//! You can of course specify a dhall type that the input should match.
+//!
+//! ```edition2018
+//! use std::collections::BTreeMap;
+//!
+//! let mut map = BTreeMap::new();
+//! map.insert("x".to_string(), 1);
+//! map.insert("y".to_string(), 2);
+//!
+//! // Some dhall data
+//! let point_data = "{ x = 1, y = 1 + 1 }";
+//! let point_type_data = "{ x: Natural, y: Natural }";
+//!
+//! // Construct a type
+//! let point_type =
+//! serde_dhall::de::from_str(point_type_data, None)
+//! .expect("Could not parse the Point type");
+//!
+//! // Deserialize it to a Rust type.
+//! let deserialized_map: BTreeMap<String, usize> =
+//! serde_dhall::de::from_str(&point_data, Some(&point_type))
+//! .expect("Failed reading the data !");
+//! assert_eq!(map, deserialized_map);
+//! ```
+//!
+//! [dhall]: https://dhall-lang.org/
+//! [serde]: https://docs.serde.rs/serde/
+//! [serde::Deserialize]: https://docs.serde.rs/serde/trait.Deserialize.html
+
+mod serde;
+pub(crate) mod static_type;
+
+pub use value::Value;
+
+mod value {
+ use super::Type;
+ use dhall::error::Result;
+ use dhall::phase::{NormalizedSubExpr, Parsed, Typed};
+
+ // A Dhall value
+ pub struct Value(Typed);
+
+ impl Value {
+ pub fn from_str(s: &str, ty: Option<&Type>) -> Result<Self> {
+ let resolved = Parsed::parse_str(s)?.resolve()?;
+ let typed = match ty {
+ None => resolved.typecheck()?,
+ Some(t) => resolved.typecheck_with(&t.to_type())?,
+ };
+ Ok(Value(typed))
+ }
+ pub(crate) fn to_expr(&self) -> NormalizedSubExpr {
+ self.0.to_expr()
+ }
+ pub(crate) fn to_typed(&self) -> Typed {
+ self.0.clone()
+ }
+ }
+}
+
+pub use typ::Type;
+
+mod typ {
+ use dhall_syntax::Builtin;
+
+ use dhall::core::thunk::{Thunk, TypeThunk};
+ use dhall::core::value::Value;
+ use dhall::error::Result;
+ use dhall::phase::{NormalizedSubExpr, Typed};
+
+ /// A Dhall expression representing a type.
+ ///
+ /// This captures what is usually simply called a "type", like
+ /// `Bool`, `{ x: Integer }` or `Natural -> Text`.
+ #[derive(Debug, Clone, PartialEq, Eq)]
+ pub struct Type(Typed);
+
+ impl Type {
+ pub(crate) fn from_value(v: Value) -> Self {
+ Type(Typed::from_value_untyped(v))
+ }
+ pub(crate) fn make_builtin_type(b: Builtin) -> Self {
+ Self::from_value(Value::from_builtin(b))
+ }
+ pub(crate) fn make_optional_type(t: Type) -> Self {
+ Self::from_value(Value::AppliedBuiltin(
+ Builtin::Optional,
+ vec![t.to_thunk()],
+ ))
+ }
+ pub(crate) fn make_list_type(t: Type) -> Self {
+ Self::from_value(Value::AppliedBuiltin(
+ Builtin::List,
+ vec![t.to_thunk()],
+ ))
+ }
+ #[doc(hidden)]
+ pub fn make_record_type(
+ kts: impl Iterator<Item = (String, Type)>,
+ ) -> Self {
+ Self::from_value(Value::RecordType(
+ kts.map(|(k, t)| {
+ (k.into(), TypeThunk::from_thunk(t.to_thunk()))
+ })
+ .collect(),
+ ))
+ }
+ #[doc(hidden)]
+ pub fn make_union_type(
+ kts: impl Iterator<Item = (String, Option<Type>)>,
+ ) -> Self {
+ Self::from_value(Value::UnionType(
+ kts.map(|(k, t)| {
+ (k.into(), t.map(|t| TypeThunk::from_thunk(t.to_thunk())))
+ })
+ .collect(),
+ ))
+ }
+
+ pub(crate) fn to_thunk(&self) -> Thunk {
+ self.0.to_thunk()
+ }
+ #[allow(dead_code)]
+ pub(crate) fn to_expr(&self) -> NormalizedSubExpr {
+ self.0.to_expr()
+ }
+ pub(crate) fn to_type(&self) -> dhall::phase::Type {
+ self.0.to_type()
+ }
+ }
+
+ impl crate::de::Deserialize for Type {
+ fn from_dhall(v: &super::Value) -> Result<Self> {
+ Ok(Type(v.to_typed()))
+ }
+ }
+}
+
+/// Deserialization of Dhall expressions into Rust
+pub mod de {
+ pub use super::static_type::StaticType;
+ pub use super::{Type, Value};
+ use dhall::error::Result;
+ #[doc(hidden)]
+ pub use dhall_proc_macros::StaticType;
+
+ /// 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.
+ // TODO: seal trait
+ pub trait Deserialize: Sized {
+ /// See [dhall::de::from_str][crate::de::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.
+ ///
+ /// If a type is provided, this additionally checks that the provided
+ /// expression has that type.
+ pub fn from_str<T>(s: &str, ty: Option<&Type>) -> Result<T>
+ where
+ T: Deserialize,
+ {
+ T::from_dhall(&Value::from_str(s, ty)?)
+ }
+
+ /// Deserialize an instance of type T from a string of Dhall text,
+ /// additionally checking that it matches the type of T.
+ ///
+ /// 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_auto_type<T>(s: &str) -> Result<T>
+ where
+ T: Deserialize + StaticType,
+ {
+ from_str(s, Some(&<T as StaticType>::static_type()))
+ }
+}
diff --git a/serde_dhall/src/serde.rs b/serde_dhall/src/serde.rs
new file mode 100644
index 0000000..3dad2d8
--- /dev/null
+++ b/serde_dhall/src/serde.rs
@@ -0,0 +1,70 @@
+use crate::de::{Deserialize, Value};
+use dhall::error::{Error, Result};
+use dhall_syntax::{ExprF, SubExpr, X};
+use std::borrow::Cow;
+
+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, SubExpr<X, X>>);
+
+// impl serde::de::Error for Error {
+// fn custom<T>(msg: T) -> Self
+// where
+// T: std::fmt::Display,
+// {
+// Error::Deserialize(msg.to_string())
+// }
+// }
+
+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 ExprF::*;
+ match self.0.as_ref().as_ref() {
+ NaturalLit(n) => match (*n).try_into() {
+ Ok(n64) => visitor.visit_u64(n64),
+ Err(_) => match (*n).try_into() {
+ Ok(n32) => visitor.visit_u32(n32),
+ Err(_) => unimplemented!(),
+ },
+ },
+ IntegerLit(n) => match (*n).try_into() {
+ Ok(n64) => visitor.visit_i64(n64),
+ Err(_) => match (*n).try_into() {
+ Ok(n32) => visitor.visit_i32(n32),
+ Err(_) => unimplemented!(),
+ },
+ },
+ RecordLit(m) => visitor.visit_map(
+ serde::de::value::MapDeserializer::new(m.iter().map(
+ |(k, v)| (k.as_ref(), Deserializer(Cow::Borrowed(v))),
+ )),
+ ),
+ _ => unimplemented!(),
+ }
+ }
+
+ 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
+ tuple_struct map struct enum identifier ignored_any
+ }
+}
diff --git a/serde_dhall/src/static_type.rs b/serde_dhall/src/static_type.rs
new file mode 100644
index 0000000..13d5d70
--- /dev/null
+++ b/serde_dhall/src/static_type.rs
@@ -0,0 +1,93 @@
+use dhall_syntax::{Builtin, Integer, Natural};
+
+use crate::Type;
+
+/// A Rust type that can be represented as a Dhall type.
+///
+/// A typical example is `Option<bool>`,
+/// represented by the dhall expression `Optional Bool`.
+///
+/// This trait can and should be automatically derived.
+///
+/// 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.
+pub trait StaticType {
+ fn static_type() -> Type;
+}
+
+macro_rules! derive_builtin {
+ ($ty:ty, $builtin:ident) => {
+ impl StaticType for $ty {
+ fn static_type() -> Type {
+ Type::make_builtin_type(Builtin::$builtin)
+ }
+ }
+ };
+}
+
+derive_builtin!(bool, Bool);
+derive_builtin!(Natural, Natural);
+derive_builtin!(u64, Natural);
+derive_builtin!(Integer, Integer);
+derive_builtin!(String, Text);
+
+impl<A, B> StaticType for (A, B)
+where
+ A: StaticType,
+ B: StaticType,
+{
+ fn static_type() -> Type {
+ Type::make_record_type(
+ vec![
+ ("_1".to_owned(), A::static_type()),
+ ("_2".to_owned(), B::static_type()),
+ ]
+ .into_iter(),
+ )
+ }
+}
+
+impl<T, E> StaticType for std::result::Result<T, E>
+where
+ T: StaticType,
+ E: StaticType,
+{
+ fn static_type() -> Type {
+ Type::make_union_type(
+ vec![
+ ("Ok".to_owned(), Some(T::static_type())),
+ ("Err".to_owned(), Some(E::static_type())),
+ ]
+ .into_iter(),
+ )
+ }
+}
+
+impl<T> StaticType for Option<T>
+where
+ T: StaticType,
+{
+ fn static_type() -> Type {
+ Type::make_optional_type(T::static_type())
+ }
+}
+
+impl<T> StaticType for Vec<T>
+where
+ T: StaticType,
+{
+ fn static_type() -> Type {
+ Type::make_list_type(T::static_type())
+ }
+}
+
+impl<'a, T> StaticType for &'a T
+where
+ T: StaticType,
+{
+ fn static_type() -> Type {
+ T::static_type()
+ }
+}
diff --git a/serde_dhall/tests/traits.rs b/serde_dhall/tests/traits.rs
new file mode 100644
index 0000000..99f1109
--- /dev/null
+++ b/serde_dhall/tests/traits.rs
@@ -0,0 +1,68 @@
+#![feature(proc_macro_hygiene)]
+use serde_dhall::de::{from_str, StaticType, Type};
+
+#[test]
+fn test_static_type() {
+ fn parse(s: &str) -> Type {
+ from_str(s, None).unwrap()
+ }
+
+ assert_eq!(bool::static_type(), parse("Bool"));
+ assert_eq!(String::static_type(), parse("Text"));
+ assert_eq!(<Option<bool>>::static_type(), parse("Optional Bool"));
+ assert_eq!(
+ <(bool, Vec<String>)>::static_type(),
+ parse("{ _1: Bool, _2: List Text }")
+ );
+
+ #[derive(serde_dhall::de::StaticType)]
+ #[allow(dead_code)]
+ struct A {
+ field1: bool,
+ field2: Option<bool>,
+ }
+ assert_eq!(
+ <A as serde_dhall::de::StaticType>::static_type(),
+ parse("{ field1: Bool, field2: Optional Bool }")
+ );
+
+ #[derive(StaticType)]
+ #[allow(dead_code)]
+ struct B<'a, T: 'a> {
+ field1: &'a T,
+ field2: Option<T>,
+ }
+ assert_eq!(<B<'static, bool>>::static_type(), A::static_type());
+
+ #[derive(StaticType)]
+ #[allow(dead_code)]
+ struct C<T>(T, Option<String>);
+ assert_eq!(
+ <C<bool>>::static_type(),
+ <(bool, Option<String>)>::static_type()
+ );
+
+ #[derive(StaticType)]
+ #[allow(dead_code)]
+ struct D();
+ assert_eq!(
+ <C<D>>::static_type(),
+ parse("{ _1: {}, _2: Optional Text }")
+ );
+
+ #[derive(StaticType)]
+ #[allow(dead_code)]
+ enum E<T> {
+ A(T),
+ B(String),
+ };
+ assert_eq!(<E<bool>>::static_type(), parse("< A: Bool | B: Text >"));
+
+ #[derive(StaticType)]
+ #[allow(dead_code)]
+ enum F {
+ A,
+ B(bool),
+ };
+ assert_eq!(F::static_type(), parse("< A | B: Bool >"));
+}