From 75de77c2cc3a91e8a2b70e4a254e14e35228b853 Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Thu, 14 Aug 2025 16:36:37 -0700 Subject: [PATCH] add json output driver --- rust/pspp/Cargo.toml | 2 +- rust/pspp/src/dictionary.rs | 2 +- rust/pspp/src/format/mod.rs | 10 +- rust/pspp/src/main.rs | 54 ++++++- rust/pspp/src/message.rs | 14 +- rust/pspp/src/output/csv.rs | 10 +- rust/pspp/src/output/driver.rs | 5 + rust/pspp/src/output/json.rs | 58 +++++++ rust/pspp/src/output/mod.rs | 9 +- rust/pspp/src/output/pivot/mod.rs | 257 ++++++++++++++++++++++++++---- rust/pspp/src/output/spv.rs | 6 +- rust/pspp/src/settings.rs | 4 +- rust/pspp/src/variable.rs | 17 +- 13 files changed, 383 insertions(+), 65 deletions(-) create mode 100644 rust/pspp/src/output/json.rs diff --git a/rust/pspp/Cargo.toml b/rust/pspp/Cargo.toml index a6c91db728..f9e7b4d093 100644 --- a/rust/pspp/Cargo.toml +++ b/rust/pspp/Cargo.toml @@ -33,7 +33,7 @@ smallstr = "0.3.0" itertools = "0.14.0" unicode-linebreak = "0.1.5" quick-xml = { version = "0.37.2", features = ["serialize"] } -serde = { version = "1.0.218", features = ["derive"] } +serde = { version = "1.0.218", features = ["derive", "rc"] } color = { version = "0.2.3", features = ["serde"] } binrw = "0.14.1" ndarray = "0.16.1" diff --git a/rust/pspp/src/dictionary.rs b/rust/pspp/src/dictionary.rs index a1f35907fb..923f289b3e 100644 --- a/rust/pspp/src/dictionary.rs +++ b/rust/pspp/src/dictionary.rs @@ -400,7 +400,7 @@ impl Dictionary { } else if index < end { None } else { - Some(index - end - start) + Some(index - (end - start)) } }) } diff --git a/rust/pspp/src/format/mod.rs b/rust/pspp/src/format/mod.rs index b651ab743a..2fe6e982dd 100644 --- a/rust/pspp/src/format/mod.rs +++ b/rust/pspp/src/format/mod.rs @@ -896,7 +896,7 @@ impl Not for Decimal { } } -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)] pub struct Epoch(pub i32); impl Epoch { @@ -935,7 +935,7 @@ impl Display for Epoch { } } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Serialize)] pub struct Settings { pub epoch: Epoch, @@ -1061,7 +1061,7 @@ impl Settings { /// A numeric output style. This can express numeric formats in /// [Category::Basic] and [Category::Custom]. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct NumberStyle { pub neg_prefix: Affix, pub prefix: Affix, @@ -1084,6 +1084,7 @@ pub struct NumberStyle { /// can be used to size memory allocations: for example, the formatted /// result of `CCA20.5` requires no more than `(20 + extra_bytes)` bytes in /// UTF-8. + #[serde(skip)] pub extra_bytes: usize, } @@ -1138,11 +1139,12 @@ impl NumberStyle { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct Affix { /// String contents of affix. pub s: String, + #[serde(skip)] /// Display width in columns (see [unicode_width]) pub width: usize, } diff --git a/rust/pspp/src/main.rs b/rust/pspp/src/main.rs index dacee9fcee..19907f1741 100644 --- a/rust/pspp/src/main.rs +++ b/rust/pspp/src/main.rs @@ -25,7 +25,9 @@ use pspp::{ }, sys::{ self, - raw::{infer_encoding, records::Compression, Decoder, Magic, Reader, Record}, + raw::{ + infer_encoding, records::Compression, Decoder, EncodingReport, Magic, Reader, Record, + }, ReadOptions, Records, }, }; @@ -254,7 +256,7 @@ struct Show { mode: Mode, /// Output format. - #[arg(long)] + #[arg(long, short = 'f')] format: Option, /// The encoding to use. @@ -271,6 +273,7 @@ enum Output { writer: Rc>>, pretty: bool, }, + Discard, } impl Output { @@ -291,6 +294,7 @@ impl Output { writeln!(writer)?; Ok(()) } + Self::Discard => Ok(()), } } @@ -311,6 +315,7 @@ impl Output { }; let _ = self.show_json(&warning); } + Self::Discard => (), } } } @@ -330,6 +335,8 @@ impl Show { "ndjson" => ShowFormat::Ndjson, _ => ShowFormat::Output, } + } else if self.mode == Mode::Encodings { + ShowFormat::Output } else { ShowFormat::Json }; @@ -354,13 +361,14 @@ impl Show { } let table: toml::Table = toml::from_str(&config)?; - if !table.contains_key("driver") - && let Some(file) = &self.output_file - { - let driver = + if !table.contains_key("driver") { + let driver = if let Some(file) = &self.output_file { ::driver_type_from_filename(file).ok_or_else(|| { anyhow!("{}: no default output format for file name", file.display()) - })?; + })? + } else { + "text" + }; #[derive(Serialize)] struct DriverConfig { @@ -386,6 +394,7 @@ impl Show { Rc::new(RefCell::new(Box::new(stdout()))) }, }, + ShowFormat::Discard => Output::Discard, }; let reader = File::open(&self.input_file)?; @@ -459,6 +468,30 @@ impl Show { output.show_json(&case?)?; } } + Output::Discard => (), + } + } + Mode::Encodings => { + let mut record_strings = reader.header().get_strings(); + for record in reader.records() { + record_strings.append(&mut record?.get_strings()); + } + let Some(encoding_report) = EncodingReport::new(&record_strings) else { + output.warn(&"No valid encodings found."); + return Ok(()); + }; + match &output { + Output::Driver { driver, .. } => { + driver + .borrow_mut() + .write(&Arc::new(Item::new(encoding_report.valid_encodings))); + if let Some(interpretations) = encoding_report.interpretations { + driver + .borrow_mut() + .write(&Arc::new(Item::new(interpretations))); + } + } + _ => todo!(), } } } @@ -495,13 +528,14 @@ fn parse_encoding(arg: &str) -> Result<&'static Encoding, UnknownEncodingError> } } -#[derive(Clone, Copy, Debug, Default, ValueEnum)] +#[derive(Clone, Copy, Debug, Default, PartialEq, ValueEnum)] enum Mode { Identify, Raw, Decoded, #[default] Parsed, + Encodings, } impl Mode { @@ -511,6 +545,7 @@ impl Mode { Mode::Raw => "raw", Mode::Decoded => "decoded", Mode::Parsed => "parsed", + Mode::Encodings => "encodings", } } } @@ -529,7 +564,10 @@ enum ShowFormat { Json, /// Newline-delimited JSON. Ndjson, + /// Pivot tables. Output, + /// No output. + Discard, } fn main() -> Result<()> { diff --git a/rust/pspp/src/message.rs b/rust/pspp/src/message.rs index 3d0f667b57..97bcc90b84 100644 --- a/rust/pspp/src/message.rs +++ b/rust/pspp/src/message.rs @@ -22,10 +22,11 @@ use std::{ }; use enum_map::Enum; +use serde::Serialize; use unicode_width::UnicodeWidthStr; /// A line number and optional column number within a source file. -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)] pub struct Point { /// 1-based line number. pub line: i32, @@ -65,7 +66,7 @@ impl Point { } /// Location relevant to an diagnostic message. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Serialize)] pub struct Location { /// File name, if any. pub file_name: Option>, @@ -76,6 +77,7 @@ pub struct Location { /// Normally, if `span` contains column information, then displaying the /// message will underline the location. Setting this to true disables /// displaying underlines. + #[serde(skip)] pub omit_underlines: bool, } @@ -136,7 +138,8 @@ impl Location { } } -#[derive(Copy, Clone, Debug, PartialEq, Eq, Enum)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Enum, Serialize)] +#[serde(rename_all = "snake_case")] pub enum Severity { Error, Warning, @@ -167,13 +170,15 @@ impl Display for Severity { } } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] pub enum Category { General, Syntax, Data, } +#[derive(Serialize)] pub struct Stack { location: Location, description: String, @@ -188,6 +193,7 @@ impl From for Diagnostics { } } +#[derive(Serialize)] pub struct Diagnostic { pub severity: Severity, pub category: Category, diff --git a/rust/pspp/src/output/csv.rs b/rust/pspp/src/output/csv.rs index b776856680..5e65b0b75e 100644 --- a/rust/pspp/src/output/csv.rs +++ b/rust/pspp/src/output/csv.rs @@ -18,7 +18,7 @@ use std::{ borrow::Cow, fmt::Display, fs::File, - io::{Error, Write}, + io::{BufWriter, Error, Write}, path::PathBuf, sync::Arc, }; @@ -40,7 +40,7 @@ pub struct CsvConfig { } pub struct CsvDriver { - file: File, + file: BufWriter, options: CsvOptions, /// Number of items written so far. @@ -130,7 +130,7 @@ impl Display for CsvField<'_> { impl CsvDriver { pub fn new(config: &CsvConfig) -> std::io::Result { Ok(Self { - file: File::create(&config.file)?, + file: BufWriter::new(File::create(&config.file)?), options: config.options.clone(), n_items: 0, }) @@ -201,7 +201,7 @@ impl Driver for CsvDriver { Details::Message(diagnostic) => { self.start_item(); let text = diagnostic.to_string(); - writeln!(&self.file, "{}", CsvField::new(&text, self.options)).unwrap(); + writeln!(&mut self.file, "{}", CsvField::new(&text, self.options)).unwrap(); } Details::Table(pivot_table) => { for layer in pivot_table.layers(true) { @@ -217,7 +217,7 @@ impl Driver for CsvDriver { TextType::Title | TextType::Log => { self.start_item(); for line in text.content.display(()).to_string().lines() { - writeln!(&self.file, "{}", CsvField::new(line, self.options)).unwrap(); + writeln!(&mut self.file, "{}", CsvField::new(line, self.options)).unwrap(); } } }, diff --git a/rust/pspp/src/output/driver.rs b/rust/pspp/src/output/driver.rs index 0d722e1f63..963661146e 100644 --- a/rust/pspp/src/output/driver.rs +++ b/rust/pspp/src/output/driver.rs @@ -23,6 +23,7 @@ use crate::output::{ cairo::{CairoConfig, CairoDriver}, csv::{CsvConfig, CsvDriver}, html::{HtmlConfig, HtmlDriver}, + json::{JsonConfig, JsonDriver}, spv::{SpvConfig, SpvDriver}, text::{TextConfig, TextDriver}, }; @@ -96,6 +97,7 @@ pub enum Config { Text(TextConfig), Pdf(CairoConfig), Html(HtmlConfig), + Json(JsonConfig), Csv(CsvConfig), Spv(SpvConfig), } @@ -107,6 +109,7 @@ pub enum DriverType { Pdf, Html, Csv, + Json, Spv, } @@ -117,6 +120,7 @@ impl dyn Driver { Config::Pdf(cairo_config) => Ok(Box::new(CairoDriver::new(cairo_config)?)), Config::Html(html_config) => Ok(Box::new(HtmlDriver::new(html_config)?)), Config::Csv(csv_config) => Ok(Box::new(CsvDriver::new(csv_config)?)), + Config::Json(json_config) => Ok(Box::new(JsonDriver::new(json_config)?)), Config::Spv(spv_config) => Ok(Box::new(SpvDriver::new(spv_config)?)), } } @@ -127,6 +131,7 @@ impl dyn Driver { "pdf" => Some("pdf"), "htm" | "html" => Some("html"), "csv" => Some("csv"), + "json" => Some("json"), "spv" => Some("spv"), _ => None, } diff --git a/rust/pspp/src/output/json.rs b/rust/pspp/src/output/json.rs new file mode 100644 index 0000000000..af6923d390 --- /dev/null +++ b/rust/pspp/src/output/json.rs @@ -0,0 +1,58 @@ +// PSPP - a program for statistical analysis. +// Copyright (C) 2025 Free Software Foundation, Inc. +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// this program. If not, see . + +use std::{ + borrow::Cow, + fs::File, + io::{BufWriter, Write}, + path::PathBuf, + sync::Arc, +}; + +use serde::{Deserialize, Serialize}; + +use super::{driver::Driver, Item}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct JsonConfig { + file: PathBuf, +} + +pub struct JsonDriver { + file: BufWriter, +} + +impl JsonDriver { + pub fn new(config: &JsonConfig) -> std::io::Result { + Ok(Self { + file: BufWriter::new(File::create(&config.file)?), + }) + } +} + +impl Driver for JsonDriver { + fn name(&self) -> Cow<'static, str> { + Cow::from("json") + } + + fn write(&mut self, item: &Arc) { + serde_json::to_writer_pretty(&mut self.file, item).unwrap(); // XXX handle errors + } + + fn flush(&mut self) { + let _ = self.file.flush(); + } +} diff --git a/rust/pspp/src/output/mod.rs b/rust/pspp/src/output/mod.rs index 5417de68b7..0779129f8e 100644 --- a/rust/pspp/src/output/mod.rs +++ b/rust/pspp/src/output/mod.rs @@ -22,6 +22,7 @@ use std::{ use enum_map::EnumMap; use pivot::PivotTable; +use serde::Serialize; use crate::{ message::Diagnostic, @@ -34,6 +35,7 @@ pub mod cairo; pub mod csv; pub mod driver; pub mod html; +pub mod json; pub mod page; pub mod pivot; pub mod render; @@ -43,6 +45,7 @@ pub mod text; pub mod text_line; /// A single output item. +#[derive(Serialize)] pub struct Item { /// The localized label for the item that appears in the outline pane in the /// output viewer and in PDF outlines. This is `None` if no label has been @@ -94,6 +97,7 @@ where } } +#[derive(Serialize)] pub enum Details { Chart, Image, @@ -180,7 +184,7 @@ impl From> for Details { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct Text { type_: TextType, @@ -228,7 +232,8 @@ impl From<&Diagnostic> for Text { } } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] pub enum TextType { /// `TITLE` and `SUBTITLE` commands. PageTitle, diff --git a/rust/pspp/src/output/pivot/mod.rs b/rust/pspp/src/output/pivot/mod.rs index 99d7271070..39645987bc 100644 --- a/rust/pspp/src/output/pivot/mod.rs +++ b/rust/pspp/src/output/pivot/mod.rs @@ -60,7 +60,7 @@ use enum_iterator::Sequence; use enum_map::{enum_map, Enum, EnumMap}; use look_xml::TableProperties; use quick_xml::{de::from_str, DeError}; -use serde::{de::Visitor, Deserialize, Serialize}; +use serde::{de::Visitor, ser::SerializeStruct, Deserialize, Serialize}; use smallstr::SmallString; use smallvec::SmallVec; use thiserror::Error as ThisError; @@ -102,6 +102,31 @@ pub enum Area { Layers, } +impl Display for Area { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Area::Title => write!(f, "title"), + Area::Caption => write!(f, "caption"), + Area::Footer => write!(f, "footer"), + Area::Corner => write!(f, "corner"), + Area::Labels(axis2) => write!(f, "labels({axis2})"), + Area::Data => write!(f, "data"), + Area::Layers => write!(f, "layers"), + } + } +} + +impl Serialize for Area { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut s = SmallString::<[u8; 16]>::new(); + write!(&mut s, "{}", self).unwrap(); + serializer.serialize_str(&s) + } +} + impl Area { fn default_cell_style(self) -> CellStyle { use HorzAlign::*; @@ -187,8 +212,34 @@ impl Border { } } +impl Display for Border { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Border::Title => write!(f, "title"), + Border::OuterFrame(box_border) => write!(f, "outer_frame({box_border})"), + Border::InnerFrame(box_border) => write!(f, "inner_frame({box_border})"), + Border::Dimension(row_col_border) => write!(f, "dimension({row_col_border})"), + Border::Category(row_col_border) => write!(f, "category({row_col_border})"), + Border::DataLeft => write!(f, "data(left)"), + Border::DataTop => write!(f, "data(top)"), + } + } +} + +impl Serialize for Border { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut s = SmallString::<[u8; 32]>::new(); + write!(&mut s, "{}", self).unwrap(); + serializer.serialize_str(&s) + } +} + /// The borders on a box. -#[derive(Copy, Clone, Debug, Enum, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Enum, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] pub enum BoxBorder { Left, Top, @@ -196,8 +247,26 @@ pub enum BoxBorder { Bottom, } +impl BoxBorder { + fn as_str(&self) -> &'static str { + match self { + BoxBorder::Left => "left", + BoxBorder::Top => "top", + BoxBorder::Right => "right", + BoxBorder::Bottom => "bottom", + } + } +} + +impl Display for BoxBorder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + /// Borders between rows and columns. -#[derive(Copy, Clone, Debug, Enum, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Enum, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] pub struct RowColBorder( /// Row or column headings. pub HeadingRegion, @@ -205,11 +274,17 @@ pub struct RowColBorder( pub Axis2, ); +impl Display for RowColBorder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.0, self.1) + } +} + /// Sizing for rows or columns of a rendered table. /// /// The comments below talk about columns and their widths but they apply /// equally to rows and their heights. -#[derive(Default, Clone, Debug)] +#[derive(Default, Clone, Debug, Serialize)] pub struct Sizing { /// Specific column widths, in 1/96" units. widths: Vec, @@ -222,7 +297,8 @@ pub struct Sizing { keeps: Vec>, } -#[derive(Copy, Clone, Debug, Enum, PartialEq, Eq, Sequence)] +#[derive(Copy, Clone, Debug, Enum, PartialEq, Eq, Sequence, Serialize)] +#[serde(rename_all = "snake_case")] pub enum Axis3 { X, Y, @@ -249,7 +325,7 @@ impl From for Axis3 { } /// An axis within a pivot table. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Serialize)] pub struct Axis { /// `dimensions[0]` is the innermost dimension. pub dimensions: Vec, @@ -338,7 +414,7 @@ impl PivotTable { /// (A dimension or a group can contain zero categories, but this is unusual. /// If a dimension contains no categories, then its table cannot contain any /// data.) -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct Dimension { /// Hierarchy of categories within the dimension. The groups and categories /// are sorted in the order that should be used for display. This might be @@ -399,8 +475,9 @@ impl Dimension { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct Group { + #[serde(skip)] len: usize, pub name: Box, @@ -505,7 +582,7 @@ where } } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Serialize)] pub struct Footnotes(pub Vec>); impl Footnotes { @@ -540,6 +617,15 @@ impl Leaf { } } +impl Serialize for Leaf { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.name.serialize(serializer) + } +} + /// Pivot result classes. /// /// These are used to mark [Leaf] categories as having particular types of data, @@ -556,7 +642,7 @@ pub enum Class { } /// A pivot_category is a leaf (a category) or a group. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub enum Category { Group(Group), Leaf(Leaf), @@ -658,7 +744,7 @@ impl From<&String> for Category { /// The division between this and the style information in [PivotTable] seems /// fairly arbitrary. The ultimate reason for the division is simply because /// that's how SPSS documentation and file formats do it. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct Look { pub name: Option, @@ -789,7 +875,7 @@ impl Look { } /// Position for group labels. -#[derive(Copy, Clone, Debug, Default, Deserialize, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] pub enum LabelPosition { /// Hierarachically enclosing the categories. /// @@ -840,12 +926,28 @@ pub enum LabelPosition { /// │ │ │ /// └──────────────────┴─────────────────────────────────────────────────┘ /// ``` -#[derive(Copy, Clone, Debug, PartialEq, Eq, Enum)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Enum, Serialize)] +#[serde(rename_all = "snake_case")] pub enum HeadingRegion { Rows, Columns, } +impl HeadingRegion { + pub fn as_str(&self) -> &'static str { + match self { + HeadingRegion::Rows => "rows", + HeadingRegion::Columns => "columns", + } + } +} + +impl Display for HeadingRegion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + impl From for HeadingRegion { fn from(axis: Axis2) -> Self { match axis { @@ -855,13 +957,13 @@ impl From for HeadingRegion { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct AreaStyle { pub cell_style: CellStyle, pub font_style: FontStyle, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct CellStyle { /// `None` means "mixed" alignment: align strings to the left, numbers to /// the right. @@ -908,7 +1010,8 @@ impl HorzAlign { } } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] pub enum VertAlign { /// Top alignment. Top, @@ -920,7 +1023,7 @@ pub enum VertAlign { Bottom, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct FontStyle { pub bold: bool, pub italic: bool, @@ -1014,6 +1117,17 @@ impl FromStr for Color { } } +impl Serialize for Color { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut s = SmallString::<[u8; 32]>::new(); + write!(&mut s, "{}", self.display_css()).unwrap(); + serializer.serialize_str(&s) + } +} + impl<'de> Deserialize<'de> for Color { fn deserialize(deserializer: D) -> Result where @@ -1061,6 +1175,18 @@ pub struct BorderStyle { pub color: Color, } +impl Serialize for BorderStyle { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut s = serializer.serialize_struct("BorderStyle", 2)?; + s.serialize_field("stroke", &self.stroke)?; + s.serialize_field("color", &self.color)?; + s.end() + } +} + impl BorderStyle { pub const fn none() -> Self { Self { @@ -1084,7 +1210,7 @@ impl BorderStyle { } } -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Enum, Deserialize)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Enum, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub enum Stroke { None, @@ -1120,6 +1246,19 @@ impl Axis2 { pub fn new_enum(x: T, y: T) -> EnumMap { EnumMap::from_array([x, y]) } + + pub fn as_str(&self) -> &'static str { + match self { + Axis2::X => "x", + Axis2::Y => "y", + } + } +} + +impl Display for Axis2 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } } impl Not for Axis2 { @@ -1251,7 +1390,7 @@ impl IndexMut for Rect2 { } } -#[derive(Copy, Clone, Debug, Default, Deserialize, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub enum FootnoteMarkerType { /// a, b, c, ... @@ -1262,7 +1401,7 @@ pub enum FootnoteMarkerType { Numeric, } -#[derive(Copy, Clone, Debug, Default, Deserialize, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub enum FootnoteMarkerPosition { /// Subscripts. @@ -1324,7 +1463,7 @@ impl IntoValueOptions for ValueOptions { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct PivotTable { pub look: Arc, @@ -1679,8 +1818,9 @@ where } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct Footnote { + #[serde(skip)] index: usize, pub content: Box, pub marker: Option>, @@ -1850,6 +1990,15 @@ pub struct Value { pub styling: Option>, } +impl Serialize for Value { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.inner.serialize(serializer) + } +} + impl Value { fn new(inner: ValueInner) -> Self { Self { @@ -1932,9 +2081,9 @@ impl Value { } else { Self::new(ValueInner::Text(TextValue { user_provided: true, - local: s.clone(), - c: s.clone(), - id: s.clone(), + localized: s.clone(), + c: None, + id: None, })) } } @@ -2253,7 +2402,9 @@ impl Display for DisplayValue<'_> { } } - ValueInner::Text(TextValue { local, .. }) => { + ValueInner::Text(TextValue { + localized: local, .. + }) => { /* if self .inner @@ -2304,7 +2455,7 @@ impl Debug for Value { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct NumberValue { pub show: Option, pub format: Format, @@ -2314,7 +2465,7 @@ pub struct NumberValue { pub value_label: Option, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct StringValue { pub show: Option, pub hex: bool, @@ -2326,7 +2477,7 @@ pub struct StringValue { pub value_label: Option, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct VariableValue { pub show: Option, pub var_name: String, @@ -2337,21 +2488,59 @@ pub struct VariableValue { pub struct TextValue { pub user_provided: bool, /// Localized. - pub local: String, + pub localized: String, /// English. - pub c: String, + pub c: Option, /// Identifier. - pub id: String, + pub id: Option, } -#[derive(Clone, Debug)] +impl Serialize for TextValue { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if self.user_provided && self.c.is_none() && self.id.is_none() { + serializer.serialize_str(&self.localized) + } else { + let mut s = serializer.serialize_struct( + "TextValue", + 2 + self.c.is_some() as usize + self.id.is_some() as usize, + )?; + s.serialize_field("user_provided", &self.user_provided)?; + s.serialize_field("localized", &self.localized)?; + if let Some(c) = &self.c { + s.serialize_field("c", &c)?; + } + if let Some(id) = &self.id { + s.serialize_field("id", &id)?; + } + s.end() + } + } +} + +impl TextValue { + pub fn localized(&self) -> &str { + self.localized.as_str() + } + pub fn c(&self) -> &str { + self.c.as_ref().unwrap_or(&self.localized).as_str() + } + pub fn id(&self) -> &str { + self.id.as_ref().unwrap_or(&self.localized).as_str() + } +} + +#[derive(Clone, Debug, Serialize)] pub struct TemplateValue { pub args: Vec>, pub local: String, pub id: String, } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Serialize)] +#[serde(rename_all = "snake_case")] pub enum ValueInner { Number(NumberValue), String(StringValue), diff --git a/rust/pspp/src/output/spv.rs b/rust/pspp/src/output/spv.rs index 3255953453..21854df5ef 100644 --- a/rust/pspp/src/output/spv.rs +++ b/rust/pspp/src/output/spv.rs @@ -1331,10 +1331,10 @@ impl BinWrite for Value { ValueInner::Text(text) => { ( 3u8, - SpvString(&text.local), + SpvString(&text.localized), ValueMod::new(self), - SpvString(&text.id), - SpvString(&text.c), + SpvString(text.id()), + SpvString(text.c()), SpvBool(true), ) .write_options(writer, endian, args)?; diff --git a/rust/pspp/src/settings.rs b/rust/pspp/src/settings.rs index 7c7678c743..aac4a4c8a8 100644 --- a/rust/pspp/src/settings.rs +++ b/rust/pspp/src/settings.rs @@ -18,6 +18,7 @@ use std::sync::{Arc, OnceLock}; use binrw::Endian; use enum_map::EnumMap; +use serde::Serialize; use crate::{ format::{Format, Settings as FormatSettings}, @@ -27,7 +28,8 @@ use crate::{ /// Whether to show variable or value labels or the underlying value or variable /// name. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] pub enum Show { /// Value (or variable name) only. Value, diff --git a/rust/pspp/src/variable.rs b/rust/pspp/src/variable.rs index 01913dfd0f..738732dff1 100644 --- a/rust/pspp/src/variable.rs +++ b/rust/pspp/src/variable.rs @@ -26,7 +26,7 @@ use std::{ use encoding_rs::{Encoding, UTF_8}; use num::integer::div_ceil; -use serde::Serialize; +use serde::{ser::SerializeSeq, Serialize}; use thiserror::Error as ThisError; use unicase::UniCase; @@ -580,7 +580,7 @@ impl HasIdentifier for Variable { /// /// For example, 1 => strongly disagree, 2 => disagree, 3 => neither agree nor /// disagree, ... -#[derive(Clone, Default, PartialEq, Eq, Serialize)] +#[derive(Clone, Default, PartialEq, Eq)] pub struct ValueLabels(pub HashMap, String>); impl ValueLabels { @@ -625,6 +625,19 @@ impl ValueLabels { } } +impl Serialize for ValueLabels { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut map = serializer.serialize_seq(Some(self.0.len()))?; + for tuple in &self.0 { + map.serialize_element(&tuple)?; + } + map.end() + } +} + impl Debug for ValueLabels { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) -- 2.30.2