summaryrefslogtreecommitdiffstats
path: root/template/src/serde_impl.rs
diff options
context:
space:
mode:
Diffstat (limited to 'template/src/serde_impl.rs')
-rw-r--r--template/src/serde_impl.rs447
1 files changed, 447 insertions, 0 deletions
diff --git a/template/src/serde_impl.rs b/template/src/serde_impl.rs
new file mode 100644
index 0000000..2ab709d
--- /dev/null
+++ b/template/src/serde_impl.rs
@@ -0,0 +1,447 @@
+use std::{
+ collections::HashMap,
+ path::{Path, PathBuf}
+};
+
+use serde::{
+ Serialize, Deserialize,
+ de::{
+ Deserializer,
+ },
+};
+use failure::Error;
+use futures::{Future, future::{self, Either}};
+use vec1::Vec1;
+
+use mail_core::{Resource, Source, IRI, Context};
+use mail_headers::header_components::MediaType;
+
+use super::{
+ Template,
+ TemplateEngine,
+ CwdBaseDir,
+ PathRebaseable,
+ Subject,
+ UnsupportedPathError,
+};
+
+/// Type used when deserializing a template using serde.
+///
+/// This type should only be used as intermediate type
+/// used for deserialization as templates need to be
+/// bundled with a template engine.
+///
+/// # Serialize/Deserialize
+///
+/// The derserialization currently only works with
+/// self-describing data formats.
+///
+/// There are a number of shortcuts to deserialize
+/// resources (from emebddings and/or attachments):
+///
+/// - Resources can be deserialized normally (from a externally tagged enum)
+/// - Resources can be deserialized from the serialized repr of `Source`
+/// - Resources can be deserialized from a string which is used to create
+/// a `Resource::Source` with a iri using the `path` scheme and the string
+/// content as the iris "tail".
+#[derive(Debug, Serialize, Deserialize)]
+pub struct TemplateBase<TE: TemplateEngine> {
+ #[serde(rename="name")]
+ template_name: String,
+ #[serde(default)]
+ base_dir: Option<CwdBaseDir>,
+ subject: LazySubject,
+ bodies: Vec1<TE::LazyBodyTemplate>,
+ //TODO impl. deserialize where
+ // resource:String -> IRI::new("path", resource) -> Resource::Source
+ #[serde(deserialize_with="deserialize_embeddings")]
+ embeddings: HashMap<String, Resource>,
+ #[serde(deserialize_with="deserialize_attachments")]
+ attachments: Vec<Resource>,
+}
+
+impl<TE> TemplateBase<TE>
+ where TE: TemplateEngine
+{
+
+ //TODO!! make this load all embeddings/attachments and make it a future
+ /// Couples the template base with a specific engine instance.
+ pub fn load(self, mut engine: TE, default_base_dir: CwdBaseDir, ctx: &impl Context) -> impl Future<Item=Template<TE>, Error=Error> {
+ let TemplateBase {
+ template_name,
+ base_dir,
+ subject,
+ bodies,
+ mut embeddings,
+ mut attachments
+ } = self;
+
+ let base_dir = base_dir.unwrap_or(default_base_dir);
+
+ //FIXME[rust/catch block] use catch block
+ let catch_res = (|| -> Result<_, Error> {
+ let subject = Subject{ template_id: engine.load_subject_template(subject.template_string)? };
+
+ let bodies = bodies.try_mapped(|mut lazy_body| -> Result<_, Error> {
+ lazy_body.rebase_to_include_base_dir(&base_dir)?;
+ Ok(engine.load_body_template(lazy_body)?)
+ })?;
+
+ for embedding in embeddings.values_mut() {
+ embedding.rebase_to_include_base_dir(&base_dir)?;
+ }
+
+ for attachment in attachments.iter_mut() {
+ attachment.rebase_to_include_base_dir(&base_dir)?;
+ }
+
+ Ok((subject, bodies))
+ })();
+
+ let (subject, bodies) =
+ match catch_res {
+ Ok(vals) => vals,
+ Err(err) => { return Either::B(future::err(err)); }
+ };
+
+ let loading_fut = Resource::load_container(embeddings, ctx)
+ .join(Resource::load_container(attachments, ctx));
+
+ let fut = loading_fut
+ .map_err(Error::from)
+ .map(|(embeddings, attachments)| {
+ Template {
+ template_name,
+ base_dir,
+ subject,
+ bodies,
+ embeddings,
+ attachments,
+ engine
+ }
+ });
+
+ Either::A(fut)
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+struct LazySubject {
+ #[serde(flatten)]
+ template_string: String
+}
+
+#[derive(Deserialize)]
+#[serde(untagged)]
+enum ResourceDeserializationHelper {
+ // This allows specifying resources in three ways.
+ // 1. as tagged enum `Resource` (e.g. `{"Source": { "iri": ...}}}`)
+ // 2. as struct `Source` (e.g. `{"iri": ...}` )
+ // 3. as String which is interpreted as path iri
+ Normal(Resource),
+ FromSource(Source),
+ FromString(String)
+}
+
+impl Into<Resource> for ResourceDeserializationHelper {
+ fn into(self) -> Resource {
+ use self::ResourceDeserializationHelper::*;
+ match self {
+ Normal(resource) => resource,
+ FromString(string) => {
+ let source = Source {
+ //UNWRAP_SAFE: only scheme validation could fail,
+ // but its static "path" which is known to be valid
+ iri: IRI::from_parts("path", &string).unwrap(),
+ use_media_type: Default::default(),
+ use_file_name: Default::default()
+ };
+
+ Resource::Source(source)
+ },
+ FromSource(source) => Resource::Source(source)
+ }
+ }
+}
+
+pub fn deserialize_embeddings<'de, D>(deserializer: D)
+ -> Result<HashMap<String, Resource>, D::Error>
+ where D: Deserializer<'de>
+{
+ //FIXME[perf] write custom visitor etc.
+ let map = <HashMap<String, ResourceDeserializationHelper>>
+ ::deserialize(deserializer)?;
+
+ let map = map.into_iter()
+ .map(|(k, helper)| (k, helper.into()))
+ .collect();
+
+ Ok(map)
+}
+
+pub fn deserialize_attachments<'de, D>(deserializer: D)
+ -> Result<Vec<Resource>, D::Error>
+ where D: Deserializer<'de>
+{
+ //FIXME[perf] write custom visitor etc.
+ let vec = <Vec<ResourceDeserializationHelper>>
+ ::deserialize(deserializer)?;
+
+ let vec = vec.into_iter()
+ .map(|helper| helper.into())
+ .collect();
+
+ Ok(vec)
+}
+
+//TODO make base dir default to the dir the template file is in if it's parsed from a template file.
+
+/// Common implementation for a type for [`TemplateEngine::LazyBodyTemplate`].
+///
+/// This impl. gives bodies a field `embeddings` which is a mapping of embedding
+/// names to embeddings (using `deserialize_embeddings`) a `path` field which
+/// allows specifying the template file (e.g. `"body.html"`) and can be relative
+/// to the base dir.
+#[derive(Debug, Serialize)]
+pub struct StandardLazyBodyTemplate {
+ pub path: PathBuf,
+ pub embeddings: HashMap<String, Resource>,
+ pub media_type: Option<MediaType>
+}
+
+
+impl PathRebaseable for StandardLazyBodyTemplate {
+ fn rebase_to_include_base_dir(&mut self, base_dir: impl AsRef<Path>)
+ -> Result<(), UnsupportedPathError>
+ {
+ self.path.rebase_to_include_base_dir(base_dir)
+ }
+
+ fn rebase_to_exclude_base_dir(&mut self, base_dir: impl AsRef<Path>)
+ -> Result<(), UnsupportedPathError>
+ {
+ self.path.rebase_to_exclude_base_dir(base_dir)
+ }
+}
+
+
+#[derive(Deserialize)]
+#[serde(untagged)]
+enum StandardLazyBodyTemplateDeserializationHelper {
+ ShortForm(String),
+ LongForm {
+ path: PathBuf,
+ #[serde(default)]
+ #[serde(deserialize_with="deserialize_embeddings")]
+ embeddings: HashMap<String, Resource>,
+ #[serde(default)]
+ media_type: Option<MediaType>
+ }
+}
+
+impl<'de> Deserialize<'de> for StandardLazyBodyTemplate {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where D: Deserializer<'de>
+ {
+ use self::StandardLazyBodyTemplateDeserializationHelper::*;
+ let helper = StandardLazyBodyTemplateDeserializationHelper::deserialize(deserializer)?;
+ let ok_val =
+ match helper {
+ ShortForm(string) => {
+ StandardLazyBodyTemplate {
+ path: string.into(),
+ embeddings: Default::default(),
+ media_type: Default::default()
+ }
+ },
+ LongForm {path, embeddings, media_type} =>
+ StandardLazyBodyTemplate { path, embeddings, media_type }
+ };
+ Ok(ok_val)
+ }
+}
+
+
+#[cfg(test)]
+mod test {
+ use toml;
+ use super::*;
+
+ fn test_source_iri(resource: &Resource, iri: &str) {
+ if let &Resource::Source(ref source) = resource {
+ assert_eq!(source.iri.as_str(), iri);
+ } else {
+ panic!("unexpected resource expected resource with source and iri {:?} but got {:?}", iri, resource);
+ }
+ }
+
+ mod attachment_deserialization {
+ use super::*;
+ use super::super::deserialize_attachments;
+
+ #[derive(Serialize, Deserialize)]
+ struct Wrapper {
+ #[serde(deserialize_with="deserialize_attachments")]
+ attachments: Vec<Resource>
+ }
+
+
+ #[test]
+ fn should_deserialize_from_strings() {
+ let raw_toml = r#"
+ attachments = ["notes.md", "pic.xd"]
+ "#;
+
+ let Wrapper { attachments } = toml::from_str(raw_toml).unwrap();
+
+ assert_eq!(attachments.len(), 2);
+ test_source_iri(&attachments[0], "path:notes.md");
+ test_source_iri(&attachments[1], "path:pic.xd");
+ }
+
+ #[test]
+ fn should_deserialize_from_sources() {
+ let raw_toml = r#"
+ [[attachments]]
+ Source = {iri="https://fun.example"}
+ [[attachments]]
+ iri="path:pic.xd"
+ "#;
+
+ let Wrapper { attachments } = toml::from_str(raw_toml).unwrap();
+
+ assert_eq!(attachments.len(), 2);
+ test_source_iri(&attachments[0], "https://fun.example");
+ test_source_iri(&attachments[1], "path:pic.xd");
+ }
+
+ #[test]
+ fn check_if_data_is_deserializable_like_expected() {
+ use mail_core::Data;
+
+ let raw_toml = r#"
+ media_type = "text/plain; charset=utf-8"
+ buffer = [65,65,65,66,65]
+ content_id = "c0rc3rcr0q0v32@example.example"
+ "#;
+
+ let data: Data = toml::from_str(raw_toml).unwrap();
+
+ assert_eq!(data.content_id().as_str(), "c0rc3rcr0q0v32@example.example");
+ assert_eq!(&**data.buffer(), b"AAABA" as &[u8]);
+ }
+
+ #[test]
+ fn should_deserialize_from_data() {
+ let raw_toml = r#"
+ [[attachments]]
+ [attachments.Data]
+ media_type = "text/plain; charset=utf-8"
+ buffer = [65,65,65,66,65]
+ content_id = "c0rc3rcr0q0v32@example.example"
+ "#;
+
+ let Wrapper { attachments } = toml::from_str(raw_toml).unwrap();
+
+ assert_eq!(attachments.len(), 1);
+ }
+ }
+
+ mod embedding_deserialization {
+ use super::*;
+ use super::super::deserialize_embeddings;
+
+ #[derive(Serialize, Deserialize)]
+ struct Wrapper {
+ #[serde(deserialize_with="deserialize_embeddings")]
+ embeddings: HashMap<String, Resource>
+ }
+
+ #[test]
+ fn should_deserialize_with_short_forms() {
+ let raw_toml = r#"
+ [embeddings]
+ pic = "hy-ya"
+ pic2 = { iri = "path:ay-ya" }
+ [embeddings.pic3.Data]
+ media_type = "text/plain; charset=utf-8"
+ buffer = [65,65,65,66,65]
+ content_id = "c0rc3rcr0q0v32@example.example"
+ [embeddings.pic4.Source]
+ iri = "path:nay-nay-way"
+ "#;
+
+ let Wrapper { embeddings } = toml::from_str(raw_toml).unwrap();
+
+ assert_eq!(embeddings.len(), 4);
+ assert!(embeddings.contains_key("pic"));
+ assert!(embeddings.contains_key("pic2"));
+ assert!(embeddings.contains_key("pic3"));
+ assert!(embeddings.contains_key("pic4"));
+ test_source_iri(&embeddings["pic"], "path:hy-ya");
+ test_source_iri(&embeddings["pic2"], "path:ay-ya");
+ test_source_iri(&embeddings["pic4"], "path:nay-nay-way");
+ assert_eq!(embeddings["pic3"].content_id().unwrap().as_str(), "c0rc3rcr0q0v32@example.example");
+ }
+ }
+
+ #[allow(non_snake_case)]
+ mod StandardLazyBodyTemplate {
+ use super::*;
+ use super::super::StandardLazyBodyTemplate;
+
+ #[derive(Serialize, Deserialize)]
+ struct Wrapper {
+ body: StandardLazyBodyTemplate
+ }
+
+ #[test]
+ fn should_deserialize_from_string() {
+ let toml_str = r#"
+ body = "template.html.hbs"
+ "#;
+
+ let Wrapper { body } = toml::from_str(toml_str).unwrap();
+ assert_eq!(body.path.to_str().unwrap(), "template.html.hbs");
+ assert_eq!(body.embeddings.len(), 0);
+ }
+
+ #[test]
+ fn should_deserialize_from_object_without_embeddings() {
+ let toml_str = r#"
+ body = { path="t.d" }
+ "#;
+
+ let Wrapper { body }= toml::from_str(toml_str).unwrap();
+ assert_eq!(body.path.to_str().unwrap(), "t.d");
+ assert_eq!(body.embeddings.len(), 0);
+ }
+
+ #[test]
+ fn should_deserialize_from_object_with_empty_embeddings() {
+ let toml_str = r#"
+ body = { path="t.d", embeddings={} }
+ "#;
+
+ let Wrapper { body } = toml::from_str(toml_str).unwrap();
+ assert_eq!(body.path.to_str().unwrap(), "t.d");
+ assert_eq!(body.embeddings.len(), 0);
+ }
+
+ #[test]
+ fn should_deserialize_from_object_with_short_from_embeddings() {
+ let toml_str = r#"
+ body = { path="t.d", embeddings={ pic1="the_embeddings" } }
+ "#;
+
+ let Wrapper { body } = toml::from_str(toml_str).unwrap();
+ assert_eq!(body.path.to_str().unwrap(), "t.d");
+ assert_eq!(body.embeddings.len(), 1);
+
+ let (key, resource) = body.embeddings.iter().next().unwrap();
+ assert_eq!(key, "pic1");
+
+ test_source_iri(resource, "path:the_embeddings");
+ }
+ }
+} \ No newline at end of file