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#[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 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 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 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 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 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 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 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#[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}