use std::fs::File;
use std::path::Path;
use serde::Serialize;
use crate::contexts::book::BookContext;
use crate::models::entry::{Entries, Entry};
use crate::result::Result;
use crate::strings;
const DIRECTORY_TEMPLATE: &str = "{{ book.author }} - {{ book.title }}";
#[derive(Debug, Copy, Clone)]
pub struct ExportRunner;
impl ExportRunner {
pub fn run<O>(entries: &mut Entries, path: &Path, options: O) -> Result<()>
where
O: Into<ExportOptions>,
{
let options: ExportOptions = options.into();
Self::export(entries, path, options)?;
Ok(())
}
fn export(entries: &Entries, path: &Path, options: ExportOptions) -> Result<()> {
let directory_template = if let Some(template) = options.directory_template {
Self::validate_template(&template)?;
template
} else {
DIRECTORY_TEMPLATE.to_string()
};
for entry in entries.values() {
let directory_name = Self::render_directory_name(&directory_template, entry)?;
let item = path.join(directory_name);
let book_json = item.join("book").with_extension("json");
let annotations_json = item.join("annotations").with_extension("json");
std::fs::create_dir_all(&item)?;
if !options.overwrite_existing && book_json.exists() {
log::debug!("skipped writing {}", book_json.display());
} else {
let book_json = File::create(book_json)?;
serde_json::to_writer_pretty(&book_json, &entry.book)?;
}
if !options.overwrite_existing && annotations_json.exists() {
log::debug!("skipped writing {}", annotations_json.display());
} else {
let annotations_json = File::create(annotations_json)?;
serde_json::to_writer_pretty(&annotations_json, &entry.annotations)?;
}
}
Ok(())
}
fn validate_template(template: &str) -> Result<()> {
let entry = Entry::dummy();
Self::render_directory_name(template, &entry).map(|_| ())
}
fn render_directory_name(template: &str, entry: &Entry) -> Result<String> {
let context = BookContext::from(&entry.book);
let context = ExportContext::from(&context);
strings::render_and_sanitize(template, context)
}
}
#[derive(Debug)]
pub struct ExportOptions {
pub directory_template: Option<String>,
pub overwrite_existing: bool,
}
#[derive(Debug, Serialize)]
struct ExportContext<'a> {
book: &'a BookContext<'a>,
}
impl<'a> From<&'a BookContext<'a>> for ExportContext<'a> {
fn from(book: &'a BookContext<'a>) -> Self {
Self { book }
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::defaults::test::TemplatesDirectory;
use crate::models::book::Book;
use crate::render::engine::RenderEngine;
use crate::utils;
#[test]
fn default_template() {
let book = Book::default();
let context = BookContext::from(&book);
let context = ExportContext { book: &context };
RenderEngine::default()
.render_str(DIRECTORY_TEMPLATE, context)
.unwrap();
}
#[test]
fn valid_context() {
let template =
utils::testing::load_template_str(TemplatesDirectory::ValidContext, "valid-export.txt");
let book = Book::default();
let context = BookContext::from(&book);
let context = ExportContext::from(&context);
RenderEngine::default()
.render_str(&template, context)
.unwrap();
}
#[test]
#[should_panic(expected = "Failed to render '__tera_one_off'")]
fn invalid_context() {
let template = utils::testing::load_template_str(
TemplatesDirectory::InvalidContext,
"invalid-export.txt",
);
let book = Book::default();
let context = BookContext::from(&book);
let context = ExportContext::from(&context);
RenderEngine::default()
.render_str(&template, context)
.unwrap();
}
}