diff options
author | Philipp Korber <p.korber@1aim.com> | 2018-11-19 19:57:27 +0100 |
---|---|---|
committer | Philipp Korber <p.korber@1aim.com> | 2018-11-19 20:13:59 +0100 |
commit | 43e697b166a0d7bbdab3ca6177357a1402c2c685 (patch) | |
tree | f852bbc38470ae0b876ffa2dbac101d4e886c180 | |
parent | bada4eb94b86f3aae99d4fcf2a504bd3dffe5f54 (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.toml | 8 | ||||
-rw-r--r-- | examples/readme.rs | 6 | ||||
-rw-r--r-- | src/additional_cid.rs | 60 | ||||
-rw-r--r-- | src/base_dir.rs | 153 | ||||
-rw-r--r-- | src/lib.rs | 290 | ||||
-rw-r--r-- | src/path_rebase.rs | 188 | ||||
-rw-r--r-- | src/serde_impl.rs | 175 |
7 files changed, 748 insertions, 132 deletions
@@ -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 @@ -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 ResourceDeserializationH |