summaryrefslogtreecommitdiffstats
path: root/template/src
diff options
context:
space:
mode:
Diffstat (limited to 'template/src')
-rw-r--r--template/src/additional_cid.rs60
-rw-r--r--template/src/base_dir.rs153
-rw-r--r--template/src/handlebars.rs96
-rw-r--r--template/src/lib.rs411
-rw-r--r--template/src/path_rebase.rs191
-rw-r--r--template/src/serde_impl.rs447
6 files changed, 1358 insertions, 0 deletions
diff --git a/template/src/additional_cid.rs b/template/src/additional_cid.rs
new file mode 100644
index 0000000..eecf2e7
--- /dev/null
+++ b/template/src/additional_cid.rs
@@ -0,0 +1,60 @@
+use std::collections::{
+ HashMap, HashSet
+};
+
+use serde::{Serialize, Serializer};
+
+use mail_core::Resource;
+use mail_headers::header_components::ContentId;
+
+pub struct AdditionalCIds<'a> {
+ additional_resources: &'a [&'a HashMap<String, Resource>]
+}
+
+impl<'a> AdditionalCIds<'a> {
+
+ /// Creates a new `AdditionalCIds` instance.
+ ///
+ /// All resources in the all hash maps have to be loaded to the
+ /// `Data` or `EncData` variants or using `get` can panic.
+ pub(crate) fn new(additional_resources: &'a [&'a HashMap<String, Resource>]) -> Self {
+ AdditionalCIds { additional_resources }
+ }
+
+
+ /// Returns the content id associated with the given name.
+ ///
+ /// If multiple of the maps used to create this type contain the
+ /// key the first match is returned and all later ones are ignored.
+ ///
+ /// # Panic
+ ///
+ /// If the resource exists but is not loaded (i.e. has no content id)
+ /// this will panic as this can only happen if there is a bug in the
+ /// mail code, or this type was used externally.
+ pub fn get(&self, name: &str) -> Option<&ContentId> {
+ for possible_source in self.additional_resources {
+ if let Some(res) = possible_source.get(name) {
+ return Some(res.content_id().expect("all resources should be loaded/have a content id"));
+ }
+ }
+ return None;
+ }
+}
+
+impl<'a> Serialize for AdditionalCIds<'a> {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where S: Serializer
+ {
+ let mut existing_keys = HashSet::new();
+ serializer.collect_map(
+ self.additional_resources
+ .iter()
+ .flat_map(|m| m.iter().map(|(k, resc)| {
+ (k, resc.content_id().expect("all resources should be loaded/have a content id"))
+ }))
+ .filter(|key| existing_keys.insert(key.to_owned()))
+ )
+ }
+}
+
diff --git a/template/src/base_dir.rs b/template/src/base_dir.rs
new file mode 100644
index 0000000..91e824a
--- /dev/null
+++ b/template/src/base_dir.rs
@@ -0,0 +1,153 @@
+use std::{
+ path::{Path, PathBuf},
+ ops::{Deref, DerefMut},
+ env, io
+};
+
+use serde::{
+ ser::{Serialize, Serializer},
+ de::{Deserialize, Deserializer}
+};
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct CwdBaseDir(PathBuf);
+
+impl CwdBaseDir {
+
+ /// Creates a new `CwdBaseDir` instance containing exactly the given path.
+ pub fn new_unchanged(path: PathBuf) -> Self {
+ CwdBaseDir(path)
+ }
+
+ /// Creates a `CwdBaseDir` from a path by prefixing the path with the
+ /// current working dir if it's relative.
+ ///
+ /// If the path is not relative it's directly used.
+ ///
+ /// # Os state side effects
+ ///
+ /// As this function accesses the current working directory (CWD) it's
+ /// not pure as the CWD can be changed (e.g. by `std::env::set_current_dir`).
+ ///
+ /// # Error
+ ///
+ /// As getting the CWD can fail this function can fail with a I/O Error, too.
+ pub fn from_path<P>(path: P) -> Result<Self, io::Error>
+ where P: AsRef<Path> + Into<PathBuf>
+ {
+ let path =
+ if path.as_ref().is_absolute() {
+ path.into()
+ } else {
+ let mut cwd = env::current_dir()?;
+ cwd.push(path.as_ref());
+ cwd
+ };
+
+ Ok(CwdBaseDir(path))
+ }
+
+ /// Turns this path into a `PathBuf` by stripping the current working dir
+ /// if it starts with it.
+ ///
+ /// If this path does not start with the CWD it's returned directly.
+ ///
+ /// # Os state side effects
+ ///
+ /// As this function used the current working dir (CWD) it is affected
+ /// by any function changing the CWD as a side effect.
+ ///
+ /// # Error
+ ///
+ /// Accessing the current working dir can fail, as such this function
+ /// can fail.
+ pub fn to_base_path(&self) -> Result<&Path, io::Error> {
+ let cwd = env::current_dir()?;
+ self.strip_prefix(&cwd)
+ .or_else(|_err_does_not_has_that_prefix| {
+ Ok(&self)
+ })
+ }
+
+ /// Turns this instance into the `PathBuf` it dereferences to.
+ pub fn into_inner_with_prefix(self) -> PathBuf {
+ let CwdBaseDir(path) = self;
+ path
+ }
+}
+
+impl Deref for CwdBaseDir {
+ type Target = PathBuf;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl DerefMut for CwdBaseDir {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+impl AsRef<Path> for CwdBaseDir {
+ fn as_ref(&self) -> &Path {
+ &self.0
+ }
+}
+
+
+
+impl<'de> Deserialize<'de> for CwdBaseDir {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where D: Deserializer<'de>
+ {
+ use serde::de::Error;
+ let path_buf = PathBuf::deserialize(deserializer)?;
+ Self::from_path(path_buf)
+ .map_err(|err| D::Error::custom(err))
+ }
+}
+
+impl Serialize for CwdBaseDir {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where S: Serializer,
+ {
+ use serde::ser::Error;
+ let path = self.to_base_path()
+ .map_err(|err| S::Error::custom(err))?;
+
+ path.serialize(serializer)
+ }
+}
+
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn from_path_does_not_affect_absolute_paths() {
+ let path = Path::new("/the/dog");
+ let base_dir = CwdBaseDir::from_path(path).unwrap();
+ assert_eq!(&*base_dir, Path::new("/the/dog"))
+ }
+
+ #[test]
+ fn from_path_prefixes_with_cwd() {
+ let cwd = env::current_dir().unwrap();
+ let expected = cwd.join("./the/dog");
+
+ let base_dir = CwdBaseDir::from_path("./the/dog").unwrap();
+ assert_eq!(&*base_dir, &expected);
+ }
+
+ #[test]
+ fn to_base_path_removes_cwd_prefix() {
+ let cwd = env::current_dir().unwrap();
+ let dir = cwd.join("hy/there");
+ let base_dir = CwdBaseDir::new_unchanged(dir);
+ let path = base_dir.to_base_path().unwrap();
+ assert_eq!(path, Path::new("hy/there"));
+ }
+} \ No newline at end of file
diff --git a/template/src/handlebars.rs b/template/src/handlebars.rs
new file mode 100644
index 0000000..887dce1
--- /dev/null
+++ b/template/src/handlebars.rs
@@ -0,0 +1,96 @@
+use hbs;
+use serde::{Serialize, Deserialize};
+use galemu::{Bound, BoundExt, create_gal_wrapper_type, Ref};
+use failure::Error;
+
+
+use super::{
+ TemplateEngine,
+ TemplateEngineCanHandleData,
+ BodyTemplate,
+ PreparationData,
+ AdditionalCIds,
+ PathRebaseable,
+ UnsupportedPathError,
+ serde_impl
+};
+
+
+
+pub struct Handlebars {
+ inner: hbs::Handlebars,
+ name_counter: usize
+}
+
+impl Handlebars {
+
+ fn next_body_template_name(&mut self) -> String {
+ let name = format!("body_{}", self.name_counter);
+ self.name_counter += 1;
+ name
+ }
+}
+impl TemplateEngine for Handlebars {
+ type Id = String;
+
+ type LazyBodyTemplate = serde_impl::StandardLazyBodyTemplate;
+
+ fn load_body_template(&mut self, tmpl: Self::LazyBodyTemplate)
+ -> Result<BodyTemplate<Self>, Error>
+ {
+ let StandardLazyBodyTemplate {
+ path, embeddings, media_type
+ } = tmpl;
+
+ let name = self.next_body_template_name();
+ self.inner.register_template_file(name, path)?;
+
+ let media_type =
+ if let Some(media_type) = media_type {
+ media_type
+ } else if let Some(extension) = path.extension().and_then(|osstr| osstr.as_str()) {
+ match extension {
+ "html" => "text/html; charset=utf-8".parse().unwrap(),
+ "txt" => "text/plain; charset=utf-8".parse().unwrap()
+ }
+ } else {
+ return Err(failure::err_msg(
+ "handlebars requires html/txt file extension or media type given in template spec"
+ ));
+ };
+
+ Ok(BodyTemplate {
+ template_id: name,
+ media_type: TODO,
+ inline_embeddings: Default::default(),
+ })
+ }
+
+ fn load_subject_template(&mut self, template_string: String)
+ -> Result<Self::Id, Error>
+ {
+ Ok(self.inner.register_template_string("subject".to_owned(), template_string)?)
+ }
+}
+
+/// Additional trait a template engine needs to implement for the types it can process as input.
+///
+/// This could for example be implemented in a wild card impl for the template engine for
+/// any data `D` which implements `Serialize`.
+impl<D> TemplateEngineCanHandleData<D> for Handlebars
+ where D: Serialize
+{
+ fn render<'r, 'a>(
+ &'r self,
+ id: &'r Self::Id,
+ data: &'r D,
+ additional_cids: AdditionalCIds<'r>
+ ) -> Result<String, Error> {
+ Ok(self.inner.render(id, SerHelper { data, cids: additional_cid })?)
+ }
+}
+
+struct SerHelper<'r, D> {
+ data: &'r D,
+ cids: AdditionalCIds<'r>
+} \ No newline at end of file
diff --git a/template/src/lib.rs b/template/src/lib.rs
new file mode 100644
index 0000000..b889213
--- /dev/null
+++ b/template/src/lib.rs
@@ -0,0 +1,411 @@
+extern crate failure;
+extern crate serde;
+extern crate futures;
+extern crate mail_core;
+extern crate mail_headers;
+extern crate vec1;
+extern crate toml;
+extern crate maybe_owned;
+#[cfg(feature="handlebars")]
+extern crate handlebars as hbs;
+
+#[cfg(all(feature="handlebars", not(feature="handlebars-bindings")))]
+compile_error!("use feature `handlebars-bindings` instead of opt-dep-auto-feature `handlebars`");
+
+use std::{
+ fs,
+ collections::HashMap,
+ fmt::Debug,
+ path::{Path, PathBuf},
+ ops::Deref
+};
+
+use serde::{
+ Serialize,
+ Deserialize
+};
+use failure::{Fail, Error};
+use futures::{
+ Future, Poll, Async,
+ try_ready,
+ future::{self, Join, Either}
+};
+use vec1::Vec1;
+use maybe_owned::MaybeOwned;
+
+use mail_core::{
+ Resource,
+ Data, Metadata,
+ Context, ResourceContainerLoadingFuture,
+ compose::{MailParts, BodyPart},
+ Mail
+};
+use mail_headers::{
+ HeaderKind, Header,
+ header_components::MediaType,
+ headers
+};
+
+pub mod serde_impl;
+mod base_dir;
+mod path_rebase;
+mod additional_cid;
+
+// #[cfg(feature="handlebars")]
+// pub mod handlebars;
+
+pub use self::base_dir::*;
+pub use self::path_rebase::*;
+pub use self::additional_cid::*;
+
+/// Trait used to bind/implement template engines.
+pub trait TemplateEngine: Sized {
+ type Id: Debug;
+
+ type LazyBodyTemplate: PathRebaseable + Debug + Send + Serialize + for<'a> Deserialize<'a>;
+
+ fn load_body_template(&mut self, tmpl: Self::LazyBodyTemplate)
+ -> Result<BodyTemplate<Self>, Error>;
+
+ fn load_subject_template(&mut self, template_string: String)
+ -> Result<Self::Id, Error>;
+}
+
+/// Additional trait a template engine needs to implement for the types it can process as input.
+///
+/// This could for example be implemented in a wild card impl for the template engine for
+/// any data `D` which implements `Serialize`.
+pub trait TemplateEngineCanHandleData<D>: TemplateEngine {
+
+ fn render<'r, 'a>(
+ &'r self,
+ id: &'r Self::Id,
+ data: &'r D,
+ additional_cids: AdditionalCIds<'r>
+ ) -> Result<String, Error>;
+}
+
+/// Load a template as described in a toml file.
+pub fn load_toml_template_from_path<TE, C>(
+ engine: TE,
+ path: PathBuf,
+ ctx: &C
+) -> impl Future<Item=Template<TE>, Error=Error>
+ where TE: TemplateEngine + 'static, C: Context
+{
+
+ let ctx2 = ctx.clone();
+ ctx.offload_fn(move || {
+ let content = fs::read_to_string(&path)?;
+ let base: serde_impl::TemplateBase<TE> = toml::from_str(&content)?;
+ let base_dir = path.parent().unwrap_or_else(||Path::new("."));
+ let base_dir = CwdBaseDir::from_path(base_dir)?;
+ Ok((base, base_dir))
+ }).and_then(move |(base, base_dir)| base.load(engine, base_dir, &ctx2))
+}
+
+/// Load a template as described in a toml string;
+pub fn load_toml_template_from_str<TE, C>(
+ engine: TE,
+ content: &str,
+ ctx: &C
+) -> impl Future<Item=Template<TE>, Error=Error>
+ where TE: TemplateEngine, C: Context
+{
+ let base: serde_impl::TemplateBase<TE> =
+ match toml::from_str(content) {
+ Ok(base) => base,
+ Err(err) => { return Either::B(future::err(Error::from(err))); }
+ };
+
+ let base_dir =
+ match CwdBaseDir::from_path(Path::new(".")) {
+ Ok(base_dir) => base_dir,
+ Err(err) => { return Either::B(future::err(Error::from(err))) }
+ };
+
+ Either::A(base.load(engine, base_dir, ctx))
+}
+
+
+/// A Mail template.
+#[derive(Debug)]
+pub struct Template<TE: TemplateEngine> {
+ template_name: String,
+ base_dir: CwdBaseDir,
+ subject: Subject<TE>,
+ /// This can only be in the loaded form _iff_ this is coupled
+ /// with a template engine instance, as using it with the wrong
+ /// template engine will lead to potential bugs and panics.
+ bodies: Vec1<BodyTemplate<TE>>,
+ //TODO: make sure
+ embeddings: HashMap<String, Resource>,
+ attachments: Vec<Resource>,
+ engine: TE,
+}
+
+impl<TE> Template<TE>
+ where TE: TemplateEngine
+{
+ pub fn inline_embeddings(&self) -> &HashMap<String, Resource> {
+ &self.embeddings
+ }
+
+ pub fn attachments(&self) -> &[Resource] {
+ &self.attachments
+ }
+
+ pub fn engine(&self) -> &TE {
+ &self.engine
+ }
+
+ pub fn bodies(&self) -> &[BodyTemplate<TE>] {
+ &self.bodies
+ }
+
+ pub fn subject_template_id(&self) -> &TE::Id {
+ &self.subject.template_id
+ }
+}
+
+
+/// Represents one of potentially many alternate bodies in a template.
+#[derive(Debug)]
+pub struct BodyTemplate<TE: TemplateEngine> {
+ pub template_id: TE::Id,
+ pub media_type: MediaType,
+ pub inline_embeddings: HashMap<String, Resource>
+}
+
+impl<TE> BodyTemplate<TE>
+ where TE: TemplateEngine
+{
+ pub fn template_id(&self) -> &TE::Id {
+ &self.template_id
+ }
+
+ pub fn media_type(&self) -> &MediaType {
+ &self.media_type
+ }
+
+ pub fn inline_embeddings(&self) -> &HashMap<String, Resource> {
+ &self.inline_embeddings
+ }
+}
+
+/// Represents a template used for generating the subject of a mail.
+#[derive(Debug)]
+pub struct Subject<TE: TemplateEngine> {
+ template_id: TE::Id
+}
+
+impl<TE> Subject<TE>
+ where TE: TemplateEngine
+{
+ pub fn template_id(&self) -> &TE::Id {
+ &self.template_id
+ }
+}
+
+/// Automatically provides the `prepare_to_render` method for all `Templates`
+///
+/// This trait is implemented for all `Templates`/`D`(data) combinations where
+/// the templates template engine can handle the given data (impl. `TemplateEngineCanHandleData<D>`)
+///
+/// This trait should not be implemented by hand.
+pub trait TemplateExt<TE, D>
+ where TE: TemplateEngine + TemplateEngineCanHandleData<D>
+{
+
+ fn render_to_mail_parts<'r>(&self, data: LoadedTemplateData<'r, D>, ctx: &impl Context)
+ -> Result<(MailParts, Header<headers::Subject>), Error>;
+
+ fn render<'r>(&self, data: LoadedTemplateData<'r, D>, ctx: &impl Context) -> Result<Mail, Error> {
+ let (parts, subject) = self.render_to_mail_parts(data, ctx)?;
+ let mut mail = parts.compose();
+ mail.insert_header(subject);
+ Ok(mail)
+ }
+}
+
+
+impl<TE, D> TemplateExt<TE, D> for Template<TE>
+ where TE: TemplateEngine + TemplateEngineCanHandleData<D>
+{
+ fn render_to_mail_parts<'r>(&self, data: LoadedTemplateData<'r, D>, ctx: &impl Context)
+ -> Result<(MailParts, Header<headers::Subject>), Error>
+ {
+ let TemplateData {
+ data,
+ inline_embeddings,
+ mut attachments
+ } = data.into();
+
+ let subject = self.engine().render(
+ self.subject_template_id(),
+ &data,
+ AdditionalCIds::new(&[])
+ )?;
+
+ let subject = headers::Subject::auto_body(subject)?;
+
+ //TODO use Vec1 try_map instead of loop
+ let mut bodies = Vec::new();
+ for body in self.bodies().iter() {
+ let raw = self.engine().render(
+ body.template_id(),
+ &data,
+ AdditionalCIds::new(&[
+ &inline_embeddings,
+ body.inline_embeddings(),
+ self.inline_embeddings()
+ ])
+ )?;
+
+ let data = Data::new(
+ raw.into_bytes(),
+ Metadata {
+ file_meta: Default::default(),
+ media_type: body.media_type().clone(),
+ content_id: ctx.generate_content_id()
+ }
+ );
+
+ let inline_embeddings = body.inline_embeddings()
+ .values()
+ .cloned()
+ .collect();
+
+ bodies.push(BodyPart {
+ resource: Resource::Data(data),
+ inline_embeddings,
+ attachments: Vec::new()
+ });
+ }
+
+ attachments.extend(self.attachments().iter().cloned());
+
+ let mut inline_embeddings_vec = Vec::new();
+ for (key, val) in self.inline_embeddings() {
+ if !inline_embeddings.contains_key(key) {
+ inline_embeddings_vec.push(val.clone())
+ }
+ }
+
+ inline_embeddings_vec.extend(inline_embeddings.into_iter().map(|(_,v)|v));
+
+ let parts = MailParts {
+ //UNWRAP_SAFE (complexly mapping a Vec1 is safe)
+ alternative_bodies: Vec1::from_vec(bodies).unwrap(),
+ inline_embeddings: inline_embeddings_vec,
+ attachments
+ };
+
+ Ok((parts, subject))
+ }
+}
+
+pub struct TemplateData<'a, D: 'a> {
+ pub data: MaybeOwned<'a, D>,
+ pub attachments: Vec<Resource>,
+ pub inline_embeddings: HashMap<String, Resource>
+}
+
+impl<'a, D> TemplateData<'a, D> {
+
+ pub fn load(self, ctx: &impl Context) -> DataLoadingFuture<'a, D> {
+ let TemplateData {
+ data,
+ attachments,
+ inline_embeddings
+ } = self;
+
+ let loading_fut = Resource::load_container(inline_embeddings, ctx)
+ .join(Resource::load_container(attachments, ctx));
+
+ DataLoadingFuture {
+ payload: Some(data),
+ loading_fut
+ }
+ }
+}
+impl<D> From<D> for TemplateData<'static, D> {
+ fn from(data: D) -> Self {
+ TemplateData {
+ data: data.into(),
+ attachments: Default::default(),
+ inline_embeddings: Default::default()
+ }
+ }
+}
+
+impl<'a, D> From<&'a D> for TemplateData<'a, D> {
+ fn from(data: &'a D) -> Self {
+ TemplateData {
+ data: data.into(),
+ attachments: Default::default(),
+ inline_embeddings: Default::default()
+ }
+ }
+}
+
+pub struct LoadedTemplateData<'a, D: 'a>(TemplateData<'a, D>);
+
+impl<'a, D> From<&'a D> for LoadedTemplateData<'a, D> {
+ fn from(data: &'a D) -> Self {
+ LoadedTemplateData(TemplateData::from(data))
+ }
+}
+
+impl<D> From<D> for LoadedTemplateData<'static, D> {
+ fn from(data: D) -> Self {
+ LoadedTemplateData(TemplateData::from(data))
+ }
+}
+
+impl<'a, D> Deref for LoadedTemplateData<'a, D> {
+ type Target = TemplateData<'a, D>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl<'a, D> Into<TemplateData<'a, D>> for LoadedTemplateData<'a, D> {
+ fn into(self) -> TemplateData<'a, D> {
+ let LoadedTemplateData(data) = self;
+ data
+ }
+}
+
+/// Future returned when preparing a template for rendering.
+pub struct DataLoadingFuture<'a, D: 'a> {
+ payload: Option<MaybeOwned<'a, D>>,
+ loading_fut: Join<
+ ResourceContainerLoadingFuture<HashMap<String, Resource>>,
+ ResourceContainerLoadingFuture<Vec<Resource>>
+ >
+}
+
+impl<'a, D> Future for DataLoadingFuture<'a, D> {
+ type Item = LoadedTemplateData<'a, D>;
+ type Error = Error;
+
+ fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
+ let (
+ inline_embeddings,
+ attachments
+ ) = try_ready!(self.loading_fut.poll());
+
+ //UNWRAP_SAFE only non if polled after resolved
+ let data = self.payload.take().unwrap();
+
+ let inner = TemplateData {
+ data,
+ inline_embeddings,
+ attachments
+ };
+
+ Ok(Async::Ready(LoadedTemplateData(inner)))
+ }
+}
diff --git a/template/src/path_rebase.rs b/template/src/path_rebase.rs
new file mode 100644
index 0000000..9818aa3
--- /dev/null
+++ b/template/src/path_rebase.rs
@@ -0,0 +1,191 @@
+use std::{
+ path::{Path, PathBuf},
+ mem
+};
+
+use mail_core::{IRI, Resource};
+use failure::{Fail, Context};
+
+#[derive(Fail, Debug)]
+#[fail(display = "unsupported path, only paths with following constraint are allowed: {}", _0)]
+pub struct UnsupportedPathError(Context<&'static str>);
+
+impl UnsupportedPathError {
+ pub fn new(violated_constraint: &'static str) -> Self {
+ UnsupportedPathError(Context::new(violated_constraint))
+ }
+}
+
+pub trait PathRebaseable {
+ /// Prefixes path in the type with `base_dir`.
+ ///
+ /// # Error
+ ///
+ /// Some implementors might not support all paths.
+ /// For example a implementor requiring rust string
+ /// compatible paths might return a
+ /// `Err(UnsupportedPathError::new("utf-8"))`.
+ fn rebase_to_include_base_dir(&mut self, base_dir: impl AsRef<Path>)
+ -> Result<(), UnsupportedPathError>;
+
+ /// Removes the `base_dir` prefix.
+ ///
+ /// # Error
+ ///
+ /// Some implementors might not support all paths.
+ /// For example a implementor requiring rust string
+ /// compatible paths might return a
+ /// `Err(UnsupportedPathError::new("utf-8"))`.
+ fn rebase_to_exclude_base_dir(&mut self, base_dir: impl AsRef<Path>)
+ -> Result<(), UnsupportedPathError>;
+}
+
+impl PathRebaseable for PathBuf {
+ fn rebase_to_include_base_dir(&mut self, base_dir: impl AsRef<Path>)
+ -> Result<(), UnsupportedPathError>
+ {
+ let new_path;
+ if self.is_relative() {
+ new_path = base_dir.as_ref().join(&self);
+ } else {
+ return Ok(());
+ }
+ mem::replace(self, new_path);
+ Ok(())
+ }
+
+ fn rebase_to_exclude_base_dir(&mut self, base_dir: impl AsRef<Path>)
+ -> Result<(), UnsupportedPathError>
+ {
+ let new_path;
+ if let Ok(path) = self.strip_prefix(base_dir) {
+ new_path = path.to_owned();
+ } else {
+ return Ok(());
+ }
+ mem::replace(self, new_path);
+ Ok(())
+ }
+}
+
+impl PathRebaseable for IRI {
+ fn rebase_to_include_base_dir(&mut self, base_dir: impl AsRef<Path>)
+ -> Result<(), UnsupportedPathError>
+ {
+ if self.scheme() != "path" {
+ return Ok(());
+ }
+
+ let new_tail = {
+ let path = Path::new(self.tail());
+ if path.is_relative() {
+ base_dir.as_ref().join(path)
+ } else {
+ return Ok(());
+ }
+ };
+
+ let new_tail = new_tail.to_str()
+ .ok_or_else(|| UnsupportedPathError::new("utf-8"))?;
+
+ let new_iri = self.with_tail(new_tail);
+ mem::replace(self, new_iri);
+ Ok(())
+ }
+
+ fn rebase_to_exclude_base_dir(&mut self, base_dir: impl AsRef<Path>)
+ -> Result<(), UnsupportedPathError>
+ {
+ if self.scheme() != "path" {
+ return Ok(());
+ }
+
+ let new_iri = {
+ let path = Path::new(self.tail());
+
+ if let Ok(path) = path.strip_prefix(base_dir) {
+ //UNWRAP_SAFE: we just striped some parts, this can
+ // not make it lose it's string-ness
+ let new_tail = path.to_str().unwrap();
+ self.with_tail(new_tail)
+ } else {
+ return Ok(());
+ }
+ };
+
+ mem::replace(self, new_iri);
+ Ok(())
+ }
+}
+
+impl PathRebaseable for Resource {
+ fn rebase_to_include_base_dir(&mut self, base_dir: impl AsRef<Path>)
+ -> Result<(), UnsupportedPathError>
+ {
+ if let &mut Resource::Source(ref mut source) = self {
+ source.iri.rebase_to_include_base_dir(base_dir)?;
+ }
+ Ok(())
+ }
+
+ fn rebase_to_exclude_base_dir(&mut self, base_dir: impl AsRef<Path>)
+ -> Result<(), UnsupportedPathError>
+ {
+ if let &mut Resource::Source(ref mut source) = self {
+ source.iri.rebase_to_exclude_base_dir(base_dir)?;
+ }
+ Ok(())
+ }
+
+}
+
+
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use mail_core::Source;
+
+ #[test]
+ fn rebase_on_path() {
+ let mut path = Path::new("/prefix/suffix.yup").to_owned();
+ path.rebase_to_exclude_base_dir("/prefix").unwrap();
+ assert_eq!(path, Path::new("suffix.yup"));
+ path.rebase_to_include_base_dir("./nfix").unwrap();
+ path.rebase_to_include_base_dir("/mfix").unwrap();
+ assert_eq!(path, Path::new("/mfix/nfix/suffix.yup"));
+ path.rebase_to_exclude_base_dir("/wrong").unwrap();
+ assert_eq!(path, Path::new("/mfix/nfix/suffix.yup"));
+ }
+
+ #[test]
+ fn rebase_on_iri() {
+ let mut iri: IRI = "path:/prefix/suffix.yup".parse().unwrap();
+ iri.rebase_to_exclude_base_dir("/prefix").unwrap();
+ assert_eq!(iri.as_str(), "path:suffix.yup");
+ iri.rebase_to_include_base_dir("nfix").unwrap();
+ iri.rebase_to_include_base_dir("/mfix").unwrap();
+ assert_eq!(iri.as_str(), "path:/mfix/nfix/suffix.yup");
+ iri.rebase_to_exclude_base_dir("/wrong").unwrap();
+ assert_eq!(iri.as_str(), "path:/mfix/nfix/suffix.yup");
+ }
+
+ #[test]
+ fn rebase_on_resource() {
+ let mut resource = Resource::Source(Source {
+ iri: "path:abc/def".parse().unwrap(),
+ use_media_type: Default::default(),
+ use_file_name: Default::default()
+ });
+
+ resource.rebase_to_include_base_dir("./abc").unwrap();
+ resource.rebase_to_include_base_dir("/pre").unwrap();
+ resource.rebase_to_exclude_base_dir("/pre").unwrap();
+ resource.rebase_to_exclude_base_dir("abc").unwrap();
+ resource.rebase_to_include_base_dir("abc").unwrap();
+
+ if let Resource::Source(Source { iri, ..}) = resource {
+ assert_eq!(iri.as_str(), "path:abc/abc/def");
+ } else { unreachable!() }
+ }
+} \ No newline at end of file
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": ...}}}`)
+