1use serde::{Deserialize, Serialize};
8use std::fmt;
9
10#[derive(Debug, Deserialize)]
11#[serde(deny_unknown_fields)]
12pub struct Spec {
13 #[serde(default)]
16 pub data: Option<Data>,
17 pub mark: Mark,
18 pub encoding: Encoding,
19 #[serde(default)]
20 pub title: Option<String>,
21 #[serde(default)]
23 pub width: Option<usize>,
24 #[serde(default)]
26 pub height: Option<usize>,
27}
28
29#[derive(Debug, Deserialize)]
33#[serde(deny_unknown_fields)]
34pub struct Data {
35 #[serde(default)]
37 pub values: Option<Vec<serde_json::Map<String, serde_json::Value>>>,
38 #[serde(default)]
39 pub columns: Option<Vec<Column>>,
40 #[serde(default)]
41 pub rows: Option<Vec<Vec<serde_json::Value>>>,
42}
43
44#[derive(Debug, Deserialize)]
47#[serde(deny_unknown_fields)]
48pub struct Column {
49 pub name: String,
50 #[serde(default, rename = "type")]
51 pub ty: Option<String>,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
55#[serde(rename_all = "lowercase")]
56pub enum Mark {
57 Bar,
58 Line,
59 Point,
60 Area,
61}
62
63#[derive(Debug, Deserialize)]
64#[serde(deny_unknown_fields)]
65pub struct Encoding {
66 pub x: Channel,
67 pub y: Channel,
68 #[serde(default)]
69 pub color: Option<Channel>,
70 #[serde(default, rename = "xOffset")]
76 pub x_offset: Option<serde_json::Value>,
77}
78
79#[derive(Debug, Deserialize)]
80#[serde(deny_unknown_fields)]
81pub struct Channel {
82 pub field: String,
83 #[serde(default, rename = "type")]
85 pub ty: Option<FieldType>,
86 #[serde(default)]
87 pub aggregate: Option<Aggregate>,
88 #[serde(default, rename = "timeUnit")]
94 pub time_unit: Option<TimeUnit>,
95 #[serde(default)]
101 pub bin: Option<BinValue>,
102}
103
104#[derive(Debug, Clone, Copy)]
109pub enum BinValue {
110 Flag(bool),
111 Config(BinConfig),
112}
113
114impl<'de> Deserialize<'de> for BinValue {
123 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
124 where
125 D: serde::Deserializer<'de>,
126 {
127 struct BinVisitor;
128 impl<'de> serde::de::Visitor<'de> for BinVisitor {
129 type Value = BinValue;
130 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
131 f.write_str("`true`/`false`, or a bin object with `maxbins` or `step`")
132 }
133 fn visit_bool<E>(self, v: bool) -> Result<BinValue, E> {
134 Ok(BinValue::Flag(v))
135 }
136 fn visit_map<A>(self, map: A) -> Result<BinValue, A::Error>
137 where
138 A: serde::de::MapAccess<'de>,
139 {
140 BinConfig::deserialize(serde::de::value::MapAccessDeserializer::new(map))
141 .map(BinValue::Config)
142 }
143 }
144 deserializer.deserialize_any(BinVisitor)
147 }
148}
149
150#[derive(Debug, Clone, Copy, Deserialize)]
155#[serde(deny_unknown_fields)]
156pub struct BinConfig {
157 #[serde(default)]
158 pub maxbins: Option<f64>,
159 #[serde(default)]
160 pub step: Option<f64>,
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
164#[serde(rename_all = "lowercase")]
165pub enum FieldType {
166 Quantitative,
167 Nominal,
168 Ordinal,
169 Temporal,
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
180#[serde(rename_all = "lowercase")]
181pub enum TimeUnit {
182 Year,
183 Quarter,
184 Month,
185 Week,
186 Day,
187 Hour,
188 Minute,
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
192#[serde(rename_all = "lowercase")]
193pub enum Aggregate {
194 Sum,
195 Mean,
196 Median,
197 Min,
198 Max,
199 Count,
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 fn parse_x(x: &str) -> Result<Spec, serde_json::Error> {
210 serde_json::from_str(&format!(
211 r#"{{"data":{{"values":[{{"a":1}}]}},"mark":"bar",
212 "encoding":{{"x":{x},"y":{{"field":"a"}}}}}}"#
213 ))
214 }
215
216 fn bin_of(spec: &Spec) -> Option<BinValue> {
217 spec.encoding.x.bin
218 }
219
220 #[test]
221 fn bin_true_parses_as_flag() {
222 let spec = parse_x(r#"{"field":"a","bin":true}"#).expect("bin:true parses");
223 assert!(matches!(bin_of(&spec), Some(BinValue::Flag(true))));
224 }
225
226 #[test]
227 fn bin_false_parses_as_flag() {
228 let spec = parse_x(r#"{"field":"a","bin":false}"#).expect("bin:false parses");
231 assert!(matches!(bin_of(&spec), Some(BinValue::Flag(false))));
232 }
233
234 #[test]
235 fn bin_absent_is_none() {
236 let spec = parse_x(r#"{"field":"a"}"#).expect("no bin parses");
237 assert!(bin_of(&spec).is_none());
238 }
239
240 #[test]
241 fn bin_maxbins_parses_as_config() {
242 let spec = parse_x(r#"{"field":"a","bin":{"maxbins":15}}"#).expect("maxbins parses");
243 assert!(matches!(
244 bin_of(&spec),
245 Some(BinValue::Config(BinConfig {
246 maxbins: Some(m),
247 step: None
248 })) if m == 15.0
249 ));
250 }
251
252 #[test]
253 fn bin_step_parses_as_config() {
254 let spec = parse_x(r#"{"field":"a","bin":{"step":10}}"#).expect("step parses");
255 assert!(matches!(
256 bin_of(&spec),
257 Some(BinValue::Config(BinConfig {
258 maxbins: None,
259 step: Some(s)
260 })) if s == 10.0
261 ));
262 }
263
264 #[test]
265 fn bin_empty_config_parses() {
266 let spec = parse_x(r#"{"field":"a","bin":{}}"#).expect("empty config parses");
268 assert!(matches!(
269 bin_of(&spec),
270 Some(BinValue::Config(BinConfig {
271 maxbins: None,
272 step: None
273 }))
274 ));
275 }
276
277 #[test]
280 fn bin_maxbins_zero_parses() {
281 parse_x(r#"{"field":"a","bin":{"maxbins":0}}"#)
282 .expect("maxbins:0 parses (preflight teaches)");
283 }
284
285 #[test]
286 fn bin_maxbins_fractional_parses() {
287 parse_x(r#"{"field":"a","bin":{"maxbins":2.5}}"#)
288 .expect("maxbins:2.5 parses (preflight teaches)");
289 }
290
291 #[test]
292 fn bin_step_negative_parses() {
293 parse_x(r#"{"field":"a","bin":{"step":-1}}"#).expect("step:-1 parses (preflight teaches)");
294 }
295
296 #[test]
297 fn bin_step_zero_parses() {
298 parse_x(r#"{"field":"a","bin":{"step":0}}"#).expect("step:0 parses (preflight teaches)");
299 }
300
301 #[test]
305 fn bin_unknown_field_errors() {
306 let err = parse_x(r#"{"field":"a","bin":{"extent":[0,1]}}"#)
307 .expect_err("unknown bin field must error, not match Flag");
308 let msg = err.to_string();
309 assert!(
310 msg.contains("unknown field `extent`") && msg.contains("maxbins"),
311 "extent error must name the stray field and the valid knobs: {msg}"
312 );
313 }
314
315 #[test]
318 fn bin_bare_number_errors() {
319 let err =
320 parse_x(r#"{"field":"a","bin":3}"#).expect_err("bare-number bin must error, not match");
321 let msg = err.to_string();
322 assert!(
323 msg.contains("maxbins") || msg.contains("bin object"),
324 "bare-number error must point at the bin shape: {msg}"
325 );
326 }
327}