use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Spec {
#[serde(default)]
pub data: Option<Data>,
pub mark: Mark,
pub encoding: Encoding,
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub width: Option<usize>,
#[serde(default)]
pub height: Option<usize>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Data {
#[serde(default)]
pub values: Option<Vec<serde_json::Map<String, serde_json::Value>>>,
#[serde(default)]
pub columns: Option<Vec<Column>>,
#[serde(default)]
pub rows: Option<Vec<Vec<serde_json::Value>>>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Column {
pub name: String,
#[serde(default, rename = "type")]
pub ty: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Mark {
Bar,
Line,
Point,
Area,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Encoding {
pub x: Channel,
pub y: Channel,
#[serde(default)]
pub color: Option<Channel>,
#[serde(default, rename = "xOffset")]
pub x_offset: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Channel {
pub field: String,
#[serde(default, rename = "type")]
pub ty: Option<FieldType>,
#[serde(default)]
pub aggregate: Option<Aggregate>,
#[serde(default, rename = "timeUnit")]
pub time_unit: Option<TimeUnit>,
#[serde(default)]
pub bin: Option<BinValue>,
}
#[derive(Debug, Clone, Copy)]
pub enum BinValue {
Flag(bool),
Config(BinConfig),
}
impl<'de> Deserialize<'de> for BinValue {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct BinVisitor;
impl<'de> serde::de::Visitor<'de> for BinVisitor {
type Value = BinValue;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("`true`/`false`, or a bin object with `maxbins` or `step`")
}
fn visit_bool<E>(self, v: bool) -> Result<BinValue, E> {
Ok(BinValue::Flag(v))
}
fn visit_map<A>(self, map: A) -> Result<BinValue, A::Error>
where
A: serde::de::MapAccess<'de>,
{
BinConfig::deserialize(serde::de::value::MapAccessDeserializer::new(map))
.map(BinValue::Config)
}
}
deserializer.deserialize_any(BinVisitor)
}
}
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BinConfig {
#[serde(default)]
pub maxbins: Option<f64>,
#[serde(default)]
pub step: Option<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum FieldType {
Quantitative,
Nominal,
Ordinal,
Temporal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum TimeUnit {
Year,
Quarter,
Month,
Week,
Day,
Hour,
Minute,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Aggregate {
Sum,
Mean,
Median,
Min,
Max,
Count,
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_x(x: &str) -> Result<Spec, serde_json::Error> {
serde_json::from_str(&format!(
r#"{{"data":{{"values":[{{"a":1}}]}},"mark":"bar",
"encoding":{{"x":{x},"y":{{"field":"a"}}}}}}"#
))
}
fn bin_of(spec: &Spec) -> Option<BinValue> {
spec.encoding.x.bin
}
#[test]
fn bin_true_parses_as_flag() {
let spec = parse_x(r#"{"field":"a","bin":true}"#).expect("bin:true parses");
assert!(matches!(bin_of(&spec), Some(BinValue::Flag(true))));
}
#[test]
fn bin_false_parses_as_flag() {
let spec = parse_x(r#"{"field":"a","bin":false}"#).expect("bin:false parses");
assert!(matches!(bin_of(&spec), Some(BinValue::Flag(false))));
}
#[test]
fn bin_absent_is_none() {
let spec = parse_x(r#"{"field":"a"}"#).expect("no bin parses");
assert!(bin_of(&spec).is_none());
}
#[test]
fn bin_maxbins_parses_as_config() {
let spec = parse_x(r#"{"field":"a","bin":{"maxbins":15}}"#).expect("maxbins parses");
assert!(matches!(
bin_of(&spec),
Some(BinValue::Config(BinConfig {
maxbins: Some(m),
step: None
})) if m == 15.0
));
}
#[test]
fn bin_step_parses_as_config() {
let spec = parse_x(r#"{"field":"a","bin":{"step":10}}"#).expect("step parses");
assert!(matches!(
bin_of(&spec),
Some(BinValue::Config(BinConfig {
maxbins: None,
step: Some(s)
})) if s == 10.0
));
}
#[test]
fn bin_empty_config_parses() {
let spec = parse_x(r#"{"field":"a","bin":{}}"#).expect("empty config parses");
assert!(matches!(
bin_of(&spec),
Some(BinValue::Config(BinConfig {
maxbins: None,
step: None
}))
));
}
#[test]
fn bin_maxbins_zero_parses() {
parse_x(r#"{"field":"a","bin":{"maxbins":0}}"#)
.expect("maxbins:0 parses (preflight teaches)");
}
#[test]
fn bin_maxbins_fractional_parses() {
parse_x(r#"{"field":"a","bin":{"maxbins":2.5}}"#)
.expect("maxbins:2.5 parses (preflight teaches)");
}
#[test]
fn bin_step_negative_parses() {
parse_x(r#"{"field":"a","bin":{"step":-1}}"#).expect("step:-1 parses (preflight teaches)");
}
#[test]
fn bin_step_zero_parses() {
parse_x(r#"{"field":"a","bin":{"step":0}}"#).expect("step:0 parses (preflight teaches)");
}
#[test]
fn bin_unknown_field_errors() {
let err = parse_x(r#"{"field":"a","bin":{"extent":[0,1]}}"#)
.expect_err("unknown bin field must error, not match Flag");
let msg = err.to_string();
assert!(
msg.contains("unknown field `extent`") && msg.contains("maxbins"),
"extent error must name the stray field and the valid knobs: {msg}"
);
}
#[test]
fn bin_bare_number_errors() {
let err =
parse_x(r#"{"field":"a","bin":3}"#).expect_err("bare-number bin must error, not match");
let msg = err.to_string();
assert!(
msg.contains("maxbins") || msg.contains("bin object"),
"bare-number error must point at the bin shape: {msg}"
);
}
}