diff options
Diffstat (limited to 'template/src/lib.rs')
-rw-r--r-- | template/src/lib.rs | 411 |
1 files changed, 411 insertions, 0 deletions
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))) + } +} |