summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPhilipp Korber <p.korber@1aim.com>2018-11-19 19:57:27 +0100
committerPhilipp Korber <p.korber@1aim.com>2018-11-19 20:13:59 +0100
commit43e697b166a0d7bbdab3ca6177357a1402c2c685 (patch)
treef852bbc38470ae0b876ffa2dbac101d4e886c180
parentbada4eb94b86f3aae99d4fcf2a504bd3dffe5f54 (diff)
chore(new-api) cleard thinks up/fixed imports
- still need to load inline_embeddings/attachments of Template (not data) on calling `load`
-rw-r--r--Cargo.toml8
-rw-r--r--examples/readme.rs6
-rw-r--r--src/additional_cid.rs60
-rw-r--r--src/base_dir.rs153
-rw-r--r--src/lib.rs290
-rw-r--r--src/path_rebase.rs188
-rw-r--r--src/serde_impl.rs175
7 files changed, 748 insertions, 132 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 8caa4cf..73ffb46 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,15 +15,15 @@ default = []
handlebars-bindings = []
[dependencies]
-mail-core = { git="https://github.com/1aim/mail" }
+mail-core = { git="https://github.com/1aim/mail", features=["serde-impl"] }
mail-internals = { git="https://github.com/1aim/mail" }
-mail-headers = { git="https://github.com/1aim/mail" }
+mail-headers = { git="https://github.com/1aim/mail", features=["serde-impl"] }
failure = "0.1.1"
futures = "0.1.14"
-vec1 = "1.0"
+vec1 = { version="1.1", features=["serde"]}
soft-ascii-string = "1.0"
-serde = { version="1", optional=true }
+serde = { version="1", features=["derive"] }
toml = "0.4.8"
galemu = "0.2.2"
diff --git a/examples/readme.rs b/examples/readme.rs
new file mode 100644
index 0000000..ea19f16
--- /dev/null
+++ b/examples/readme.rs
@@ -0,0 +1,6 @@
+
+
+
+fn main() {
+
+} \ No newline at end of file
diff --git a/src/additional_cid.rs b/src/additional_cid.rs
new file mode 100644
index 0000000..7be0786
--- /dev/null
+++ b/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 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/src/base_dir.rs b/src/base_dir.rs
new file mode 100644
index 0000000..e7072f1
--- /dev/null
+++ b/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/src/lib.rs b/src/lib.rs
index b1ffeb6..1864815 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,56 +1,85 @@
-use std::{mem, fs};
+extern crate failure;
+extern crate serde;
+extern crate futures;
+extern crate galemu;
+extern crate mail_core;
+extern crate mail_headers;
+extern crate vec1;
+extern crate toml;
+
+use std::{
+
+ fs,
+ collections::HashMap,
+ fmt::Debug,
+ path::Path,
+ sync::Arc
+};
+use serde::{
+ Serialize,
+ Deserialize
+};
use galemu::{Bound, BoundExt};
-use serde::Deserialize;
-use failure::Fail;
+use failure::{Fail, Error};
use futures::{
Future, Poll, Async,
try_ready,
- future::{
- self,
- JoinAll, Either, FutureResult
- }
+ future::Join
+};
+use vec1::Vec1;
+
+use mail_core::{
+ Resource,
+ Data, Metadata,
+ Context, ResourceContainerLoadingFuture,
+ compose::{MailParts, BodyPart},
+ Mail
+};
+use mail_headers::{
+ HeaderKind, Header,
+ header_components::MediaType,
+ headers
};
-use mail_base::Source;
-mod serde_impl;
+pub mod serde_impl;
mod base_dir;
mod path_rebase;
+mod additional_cid;
-pub use self::base_dir::CwdBaseDir;
-pub use self::path_rebase::PathRebaseable;
+pub use self::base_dir::*;
+pub use self::path_rebase::*;
+pub use self::additional_cid::*;
-pub trait TemplateEngine {
+pub trait TemplateEngine: Sized {
type Id: Debug;
type Error: Fail;
- type LazyBodyTemplate: PathRebaseable + Debug + for<'a> Deserialize<'a>;
+ type LazyBodyTemplate: PathRebaseable + Debug + Serialize + for<'a> Deserialize<'a>;
fn load_body_template(&mut self, tmpl: Self::LazyBodyTemplate)
- -> Result<BodyTemplate<Self>, TODO>;
+ -> Result<BodyTemplate<Self>, Error>;
fn load_subject_template(&mut self, template_string: String)
- -> Result<Self::Id, TODO>;
+ -> Result<Self::Id, Error>;
- pub fn load_template_from_path<P>(self, path: P) -> Result<Self, TODO>
+ fn load_toml_template_from_path<P>(self, path: P, ctx: &impl Context) -> Result<Template<Self>, Error>
where P: AsRef<Path>
{
let content = fs::read_to_string(path)?;
- //TODO choose serde serializer by file extension (toml, json)
- // then serialize to TemplateBase
- // then `base.with_engine(self)`
- load_template_from_str(&content)
+ self.load_toml_template_from_str(&content, ctx)
}
- pub fn load_template_from_str(self, desc: &str) -> Result<Self, TODO> {
- self::load_template::from_str(desc)
+ fn load_toml_template_from_str(self, desc: &str, ctx: &impl Context) -> Result<Template<Self>, Error> {
+ let base: serde_impl::TemplateBase<Self> = toml::from_str(desc)?;
+ Ok(base.load(self)?)
}
}
-pub struct PreparationData<'a, D: for<'a> BoundExt<'a>> {
+pub struct PreparationData<'a, PD: for<'any> BoundExt<'any>> {
pub attachments: Vec<Resource>,
pub inline_embeddings: HashMap<String, Resource>,
- pub prepared_data: Bound<'a, D>
+ pub prepared_data: Bound<'a, PD>
}
pub trait UseTemplateEngine<D>: TemplateEngine {
@@ -60,13 +89,13 @@ pub trait UseTemplateEngine<D>: TemplateEngine {
type PreparedData: for<'a> BoundExt<'a>;
//TODO[design]: allow returning a result
- fn prepare_data<'a>(raw: &'a D) -> PreparationData<'a, Self::PreparedData>;
+ fn prepare_data<'a>(&self, raw: &'a D) -> PreparationData<'a, Self::PreparedData>;
- fn render(
- &self,
- id: &Self::Id,
- data: &Bound<'a, Self::PreparedData>,
- additional_cids: AdditionalCids
+ fn render<'r, 'a>(
+ &'r self,
+ id: &'r Self::Id,
+ data: &'r Bound<'a, Self::PreparedData>,
+ additional_cids: AdditionalCIds<'r>
) -> Result<String, Self::Error>;
}
@@ -75,10 +104,43 @@ pub struct Template<TE: TemplateEngine> {
inner: Arc<InnerTemplate<TE>>
}
+impl<TE> Template<TE>
+ where TE: TemplateEngine
+{
+ pub fn inline_embeddings(&self) -> &HashMap<String, Resource> {
+ &self.inner.embeddings
+ }
+
+ pub fn attachments(&self) -> &[Resource] {
+ &self.inner.attachments
+ }
+
+ pub fn engine(&self) -> &TE {
+ &self.inner.engine
+ }
+
+ pub fn bodies(&self) -> &[BodyTemplate<TE>] {
+ &self.inner.bodies
+ }
+
+ pub fn subject_template_id(&self) -> &TE::Id {
+ &self.inner.subject.template_id
+ }
+}
+
+impl<TE> Clone for Template<TE>
+ where TE: TemplateEngine
+{
+ fn clone(&self) -> Self {
+ Template { inner: self.inner.clone() }
+ }
+}
+
+#[derive(Debug)]
struct InnerTemplate<TE: TemplateEngine> {
template_name: String,
base_dir: CwdBaseDir,
- subject: Subject,
+ 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.
@@ -89,45 +151,49 @@ struct InnerTemplate<TE: TemplateEngine> {
engine: TE,
}
-type Embeddings = HashMap<String, Resource>;
-type Attachments = Vec<Resource>;
-
-pub trait TemplateExt<D, TE> {
- fn prepare_to_render<C>(&self, data: &D, ctx: &C) -> RenderPreparationFuture<TE, D, C>;
+pub trait TemplateExt<TE, D>
+ where TE: TemplateEngine + UseTemplateEngine<D>
+{
+ fn prepare_to_render<'s, 'r, C>(&'s self, data: &'r D, ctx: &'s C) -> RenderPreparationFuture<'r, TE, D, C>
+ where C: Context;
}
-impl<D, TE> TemplateExt<D, TE> for Template<TE>
- where TE: UseTemplateEngine<D>
+impl<TE, D> TemplateExt<TE, D> for Template<TE>
+ where TE: TemplateEngine + UseTemplateEngine<D>
{
- fn prepare_to_render<: Context>(&self, data: &D, ctx: &C) ->
- MailPreparationFuture<D, TE, C>
+ fn prepare_to_render<'s, 'r, C>(&'s self, data: &'r D, ctx: &'s C) -> RenderPreparationFuture<'r, TE, D, C>
+ where C: Context
{
- let preps = self.engine.prepare_data(data);
+ let preps = self.inner.engine.prepare_data(data);
let PreparationData {
inline_embeddings,
attachments,
- prepare_data
- } = self;
+ prepared_data
+ } = preps;
let loading_fut = Resource::load_container(inline_embeddings, ctx)
.join(Resource::load_container(attachments, ctx));
RenderPreparationFuture {
- template: self.clone(),
- context: ctx.clone(),
- prepare_data,
+ payload: Some((
+ self.clone(),
+ prepared_data,
+ ctx.clone()
+ )),
loading_fut
}
}
}
-pub struct RenderPreparationFuture<TE, D, C> {
+pub struct RenderPreparationFuture<'a, TE, D, C>
+ where TE: TemplateEngine + UseTemplateEngine<D>, C: Context
+{
payload: Option<(
Template<TE>,
- <TE as UseTemplateEngine<D>>::PreparationData,
+ Bound<'a, <TE as UseTemplateEngine<D>>::PreparedData>,
C
)>,
loading_fut: Join<
@@ -136,17 +202,17 @@ pub struct RenderPreparationFuture<TE, D, C> {
>
}
-impl<TE,D,C> Future for RenderPreparationFuture<TE, D, C>
- TE: TemplateEngine, TE: UseTemplateEngine<D>, C: Context
+impl<'a, TE,D,C> Future for RenderPreparationFuture<'a, TE, D, C>
+ where TE: TemplateEngine, TE: UseTemplateEngine<D>, C: Context
{
- type Item = Preparations<TE, D, C>;
+ type Item = Preparations<'a, TE, D, C>;
type Error = Error;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
let (
inline_embeddings,
attachments
- ) = try_ready!(&mut self.loading_fut);
+ ) = try_ready!(self.loading_fut.poll());
//UNWRAP_SAFE only non if polled after resolved
let (template, prepared_data, ctx) = self.payload.take().unwrap();
@@ -161,41 +227,44 @@ impl<TE,D,C> Future for RenderPreparationFuture<TE, D, C>
}
}
-pub struct Preparations<TE, D, C> {
+pub struct Preparations<'a, TE, D, C>
+ where TE: TemplateEngine + UseTemplateEngine<D>, C: Context
+{
template: Template<TE>,
- prepared_data: <TE as UseTemplateEngine<D>>::PreparationData,
+ prepared_data: Bound<'a, <TE as UseTemplateEngine<D>>::PreparedData>,
ctx: C,
inline_embeddings: HashMap<String, Resource>,
- attachemnts: Vec<Resource>
+ attachments: Vec<Resource>
}
-impl<TE, D, C> Preparations<TE, D, C>
+impl<'a, TE, D, C> Preparations<'a, TE, D, C>
where TE: TemplateEngine, TE: UseTemplateEngine<D>, C: Context
{
- pub fn render_to_mail_parts(self) -> Result<MailParts, Error> {
+ pub fn render_to_mail_parts(self) -> Result<(MailParts, Header<headers::Subject>), Error> {
let Preparations {
template,
prepared_data,
ctx,
- //UPS thats a hash map not a Vec
- inline_embeddings: inline_embeddings_from_data,
- attachemnts
+ inline_embeddings,
+ mut attachments
} = self;
let subject = template.engine().render(
template.subject_template_id(),
- &prepare_data,
- AdditionalCids::new(&[])
+ &prepared_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 template.bodies().iter() {
- let raw = self.engine.render(
+ let raw = template.engine().render(
body.template_id(),
- &prepare_data,
- AdditionalCids::new(&[
- &inline_embeddings
+ &prepared_data,
+ AdditionalCIds::new(&[
+ &inline_embeddings,
body.inline_embeddings(),
template.inline_embeddings()
])
@@ -210,29 +279,43 @@ impl<TE, D, C> Preparations<TE, D, C>
}
);
- let inline_embeddings = body.embeddings()
+ let inline_embeddings = body.inline_embeddings()
.values()
.cloned()
.collect();
bodies.push(BodyPart {
- resource: Resource::Data(data)
- inline_embeddings
+ resource: Resource::Data(data),
+ inline_embeddings,
+ attachments: Vec::new()
});
}
- Ok(MailParts {
+ attachments.extend(template.attachments().iter().cloned());
+
+ let mut inline_embeddings_vec = Vec::new();
+ for (key, val) in template.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::new(bodies).unwrap(),
- inline_embeddings: template.embeddings().values().cloned().collect(),
- attachments: template.attachments().clone()
- })
+ alternative_bodies: Vec1::from_vec(bodies).unwrap(),
+ inline_embeddings: inline_embeddings_vec,
+ attachments
+ };
+
+ Ok((parts, subject))
}
pub fn render(self) -> Result<Mail, Error> {
- let parts = self.render_to_mail_parts()?;
- //PANIC_SAFE: templates load all data to at last the point where it has a content id.
- let mail = parts.compose_without_generating_content_ids()?;
+ let (parts, subject) = self.render_to_mail_parts()?;
+ let mut mail = parts.compose();
+ mail.insert_header(subject);
Ok(mail)
}
}
@@ -256,7 +339,7 @@ impl<TE> BodyTemplate<TE>
&self.media_type
}
- pub fn embeddings(&self) -> &HashMap<String, Resource> {
+ pub fn inline_embeddings(&self) -> &HashMap<String, Resource> {
&self.embeddings
}
}
@@ -272,53 +355,4 @@ impl<TE> Subject<TE>
pub fn template_id(&self) -> &TE::Id {
&self.template_id
}
-}
-
-//--------------------
-
-pub struct AdditionalCids<'a> {
- additional: &'a [&'a HashMap<String, Resource>]
-}
-
-
-
-
-// pub struct AdditionalCIds<'a> {
-// additional_resources: &'a [&'a HashMap<String, EmbeddedWithCId>]
-// }
-
-// impl<'a> AdditionalCIds<'a> {
-
-// pub fn new(additional_resources: &'a [&'a HashMap<String, EmbeddedWithCId>]) -> 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.
-// 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());
-// }
-// }
-// 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, v)| (k, v.content_id())))
-// .filter(|key| existing_keys.insert(key.to_owned()))
-// )
-// }
-// }
-
+} \ No newline at end of file
diff --git a/src/path_rebase.rs b/src/path_rebase.rs
new file mode 100644
index 0000000..f6fc967
--- /dev/null
+++ b/src/path_rebase.rs
@@ -0,0 +1,188 @@
+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::*;
+
+ #[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 = "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();
+
+ 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/src/serde_impl.rs b/src/serde_impl.rs
new file mode 100644
index 0000000..9282509
--- /dev/null
+++ b/src/serde_impl.rs
@@ -0,0 +1,175 @@
+use std::{
+ collections::HashMap,
+ sync::Arc
+};
+
+use serde::{
+ Serialize, Deserialize,
+ de::{
+ Deserializer,
+ },
+};
+use failure::Error;
+use vec1::Vec1;
+
+use mail_core::{Resource, Source, IRI};
+
+use super::{
+ Template,
+ TemplateEngine,
+ CwdBaseDir,
+ PathRebaseable,
+ InnerTemplate,
+ Subject,
+};
+
+/// 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,
+ base_dir: 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) -> Result<Template<TE>, Error> {
+ let TemplateBase {
+ template_name,
+ base_dir,
+ subject,
+ bodies,
+ mut embeddings,
+ mut attachments
+ } = self;
+
+ 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)?;
+ }
+
+
+ let inner = InnerTemplate {
+ template_name,
+ base_dir,
+ subject,
+ bodies,
+ embeddings,
+ attachments,
+ engine
+ };
+
+ Ok(Template { inner: Arc::new(inner) })
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+struct LazySubject {
+ #[serde(flatten)]
+ template_string: String
+}
+
+#[derive(Deserialize)]
+#[serde(untagged)]
+enum ResourceDeserializationHelper {
+ // Note: VARIANT ORDER MATTERS (serde,untagged)
+ // 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,