diff options
Diffstat (limited to 'template/src')
-rw-r--r-- | template/src/additional_cid.rs | 60 | ||||
-rw-r--r-- | template/src/base_dir.rs | 153 | ||||
-rw-r--r-- | template/src/handlebars.rs | 96 | ||||
-rw-r--r-- | template/src/lib.rs | 411 | ||||
-rw-r--r-- | template/src/path_rebase.rs | 191 | ||||
-rw-r--r-- | template/src/serde_impl.rs | 447 |
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": ...}}}`) + |