1use std::fs::File;
4use std::path::Path;
5
6use serde::Serialize;
7
8use crate::contexts::book::BookContext;
9use crate::models::entry::{Entries, Entry};
10use crate::result::Result;
11use crate::strings;
12
13const DIRECTORY_TEMPLATE: &str = "{{ book.author }} - {{ book.title }}";
17
18#[derive(Debug, Copy, Clone)]
20pub struct ExportRunner;
21
22impl ExportRunner {
23 pub fn run<O>(entries: &mut Entries, path: &Path, options: O) -> Result<()>
39 where
40 O: Into<ExportOptions>,
41 {
42 let options: ExportOptions = options.into();
43
44 Self::export(entries, path, options)?;
45
46 Ok(())
47 }
48
49 fn export(entries: &Entries, path: &Path, options: ExportOptions) -> Result<()> {
79 let directory_template = if let Some(template) = options.directory_template {
80 Self::validate_template(&template)?;
81 template
82 } else {
83 DIRECTORY_TEMPLATE.to_string()
84 };
85
86 for entry in entries.values() {
87 let directory_name = Self::render_directory_name(&directory_template, entry)?;
89
90 let item = path.join(directory_name);
92 let book_json = item.join("book").with_extension("json");
94 let annotations_json = item.join("annotations").with_extension("json");
96
97 std::fs::create_dir_all(&item)?;
98
99 if !options.overwrite_existing && book_json.exists() {
100 log::debug!("skipped writing {}", book_json.display());
101 } else {
102 let book_json = File::create(book_json)?;
103 serde_json::to_writer_pretty(&book_json, &entry.book)?;
104 }
105
106 if !options.overwrite_existing && annotations_json.exists() {
107 log::debug!("skipped writing {}", annotations_json.display());
108 } else {
109 let annotations_json = File::create(annotations_json)?;
110 serde_json::to_writer_pretty(&annotations_json, &entry.annotations)?;
111 }
112 }
113
114 Ok(())
115 }
116
117 fn validate_template(template: &str) -> Result<()> {
125 let entry = Entry::dummy();
126 Self::render_directory_name(template, &entry).map(|_| ())
127 }
128
129 fn render_directory_name(template: &str, entry: &Entry) -> Result<String> {
136 let context = BookContext::from(&entry.book);
137 let context = ExportContext::from(&context);
138 strings::render_and_sanitize(template, context)
139 }
140}
141
142#[derive(Debug)]
144pub struct ExportOptions {
145 pub directory_template: Option<String>,
147
148 pub overwrite_existing: bool,
150}
151
152#[derive(Debug, Serialize)]
156struct ExportContext<'a> {
157 book: &'a BookContext<'a>,
158}
159
160impl<'a> From<&'a BookContext<'a>> for ExportContext<'a> {
161 fn from(book: &'a BookContext<'a>) -> Self {
162 Self { book }
163 }
164}
165
166#[cfg(test)]
167mod test {
168
169 use super::*;
170
171 use crate::defaults::test::TemplatesDirectory;
172 use crate::models::book::Book;
173 use crate::render::engine::RenderEngine;
174 use crate::utils;
175
176 #[test]
178 fn default_template() {
179 let book = Book::default();
180 let context = BookContext::from(&book);
181 let context = ExportContext { book: &context };
182
183 RenderEngine::default()
184 .render_str(DIRECTORY_TEMPLATE, context)
185 .unwrap();
186 }
187
188 #[test]
190 fn valid_context() {
191 let template =
192 utils::testing::load_template_str(TemplatesDirectory::ValidContext, "valid-export.txt");
193
194 let book = Book::default();
195 let context = BookContext::from(&book);
196 let context = ExportContext::from(&context);
197
198 RenderEngine::default()
199 .render_str(&template, context)
200 .unwrap();
201 }
202
203 #[test]
205 #[should_panic(expected = "Failed to render '__tera_one_off'")]
206 fn invalid_context() {
207 let template = utils::testing::load_template_str(
208 TemplatesDirectory::InvalidContext,
209 "invalid-export.txt",
210 );
211
212 let book = Book::default();
213 let context = BookContext::from(&book);
214 let context = ExportContext::from(&context);
215
216 RenderEngine::default()
217 .render_str(&template, context)
218 .unwrap();
219 }
220}