mail_template/
serde_impl.rs

1use std::{
2    collections::HashMap,
3    path::{Path, PathBuf},
4    mem
5};
6
7use serde::{
8    Serialize, Deserialize,
9    Serializer, Deserializer
10};
11use failure::Error;
12use futures::{Future, future::{self, Either}};
13use vec1::Vec1;
14
15use mail_core::{Resource, Source, IRI, Context};
16use mail_headers::header_components::MediaType;
17
18use super::{
19    Template,
20    TemplateEngine,
21    CwdBaseDir,
22    PathRebaseable,
23    Subject,
24    UnsupportedPathError,
25};
26
27/// Type used when deserializing a template using serde.
28///
29/// This type should only be used as intermediate type
30/// used for deserialization as templates need to be
31/// bundled with a template engine.
32///
33/// # Serialize/Deserialize
34///
35/// The derserialization currently only works with
36/// self-describing data formats.
37///
38/// There are a number of shortcuts to deserialize
39/// resources (from emebddings and/or attachments):
40///
41/// - Resources can be deserialized normally (from a externally tagged enum)
42/// - Resources can be deserialized from the serialized repr of `Source`
43/// - Resources can be deserialized from a string which is used to create
44///   a `Resource::Source` with a iri using the `path` scheme and the string
45///   content as the iris "tail".
46#[derive(Debug, Serialize, Deserialize)]
47pub struct TemplateBase<TE: TemplateEngine> {
48    #[serde(rename="name")]
49    template_name: String,
50    #[serde(default)]
51    base_dir: Option<CwdBaseDir>,
52    subject: LazySubject,
53    bodies: Vec1<TE::LazyBodyTemplate>,
54    #[serde(default)]
55    #[serde(deserialize_with="deserialize_embeddings")]
56    embeddings: HashMap<String, Resource>,
57    #[serde(default)]
58    #[serde(deserialize_with="deserialize_attachments")]
59    attachments: Vec<Resource>,
60}
61
62impl<TE> TemplateBase<TE>
63    where TE: TemplateEngine
64{
65
66    //TODO!! make this load all embeddings/attachments and make it a future
67    /// Couples the template base with a specific engine instance.``
68    pub fn load(self, mut engine: TE, default_base_dir: CwdBaseDir, ctx: &impl Context) -> impl Future<Item=Template<TE>, Error=Error> {
69        let TemplateBase {
70            template_name,
71            base_dir,
72            subject,
73            bodies,
74            mut embeddings,
75            mut attachments
76        } = self;
77
78        let base_dir = base_dir.unwrap_or(default_base_dir);
79
80        //FIXME[rust/catch block] use catch block
81        let catch_res = (|| -> Result<_, Error> {
82            let subject = Subject{ template_id: engine.load_subject_template(subject.template_string)? };
83
84            let bodies = bodies.try_mapped(|mut lazy_body| -> Result<_, Error> {
85                lazy_body.rebase_to_include_base_dir(&base_dir)?;
86                Ok(engine.load_body_template(lazy_body)?)
87            })?;
88
89            for embedding in embeddings.values_mut() {
90                embedding.rebase_to_include_base_dir(&base_dir)?;
91            }
92
93            for attachment in attachments.iter_mut() {
94                attachment.rebase_to_include_base_dir(&base_dir)?;
95            }
96
97            Ok((subject, bodies))
98        })();
99
100        let (subject, mut bodies) =
101            match catch_res {
102                Ok(vals) => vals,
103                Err(err) => { return Either::B(future::err(err)); }
104            };
105
106        let loading_embeddings = Resource::load_container(embeddings, ctx);
107        let loading_attachments = Resource::load_container(attachments, ctx);
108        let loading_body_embeddings = bodies.iter_mut()
109            .map(|body| {
110                //Note: empty HashMap does not alloc!
111                let body_embeddings = mem::replace(&mut body.inline_embeddings, HashMap::new());
112                Resource::load_container(body_embeddings, ctx)
113            })
114            .collect::<Vec<_>>();
115        let loading_body_embeddings = future::join_all(loading_body_embeddings);
116
117
118        let fut = loading_embeddings
119            .join3(loading_attachments, loading_body_embeddings)
120            .map_err(Error::from)
121            .map(|(embeddings, attachments, body_embeddings)| {
122                for (body, loaded_embeddings) in bodies.iter_mut().zip(body_embeddings) {
123                    mem::replace(&mut body.inline_embeddings, loaded_embeddings);
124                }
125                Template {
126                    template_name,
127                    base_dir,
128                    subject,
129                    bodies,
130                    embeddings,
131                    attachments,
132                    engine
133                }
134            });
135
136        Either::A(fut)
137    }
138}
139
140#[derive(Debug)]
141struct LazySubject {
142    template_string: String
143}
144
145impl Serialize for LazySubject {
146
147    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
148        where S: Serializer
149    {
150        serializer.serialize_str(&self.template_string)
151    }
152}
153
154impl<'de> Deserialize<'de> for LazySubject {
155
156    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
157        where D: Deserializer<'de>
158    {
159        let template_string = String::deserialize(deserializer)?;
160        Ok(LazySubject { template_string })
161    }
162}
163
164#[derive(Deserialize)]
165#[serde(untagged)]
166enum ResourceDeserializationHelper {
167    // This allows specifying resources in three ways.
168    // 1. as tagged enum `Resource` (e.g. `{"Source": { "iri": ...}}}`)
169    // 2. as struct `Source` (e.g. `{"iri": ...}` )
170    // 3. as String which is interpreted as path iri
171    Normal(Resource),
172    FromSource(Source),
173    FromString(String)
174}
175
176impl Into<Resource> for ResourceDeserializationHelper {
177    fn into(self) -> Resource {
178        use self::ResourceDeserializationHelper::*;
179        match self {
180            Normal(resource) => resource,
181            FromString(string) => {
182                let source = Source {
183                    //UNWRAP_SAFE: only scheme validation could fail,
184                    // but its static "path" which is known to be valid
185                    iri: IRI::from_parts("path", &string).unwrap(),
186                    use_media_type: Default::default(),
187                    use_file_name: Default::default()
188                };
189
190                Resource::Source(source)
191            },
192            FromSource(source) => Resource::Source(source)
193        }
194    }
195}
196
197pub fn deserialize_embeddings<'de, D>(deserializer: D)
198    -> Result<HashMap<String, Resource>, D::Error>
199    where D: Deserializer<'de>
200{
201    //FIXME[perf] write custom visitor etc.
202    let map = <HashMap<String, ResourceDeserializationHelper>>
203        ::deserialize(deserializer)?;
204
205    let map = map.into_iter()
206        .map(|(k, helper)| (k, helper.into()))
207        .collect();
208
209    Ok(map)
210}
211
212pub fn deserialize_attachments<'de, D>(deserializer: D)
213    -> Result<Vec<Resource>, D::Error>
214    where D: Deserializer<'de>
215{
216    //FIXME[perf] write custom visitor etc.
217    let vec = <Vec<ResourceDeserializationHelper>>
218        ::deserialize(deserializer)?;
219
220    let vec = vec.into_iter()
221        .map(|helper| helper.into())
222        .collect();
223
224    Ok(vec)
225}
226
227//TODO make base dir default to the dir the template file is in if it's parsed from a template file.
228
229/// Common implementation for a type for [`TemplateEngine::LazyBodyTemplate`].
230///
231/// This impl. gives bodies a field `embeddings` which is a mapping of embedding
232/// names to embeddings (using `deserialize_embeddings`) a `path` field which
233/// allows specifying the template file (e.g. `"body.html"`) and can be relative
234/// to the base dir.
235#[derive(Debug, Serialize)]
236pub struct StandardLazyBodyTemplate {
237    pub path: PathBuf,
238    pub embeddings: HashMap<String, Resource>,
239    pub media_type: Option<MediaType>
240}
241
242
243impl PathRebaseable for StandardLazyBodyTemplate {
244    fn rebase_to_include_base_dir(&mut self, base_dir: impl AsRef<Path>)
245        -> Result<(), UnsupportedPathError>
246    {
247        let base_dir = base_dir.as_ref();
248        self.path.rebase_to_include_base_dir(base_dir)?;
249        for embedding in self.embeddings.values_mut() {
250            embedding.rebase_to_include_base_dir(base_dir)?;
251        }
252        Ok(())
253    }
254
255    fn rebase_to_exclude_base_dir(&mut self, base_dir: impl AsRef<Path>)
256        -> Result<(), UnsupportedPathError>
257    {
258        let base_dir = base_dir.as_ref();
259        self.path.rebase_to_exclude_base_dir(base_dir)?;
260        for embedding in self.embeddings.values_mut() {
261            embedding.rebase_to_exclude_base_dir(base_dir)?;
262        }
263        Ok(())
264    }
265}
266
267
268#[derive(Deserialize)]
269#[serde(untagged)]
270enum StandardLazyBodyTemplateDeserializationHelper {
271    ShortForm(String),
272    LongForm {
273        path: PathBuf,
274        #[serde(default)]
275        #[serde(deserialize_with="deserialize_embeddings")]
276        embeddings: HashMap<String, Resource>,
277        #[serde(default)]
278        media_type: Option<MediaType>
279    }
280}
281
282impl<'de> Deserialize<'de> for StandardLazyBodyTemplate {
283    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
284        where D: Deserializer<'de>
285    {
286        use self::StandardLazyBodyTemplateDeserializationHelper::*;
287        let helper = StandardLazyBodyTemplateDeserializationHelper::deserialize(deserializer)?;
288        let ok_val =
289            match helper {
290                ShortForm(string) => {
291                    StandardLazyBodyTemplate {
292                        path: string.into(),
293                        embeddings: Default::default(),
294                        media_type: Default::default()
295                    }
296                },
297                LongForm {path, embeddings, media_type} =>
298                    StandardLazyBodyTemplate { path, embeddings, media_type }
299            };
300        Ok(ok_val)
301    }
302}
303
304
305#[cfg(test)]
306mod test {
307    use toml;
308    use super::*;
309
310    fn test_source_iri(resource: &Resource, iri: &str) {
311        if let &Resource::Source(ref source) = resource {
312            assert_eq!(source.iri.as_str(), iri);
313        } else {
314            panic!("unexpected resource expected resource with source and iri {:?} but got {:?}", iri, resource);
315        }
316    }
317
318    mod attachment_deserialization {
319        use super::*;
320        use super::super::deserialize_attachments;
321
322        #[derive(Serialize, Deserialize)]
323        struct Wrapper {
324            #[serde(deserialize_with="deserialize_attachments")]
325            attachments: Vec<Resource>
326        }
327
328
329        #[test]
330        fn should_deserialize_from_strings() {
331            let raw_toml = r#"
332                attachments = ["notes.md", "pic.xd"]
333            "#;
334
335            let Wrapper { attachments } = toml::from_str(raw_toml).unwrap();
336
337            assert_eq!(attachments.len(), 2);
338            test_source_iri(&attachments[0], "path:notes.md");
339            test_source_iri(&attachments[1], "path:pic.xd");
340        }
341
342        #[test]
343        fn should_deserialize_from_sources() {
344            let raw_toml = r#"
345                [[attachments]]
346                Source = {iri="https://fun.example"}
347                [[attachments]]
348                iri="path:pic.xd"
349            "#;
350
351            let Wrapper { attachments } = toml::from_str(raw_toml).unwrap();
352
353            assert_eq!(attachments.len(), 2);
354            test_source_iri(&attachments[0], "https://fun.example");
355            test_source_iri(&attachments[1], "path:pic.xd");
356        }
357
358        #[test]
359        fn check_if_data_is_deserializable_like_expected() {
360            use mail_core::Data;
361
362            let raw_toml = r#"
363                media_type = "text/plain; charset=utf-8"
364                buffer = [65,65,65,66,65]
365                content_id = "c0rc3rcr0q0v32@example.example"
366            "#;
367
368            let data: Data = toml::from_str(raw_toml).unwrap();
369
370            assert_eq!(data.content_id().as_str(), "c0rc3rcr0q0v32@example.example");
371            assert_eq!(&**data.buffer(), b"AAABA" as &[u8]);
372        }
373
374        #[test]
375        fn should_deserialize_from_data() {
376            let raw_toml = r#"
377                [[attachments]]
378                [attachments.Data]
379                media_type = "text/plain; charset=utf-8"
380                buffer = [65,65,65,66,65]
381                content_id = "c0rc3rcr0q0v32@example.example"
382            "#;
383
384            let Wrapper { attachments } = toml::from_str(raw_toml).unwrap();
385
386            assert_eq!(attachments.len(), 1);
387        }
388    }
389
390    mod embedding_deserialization {
391        use super::*;
392        use super::super::deserialize_embeddings;
393
394        #[derive(Serialize, Deserialize)]
395        struct Wrapper {
396            #[serde(deserialize_with="deserialize_embeddings")]
397            embeddings: HashMap<String, Resource>
398        }
399
400        #[test]
401        fn should_deserialize_with_short_forms() {
402            let raw_toml = r#"
403                [embeddings]
404                pic = "hy-ya"
405                pic2 = { iri = "path:ay-ya" }
406                [embeddings.pic3.Data]
407                media_type = "text/plain; charset=utf-8"
408                buffer = [65,65,65,66,65]
409                content_id = "c0rc3rcr0q0v32@example.example"
410                [embeddings.pic4.Source]
411                iri = "path:nay-nay-way"
412            "#;
413
414            let Wrapper { embeddings } = toml::from_str(raw_toml).unwrap();
415
416            assert_eq!(embeddings.len(), 4);
417            assert!(embeddings.contains_key("pic"));
418            assert!(embeddings.contains_key("pic2"));
419            assert!(embeddings.contains_key("pic3"));
420            assert!(embeddings.contains_key("pic4"));
421            test_source_iri(&embeddings["pic"], "path:hy-ya");
422            test_source_iri(&embeddings["pic2"], "path:ay-ya");
423            test_source_iri(&embeddings["pic4"], "path:nay-nay-way");
424            assert_eq!(embeddings["pic3"].content_id().unwrap().as_str(), "c0rc3rcr0q0v32@example.example");
425        }
426    }
427
428    #[allow(non_snake_case)]
429    mod StandardLazyBodyTemplate {
430        use super::*;
431        use super::super::StandardLazyBodyTemplate;
432
433        #[derive(Serialize, Deserialize)]
434        struct Wrapper {
435            body: StandardLazyBodyTemplate
436        }
437
438        #[test]
439        fn should_deserialize_from_string() {
440            let toml_str = r#"
441                body = "template.html.hbs"
442            "#;
443
444            let Wrapper { body } = toml::from_str(toml_str).unwrap();
445            assert_eq!(body.path.to_str().unwrap(), "template.html.hbs");
446            assert_eq!(body.embeddings.len(), 0);
447        }
448
449        #[test]
450        fn should_deserialize_from_object_without_embeddings() {
451            let toml_str = r#"
452                body = { path="t.d" }
453            "#;
454
455            let Wrapper { body }= toml::from_str(toml_str).unwrap();
456            assert_eq!(body.path.to_str().unwrap(), "t.d");
457            assert_eq!(body.embeddings.len(), 0);
458        }
459
460        #[test]
461        fn should_deserialize_from_object_with_empty_embeddings() {
462            let toml_str = r#"
463                body = { path="t.d", embeddings={} }
464            "#;
465
466            let Wrapper { body } = toml::from_str(toml_str).unwrap();
467            assert_eq!(body.path.to_str().unwrap(), "t.d");
468            assert_eq!(body.embeddings.len(), 0);
469        }
470
471        #[test]
472        fn should_deserialize_from_object_with_short_from_embeddings() {
473            let toml_str = r#"
474                body = { path="t.d", embeddings={ pic1="the_embeddings" } }
475            "#;
476
477            let Wrapper { body } = toml::from_str(toml_str).unwrap();
478            assert_eq!(body.path.to_str().unwrap(), "t.d");
479            assert_eq!(body.embeddings.len(), 1);
480
481            let (key, resource) = body.embeddings.iter().next().unwrap();
482            assert_eq!(key, "pic1");
483
484            test_source_iri(resource, "path:the_embeddings");
485        }
486    }
487}