use std::{borrow::Cow, fmt::Write, path::Path, sync::Arc};
use anyhow::{anyhow, bail};
use serde::{Deserialize, Serialize};
use crate::{
data::{ByteString, Case, Datum},
dictionary::Dictionary,
};
use super::{Item, page::PageSetup};
pub mod cairo;
use cairo::{CairoConfig, CairoDriver};
pub mod csv;
use csv::{CsvConfig, CsvDriver};
pub mod html;
use html::{HtmlConfig, HtmlDriver};
pub mod json;
use json::{JsonConfig, JsonDriver};
pub mod por;
use por::{PorConfig, PorDriver};
pub mod sav;
use sav::{SavConfig, SavDriver};
pub mod spv;
use spv::{SpvConfig, SpvDriver};
pub mod text;
use text::{TextConfig, TextDriver};
pub trait Driver {
fn name(&self) -> Cow<'static, str>;
fn write(&mut self, item: &Arc<Item>);
fn can_serialize(&self) -> bool {
false
}
fn serialize(&mut self, item: &dyn erased_serde::Serialize) {
let _ = item;
unreachable!("This driver does not support serialization");
}
fn can_write_data_file(&self) -> bool {
false
}
fn write_data_file<'a>(
&'a mut self,
dictionary: &'a Dictionary,
) -> anyhow::Result<Option<Box<dyn CaseWriter + 'a>>> {
let _ = dictionary;
Ok(None)
}
fn setup(&mut self, page_setup: &PageSetup) -> bool {
let _ = page_setup;
false
}
fn flush(&mut self) {}
fn handles_show(&self) -> bool {
false
}
fn handles_groups(&self) -> bool {
false
}
}
impl Driver for Box<dyn Driver> {
fn name(&self) -> Cow<'static, str> {
(**self).name()
}
fn write(&mut self, item: &Arc<Item>) {
(**self).write(item);
}
fn setup(&mut self, page_setup: &PageSetup) -> bool {
(**self).setup(page_setup)
}
fn flush(&mut self) {
(**self).flush();
}
fn handles_show(&self) -> bool {
(**self).handles_show()
}
fn handles_groups(&self) -> bool {
(**self).handles_groups()
}
fn can_serialize(&self) -> bool {
(**self).can_serialize()
}
fn serialize(&mut self, item: &dyn erased_serde::Serialize) {
(**self).serialize(item);
}
fn can_write_data_file(&self) -> bool {
(**self).can_write_data_file()
}
fn write_data_file<'a>(
&'a mut self,
dictionary: &'a Dictionary,
) -> anyhow::Result<Option<Box<dyn CaseWriter + 'a>>> {
(**self).write_data_file(dictionary)
}
}
pub trait CaseWriter {
fn write_case(&mut self, case: Case<Vec<Datum<ByteString>>>) -> anyhow::Result<()>;
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "driver", rename_all = "snake_case")]
pub enum Config {
Text(TextConfig),
Pdf(CairoConfig),
Html(HtmlConfig),
Json(JsonConfig),
Csv(CsvConfig),
Por(PorConfig),
Sav(SavConfig),
Spv(SpvConfig),
}
impl dyn Driver {
pub fn from_options<P>(
file: Option<P>,
options: &[String],
default_driver: &str,
) -> anyhow::Result<Box<Self>>
where
P: AsRef<Path>,
{
let mut config = String::new();
for option in options {
writeln!(&mut config, "{option}").unwrap();
}
let mut config: toml::Table = toml::from_str(&config)?;
let file = file.as_ref().map(|p| p.as_ref());
if let Some(file) = file {
let Some(file) = file.to_str() else {
bail!("{}: not a valid UTF-8 filename", file.display())
};
config.insert(String::from("file"), toml::Value::String(file.into()));
}
if !config.contains_key("driver") {
let driver = if let Some(file) = file {
<dyn Driver>::driver_type_from_filename(file).ok_or_else(|| {
anyhow!("{}: no default output format for file name", file.display())
})?
} else {
default_driver
};
config.insert(String::from("driver"), toml::Value::String(driver.into()));
}
Self::new(&Config::deserialize(config)?)
}
pub fn new(config: &Config) -> anyhow::Result<Box<Self>> {
match config {
Config::Csv(csv_config) => Ok(Box::new(CsvDriver::new(csv_config)?)),
Config::Html(html_config) => Ok(Box::new(HtmlDriver::new(html_config)?)),
Config::Json(json_config) => Ok(Box::new(JsonDriver::new(json_config)?)),
Config::Pdf(cairo_config) => Ok(Box::new(CairoDriver::new(cairo_config)?)),
Config::Por(por_config) => Ok(Box::new(PorDriver::new(por_config)?)),
Config::Sav(sav_config) => Ok(Box::new(SavDriver::new(sav_config)?)),
Config::Spv(spv_config) => Ok(Box::new(SpvDriver::new(spv_config)?)),
Config::Text(text_config) => Ok(Box::new(TextDriver::new(text_config)?)),
}
}
pub fn driver_type_from_filename(file: impl AsRef<Path>) -> Option<&'static str> {
match file.as_ref().extension()?.to_str()? {
"txt" | "text" => Some("text"),
"pdf" => Some("pdf"),
"htm" | "html" => Some("html"),
"csv" => Some("csv"),
"json" | "ndjson" => Some("json"),
"spv" => Some("spv"),
"sav" => Some("sav"),
"por" => Some("por"),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use serde::Serialize;
use crate::output::drivers::Config;
#[test]
fn toml() {
let config = r#"driver = "text"
file = "filename.text"
"#;
let toml: Config = toml::from_str(config).unwrap();
println!("{}", toml::to_string_pretty(&toml).unwrap());
#[derive(Serialize)]
struct Map<'a> {
file: &'a str,
}
println!(
"{}",
toml::to_string_pretty(&Map { file: "filename" }).unwrap()
);
}
}