From 52f9ecfc4dac65d305fd920e8c7f748889a0804f Mon Sep 17 00:00:00 2001 From: Nadrieril Date: Mon, 12 Aug 2019 23:24:48 +0200 Subject: Move api into its own crate --- serde_dhall/Cargo.toml | 12 ++ serde_dhall/src/lib.rs | 264 +++++++++++++++++++++++++++++++++++++++++ serde_dhall/src/serde.rs | 70 +++++++++++ serde_dhall/src/static_type.rs | 93 +++++++++++++++ serde_dhall/tests/traits.rs | 68 +++++++++++ 5 files changed, 507 insertions(+) create mode 100644 serde_dhall/Cargo.toml create mode 100644 serde_dhall/src/lib.rs create mode 100644 serde_dhall/src/serde.rs create mode 100644 serde_dhall/src/static_type.rs create mode 100644 serde_dhall/tests/traits.rs (limited to 'serde_dhall') 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 "] +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 = +//! 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 = +//! 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 { + 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, + ) -> 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)>, + ) -> 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 { + 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; + } + + /// 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(s: &str, ty: Option<&Type>) -> Result + 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(s: &str) -> Result + where + T: Deserialize + StaticType, + { + from_str(s, Some(&::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 { + T::deserialize(Deserializer(Cow::Owned(v.to_expr()))) + } +} + +struct Deserializer<'a>(Cow<'a, SubExpr>); + +// impl serde::de::Error for Error { +// fn custom(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(self, visitor: V) -> Result + 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`, +/// 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` 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 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 StaticType for std::result::Result +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 StaticType for Option +where + T: StaticType, +{ + fn static_type() -> Type { + Type::make_optional_type(T::static_type()) + } +} + +impl StaticType for Vec +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!(>::static_type(), parse("Optional Bool")); + assert_eq!( + <(bool, Vec)>::static_type(), + parse("{ _1: Bool, _2: List Text }") + ); + + #[derive(serde_dhall::de::StaticType)] + #[allow(dead_code)] + struct A { + field1: bool, + field2: Option, + } + assert_eq!( + ::static_type(), + parse("{ field1: Bool, field2: Optional Bool }") + ); + + #[derive(StaticType)] + #[allow(dead_code)] + struct B<'a, T: 'a> { + field1: &'a T, + field2: Option, + } + assert_eq!(>::static_type(), A::static_type()); + + #[derive(StaticType)] + #[allow(dead_code)] + struct C(T, Option); + assert_eq!( + >::static_type(), + <(bool, Option)>::static_type() + ); + + #[derive(StaticType)] + #[allow(dead_code)] + struct D(); + assert_eq!( + >::static_type(), + parse("{ _1: {}, _2: Optional Text }") + ); + + #[derive(StaticType)] + #[allow(dead_code)] + enum E { + A(T), + B(String), + }; + assert_eq!(>::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 >")); +} -- cgit v1.2.3