summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPhilipp Korber <philippkorber@gmail.com>2018-05-31 16:42:46 +0200
committerPhilipp Korber <philippkorber@gmail.com>2018-05-31 16:42:46 +0200
commit96602c3da0eed6afe8279c9d6dc2d0717ed553f2 (patch)
treeaa544e1105c0c6e998fb5ee859a5e432130289c5
parent16e57311934a4256b0b6d6f83be55b714bf9ac0f (diff)
chore(askama): added support for askama engine
-rw-r--r--Cargo.toml6
-rw-r--r--src/askama_engine/error.rs38
-rw-r--r--src/askama_engine/mod.rs249
-rw-r--r--src/lib.rs10
4 files changed, 300 insertions, 3 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 3f46951..004196a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -32,10 +32,14 @@ git="https://github.com/1aim/mime"
branch="parser_revamp"
version="0.4.0"
+[dependencies.askama]
+version = "0.6.4"
+optional = true
[dev-dependencies]
serde_json = "1.0.2"
regex = "0.2.6"
[features]
-default = [] \ No newline at end of file
+default = []
+askama_engine = ["askama"] \ No newline at end of file
diff --git a/src/askama_engine/error.rs b/src/askama_engine/error.rs
new file mode 100644
index 0000000..8edffd9
--- /dev/null
+++ b/src/askama_engine/error.rs
@@ -0,0 +1,38 @@
+use std::fmt::{self, Debug, Display};
+use std::sync::Mutex;
+use askama;
+
+/// a wrapper needed as askama is not Sync
+#[derive(Fail)]
+pub struct AskamaError {
+ inner: Mutex<askama::Error>
+}
+
+impl AskamaError {
+
+ pub fn inner(&self) -> &Mutex<askama::Error> {
+ &self.inner
+ }
+}
+
+const POISON_MSG: &str = "<Debug/Display of crate::askama::Error paniced previously>";
+macro_rules! impl_fmt {
+ ($($trait:ident),*) => ($(
+ impl $trait for AskamaError {
+ fn fmt(&self, fter: &mut fmt::Formatter) -> fmt::Result {
+ match self.inner.lock() {
+ Ok(inner_error) => $trait::fmt(&*inner_error, fter),
+ Err(_err) => write!(fter, "{}", POISON_MSG)
+ }
+ }
+ }
+ )*);
+}
+
+impl_fmt!(Debug, Display);
+
+impl From<askama::Error> for AskamaError {
+ fn from(err: askama::Error) -> Self {
+ AskamaError { inner: Mutex::new(err) }
+ }
+} \ No newline at end of file
diff --git a/src/askama_engine/mod.rs b/src/askama_engine/mod.rs
new file mode 100644
index 0000000..ca064dd
--- /dev/null
+++ b/src/askama_engine/mod.rs
@@ -0,0 +1,249 @@
+use vec1::Vec1;
+
+use askama;
+use headers::components::MediaType;
+use mail::{Resource, Context};
+
+use ::{TemplateEngine, MailParts, BodyPart, EmbeddedWithCId};
+mod error;
+pub use self::error::*;
+
+pub trait AskamaMailTemplate: askama::Template {
+
+ fn media_type(&self) -> MediaType;
+
+ /// Implement this to have alternate bodies, e.g. a alternate text body for an html body
+ ///
+ /// A simple way to bind another template to an data type is by wrapping a reference of
+ /// the original type into it.
+ ///
+ /// # Example
+ ///
+ /// ```rust
+ /// # #[macro_use]
+ /// # extern crate askama;
+ /// # extern crate mail_template;
+ /// # extern crate mail_headers;
+ /// # mod mail { pub use mail_template::askama_engine as askama; pub use mail_headers::components::MediaType; }
+ /// use std::ops::Deref;
+ /// use std::borrow::Cow;
+ /// use askama::Template;
+ /// use mail::askama::AskamaMailTemplate;
+ /// use mail::MediaType;
+ ///
+ /// #[derive(Template)]
+ /// #[template(source = "<h2>Hy {{ name }}</h2>", ext="html")]
+ /// struct HtmlHy {
+ /// name: &'static str
+ /// }
+ ///
+ /// impl AskamaMailTemplate for HtmlHy {
+ /// fn media_type(&self) -> MediaType {
+ /// MediaType::parse("text/html; charset=utf-8").unwrap()
+ /// }
+ ///
+ /// fn alternate_template<'a>(&'a self) -> Option<Box<AskamaMailTemplate + 'a>> {
+ /// // theoretically we could circumvent the boxing by returning a &Trait
+ /// // but this would require transmuting `&HtmlHy` to `&TextHy` so we don't
+ /// // do this
+ /// Some(Box::new(TextHy(self)))
+ /// }
+ /// }
+ ///
+ /// #[derive(Template)]
+ /// #[template(source = "Hy {{ name }}, use html please", ext="txt")]
+ /// struct TextHy<'a>(&'a HtmlHy);
+ ///
+ /// /// we implement deref so that we can use the fields
+ /// /// of `HtmlHy` without indirection, e.g. use `name`
+ /// /// instead of `inner.name`
+ /// impl<'a> Deref for TextHy<'a> {
+ /// type Target = HtmlHy;
+ ///
+ /// fn deref(&self) -> &Self::Target {
+ /// self.0
+ /// }
+ /// }
+ ///
+ /// impl<'a> AskamaMailTemplate for TextHy<'a> {
+ /// fn media_type(&self) -> MediaType {
+ /// MediaType::parse("text/plain; charset=utf-8").unwrap()
+ /// }
+ /// }
+ ///
+ /// fn main() {
+ /// let hy = HtmlHy { name: "Liz" };
+ ///
+ /// let rendered = hy.render().unwrap();
+ /// assert_eq!(rendered, "<h2>Hy Liz</h2>");
+ /// let rendered = hy.alternate_template().unwrap().render().unwrap();
+ /// assert_eq!(rendered, "Hy Liz, use html please");
+ /// }
+ /// ```
+ ///
+ fn alternate_template<'a>(&'a self) -> Option<Box<AskamaMailTemplate + 'a>> {
+ None
+ }
+
+ fn attachments(&self) -> Vec<Resource> {
+ Vec::new()
+ }
+}
+
+
+pub struct AskamaTemplateEngine;
+
+
+impl<C, D> TemplateEngine<C, D> for AskamaTemplateEngine
+ where C: Context, D: AskamaMailTemplate
+{
+ type TemplateId = ();
+ type Error = AskamaError;
+
+ fn use_template(&self, _id: &(), data: &D, ctx: &C) -> Result<MailParts, Self::Error> {
+ let mut state = State::new(ctx);
+ state.render_bodies::<Self::Error>(data)?;
+ let (alternative_bodies, attachments) = state.destruct();
+
+ Ok(MailParts {
+ alternative_bodies,
+ attachments,
+ shared_embeddings: Vec::new(),
+ })
+ }
+}
+
+struct State<'a, C: 'a> {
+ ctx: &'a C,
+ bodies: Vec<BodyPart>,
+ attachments: Vec<EmbeddedWithCId>
+}
+
+
+impl<'a, C: 'a> State<'a, C>
+ where C: Context
+{
+ fn new(ctx: &'a C) -> Self {
+ State {
+ ctx,
+ bodies: Vec::new(),
+ attachments: Vec::new()
+ }
+ }
+
+ fn render_bodies<E>(
+ &mut self,
+ template: &AskamaMailTemplate,
+ ) -> Result<(), E>
+ where E: From<askama::Error>
+ {
+ let string = template.render()?;
+ let media_type = template.media_type();
+ let resource = Resource::sourceless(media_type, string);
+ self.bodies.push(BodyPart {
+ body_resource: resource,
+ embeddings: Vec::new()
+ });
+
+ for attachment in template.attachments() {
+ self.attachments.push(EmbeddedWithCId::attachment(attachment, self.ctx));
+ }
+
+ let sub = template.alternate_template();
+ if let Some(alt) = sub {
+ self.render_bodies::<E>(&*alt)?;
+ }
+ Ok(())
+ }
+
+ /// # Panics
+ ///
+ /// if render_bodies was not called at last once successfully
+ fn destruct(self) -> (Vec1<BodyPart>, Vec<EmbeddedWithCId>) {
+ let State { bodies, attachments, ctx:_ } = self;
+ let bodies = Vec1::from_vec(bodies).expect("[BUG] should have at last one body");
+ (bodies, attachments)
+ }
+}
+
+
+#[cfg(test)]
+mod test {
+
+ use std::ops::Deref;
+ use askama::Template;
+ use super::*;
+ //TODO test with alternate bodies and attachments
+
+ struct Person {
+ name: &'static str,
+ name_prefix: &'static str
+ }
+
+ #[derive(Template)]
+ #[template(source="<h2>Dear {{name_prefix}} {{name}}</h2>", ext="html")]
+ // #[askama_mail(media_type = "text/html; charset=utf-8")]
+ // #[askama_mail(alternate=TextGreeting)]
+ struct HtmlGreeting<'a> {
+ person: &'a mut Person
+ }
+
+ impl<'a> Deref for HtmlGreeting<'a> {
+ type Target = Person;
+
+ fn deref(&self) -> &Self::Target {
+ self.person
+ }
+ }
+
+ impl<'a> AskamaMailTemplate for HtmlGreeting<'a> {
+ fn media_type(&self) -> MediaType {
+ MediaType::parse("text/html; charset=utf-8").unwrap()
+ }
+
+ fn attachments(&self) -> Vec<Resource> {
+ vec![ Resource::sourceless_from_string("hy"), Resource::sourceless_from_string("ho") ]
+ }
+
+ fn alternate_template<'e>(&'e self) -> Option<Box<AskamaMailTemplate + 'e>> {
+ Some(Box::new(TextGreeting::from(self)))
+ }
+ }
+
+
+
+ #[derive(Template)]
+ #[template(source="Dear {{name_prefix}} {{name}}", ext="txt")]
+ // #[askama_mail(media_type = "text/plain; charset=utf-8")]
+ // #[askama_mail(wraps=HtmlGreeting)]
+ struct TextGreeting<'a> {
+ inner: &'a HtmlGreeting<'a>
+ }
+
+ impl<'a> AskamaMailTemplate for TextGreeting<'a> {
+ fn media_type(&self) -> MediaType {
+ MediaType::parse("text/plain; charset=utf-8").unwrap()
+ }
+
+ fn attachments(&self) -> Vec<Resource> {
+ vec![ Resource::sourceless_from_string("so") ]
+ }
+ }
+
+ //auto-gen from wraps
+ impl<'a> Deref for TextGreeting<'a> {
+ type Target = HtmlGreeting<'a>;
+
+ fn deref(&self) -> &Self::Target {
+ self.inner
+ }
+ }
+
+ //auto-gen from wraps
+ impl<'a> From<&'a HtmlGreeting<'a>> for TextGreeting<'a> {
+ fn from(inner: &'a HtmlGreeting<'a>) -> Self {
+ TextGreeting { inner }
+ }
+ }
+
+} \ No newline at end of file
diff --git a/src/lib.rs b/src/lib.rs
index 4000a00..3fb2191 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -16,6 +16,9 @@ extern crate chrono;
#[macro_use]
extern crate vec1;
+#[cfg(feature="askama_engine")]
+#[cfg_attr(test, macro_use)]
+extern crate askama;
#[macro_use]
#[allow(unused_imports)]
@@ -31,8 +34,11 @@ mod template_engine;
mod builder_extension;
mod compositor;
-// reexports
-pub use self::builder_extension::BuilderExt;
+#[cfg(feature="askama_engine")]
+pub mod askama_engine;
+
+// re-exports flatten crate
+pub use self::builder_extension::*;
pub use self::compositor::*;
pub use self::resource::*;
pub use self::template_engine::*; \ No newline at end of file