use anyhow::{Result, anyhow};
use clap::{Args, ValueEnum};
use itertools::Itertools;
use pspp::{
data::cases_to_output,
output::{Item, Text, drivers::Driver, pivot::PivotTable},
por::PortableFile,
};
use serde::Serialize;
use std::{cell::RefCell, fmt::Display, fs::File, io::BufReader, path::PathBuf, rc::Rc, sync::Arc};
#[derive(Args, Clone, Debug)]
pub struct ShowPor {
#[arg(value_enum)]
mode: Mode,
#[arg(required = true)]
input: PathBuf,
output: Option<PathBuf>,
#[arg(
long = "data",
num_args = 0..=1,
default_missing_value = "18446744073709551615",
default_value_t = 0,
help_heading = "Input file options"
)]
max_cases: usize,
#[arg(short = 'o', help_heading = "Output options")]
output_options: Vec<String>,
}
struct Output {
driver: Rc<RefCell<Box<dyn Driver>>>,
mode: Mode,
}
impl Output {
fn can_show_json(&self) -> bool {
self.driver.borrow().can_serialize()
}
fn show_json<T>(&self, value: &T) -> Result<()>
where
T: Serialize,
{
let mut driver = self.driver.borrow_mut();
if driver.can_serialize() {
driver.serialize(value);
Ok(())
} else {
Err(anyhow!(
"Mode '{}' only supports output as JSON.",
self.mode
))
}
}
fn warn(&self, warning: &impl Display) {
let mut driver = self.driver.borrow_mut();
#[derive(Serialize)]
struct Warning {
warning: String,
}
let w = Warning {
warning: warning.to_string(),
};
if driver.can_serialize() {
driver.serialize(&w);
} else {
driver.write(&Arc::new(Item::from(Text::new_log(warning.to_string()))));
}
}
}
impl ShowPor {
pub fn run(self) -> Result<()> {
let output = Output {
mode: self.mode,
driver: Rc::new(RefCell::new(Box::new(<dyn Driver>::from_options(
self.output.as_ref(),
&self.output_options,
"json",
)?))),
};
let reader = BufReader::new(File::open(&self.input)?);
match self.mode {
Mode::Dictionary => {
let PortableFile {
dictionary,
metadata: _,
cases,
} = PortableFile::open(reader, |warning| output.warn(&warning))?;
let cases = cases.take(self.max_cases);
if output.can_show_json() {
output.show_json(&dictionary)?;
for (_index, case) in (0..self.max_cases).zip(cases) {
output.show_json(&case?)?;
}
} else {
let mut items = Vec::new();
items.extend(dictionary.all_pivot_tables().into_iter().map_into());
items.extend(cases_to_output(&dictionary, cases));
output
.driver
.borrow_mut()
.write(&Arc::new(items.into_iter().collect()));
}
}
Mode::Metadata => {
let metadata =
PortableFile::open(reader, |warning| output.warn(&warning))?.metadata;
if output.can_show_json() {
output.show_json(&metadata)?;
} else {
output
.driver
.borrow_mut()
.write(&Arc::new(PivotTable::from(&metadata).into()));
}
}
Mode::Histogram => {
let (histogram, translations) = PortableFile::read_histogram(reader)?;
let h = histogram
.into_iter()
.enumerate()
.filter_map(|(index, count)| {
if count > 0
&& index != translations[index as u8] as usize
&& translations[index as u8] != 0
{
Some((
format!("{index:02x}"),
translations[index as u8] as char,
count,
))
} else {
None
}
})
.collect::<Vec<_>>();
output.show_json(&h)?;
}
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, ValueEnum)]
enum Mode {
#[default]
#[value(alias = "dict")]
Dictionary,
Metadata,
Histogram,
}
impl Mode {
fn as_str(&self) -> &'static str {
match self {
Mode::Dictionary => "dictionary",
Mode::Metadata => "metadata",
Mode::Histogram => "histogram",
}
}
}
impl Display for Mode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}