1use std::collections::HashSet;
14
15use serde::{Deserialize, Serialize};
16use serde_json::Value;
17
18use super::{
19 CustomNumberFormatConfig, DatetimeFormatType, KeyValueOpts, NumberSeriesStyleDefaultConfig,
20};
21use crate::utils::{CssKind, GradientStopSpec, canonicalize_gradient_stops};
22
23#[derive(Clone, Debug, Default, Deserialize, Serialize)]
28pub struct ColumnConfigSchema {
29 pub fields: Vec<ControlSpec>,
30}
31
32impl ColumnConfigSchema {
33 pub fn active_keys(&self) -> HashSet<String> {
39 let mut out = HashSet::new();
40 for spec in &self.fields {
41 for k in spec.serialized_keys() {
42 out.insert(k.to_string());
43 }
44 }
45 out
46 }
47
48 pub fn leaf_fields(&self) -> Vec<&ControlSpec> {
49 fn collect<'a>(fields: &'a [ControlSpec], out: &mut Vec<&'a ControlSpec>) {
50 for spec in fields {
51 match spec {
52 ControlSpec::Group { fields, .. } => collect(fields, out),
53 leaf => out.push(leaf),
54 }
55 }
56 }
57
58 let mut out = vec![];
59 collect(&self.fields, &mut out);
60 out
61 }
62}
63
64#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
66#[serde(rename_all = "snake_case")]
67pub enum NumberStat {
68 AbsMax,
70}
71
72#[derive(Clone, Debug, Deserialize, Serialize)]
78#[serde(tag = "kind")]
79pub enum ControlSpec {
80 Enum {
81 key: String,
82 variants: Vec<EnumVariant>,
83 default: String,
84 },
85
86 Font {
90 key: String,
91 default: String,
92
93 #[serde(default, skip_serializing_if = "Option::is_none")]
94 size: Option<FontSize>,
95
96 #[serde(default, skip_serializing_if = "Option::is_none")]
97 bold: Option<FontToggle>,
98
99 #[serde(default, skip_serializing_if = "Option::is_none")]
100 italic: Option<FontToggle>,
101 },
102 Alignment {
105 key: String,
106
107 #[serde(default, skip_serializing_if = "Option::is_none")]
111 default: Option<Alignment>,
112
113 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
115 corners: bool,
116 },
117 Bool {
118 key: String,
119 default: bool,
120 },
121 Number {
122 key: String,
123 default: f64,
124
125 #[serde(default, skip_serializing_if = "Option::is_none")]
127 include: Option<bool>,
128
129 #[serde(default, skip_serializing_if = "Option::is_none")]
130 min: Option<f64>,
131
132 #[serde(default, skip_serializing_if = "Option::is_none")]
133 max: Option<f64>,
134
135 #[serde(default, skip_serializing_if = "Option::is_none")]
136 step: Option<f64>,
137
138 #[serde(default, skip_serializing_if = "Option::is_none")]
141 default_stat: Option<NumberStat>,
142 },
143 String {
144 key: String,
145 default: String,
146 #[serde(default, skip_serializing_if = "Option::is_none")]
147 placeholder: Option<String>,
148 },
149 Color {
150 key: String,
151 default: String,
152 },
153 Palette {
154 key: String,
155 default: String,
156
157 #[serde(default, skip_serializing_if = "Option::is_none")]
158 max: Option<usize>,
159 },
160 GradientStops {
161 key: String,
162 default: String,
163
164 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
165 discrete: bool,
166 },
167 DatetimeFormat {
168 #[serde(default, skip_serializing_if = "Option::is_none")]
171 default: Option<DatetimeFormatType>,
172 },
173 NumberSeriesStyle {
174 default: NumberSeriesStyleDefaultConfig,
175 },
176 Symbols {
177 default: KeyValueOpts,
178 },
179 NumberFormat {
180 #[serde(default, skip_serializing_if = "Option::is_none")]
183 default: Option<CustomNumberFormatConfig>,
184 },
185 AggregateDepth,
186
187 Group {
188 key: String,
189 #[serde(default)]
190 fields: Vec<ControlSpec>,
191 },
192}
193
194#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
198#[serde(rename_all = "kebab-case")]
199pub enum Alignment {
200 TopLeft,
201 Top,
202 TopRight,
203 Left,
204 Center,
205 Right,
206 BottomLeft,
207 Bottom,
208 BottomRight,
209}
210
211impl Alignment {
212 pub const ALL: [Alignment; 9] = [
214 Alignment::TopLeft,
215 Alignment::Top,
216 Alignment::TopRight,
217 Alignment::Left,
218 Alignment::Center,
219 Alignment::Right,
220 Alignment::BottomLeft,
221 Alignment::Bottom,
222 Alignment::BottomRight,
223 ];
224 pub const CORNERS: [Alignment; 4] = [
225 Alignment::TopLeft,
226 Alignment::TopRight,
227 Alignment::BottomLeft,
228 Alignment::BottomRight,
229 ];
230
231 pub fn is_corner(self) -> bool {
232 Self::CORNERS.contains(&self)
233 }
234
235 pub fn as_str(self) -> &'static str {
237 match self {
238 Alignment::TopLeft => "top-left",
239 Alignment::Top => "top",
240 Alignment::TopRight => "top-right",
241 Alignment::Left => "left",
242 Alignment::Center => "center",
243 Alignment::Right => "right",
244 Alignment::BottomLeft => "bottom-left",
245 Alignment::Bottom => "bottom",
246 Alignment::BottomRight => "bottom-right",
247 }
248 }
249
250 pub fn parse(src: &str) -> Option<Self> {
251 Self::ALL.into_iter().find(|x| x.as_str() == src)
252 }
253
254 pub fn humanized(self) -> String {
256 self.as_str()
257 .split('-')
258 .map(|word| {
259 let mut chars = word.chars();
260 match chars.next() {
261 Some(first) => first.to_uppercase().chain(chars).collect::<String>(),
262 None => String::new(),
263 }
264 })
265 .collect::<Vec<_>>()
266 .join(" ")
267 }
268}
269
270#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
273pub struct FontToggle {
274 pub key: String,
275 #[serde(default)]
276 pub default: bool,
277}
278
279#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
282pub struct FontSize {
283 pub key: String,
284 pub default: f64,
285
286 #[serde(default, skip_serializing_if = "Option::is_none")]
287 pub min: Option<f64>,
288
289 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub max: Option<f64>,
291
292 #[serde(default, skip_serializing_if = "Option::is_none")]
293 pub step: Option<f64>,
294}
295
296#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
297pub struct EnumVariant {
298 pub value: String,
299 #[serde(default, skip_serializing_if = "Option::is_none")]
300 pub label: Option<String>,
301}
302
303pub fn discrete_pair(stops: Vec<GradientStopSpec>) -> Vec<GradientStopSpec> {
306 let stops = canonicalize_gradient_stops(stops);
307 match (stops.first(), stops.last()) {
308 (Some(first), Some(last)) if stops.len() > 2 => vec![
309 GradientStopSpec {
310 color: first.color.clone(),
311 offset: 0.0,
312 },
313 GradientStopSpec {
314 color: last.color.clone(),
315 offset: 1.0,
316 },
317 ],
318 _ => stops,
319 }
320}
321
322impl ColumnConfigSchema {
323 pub fn canonicalize(self) -> Self {
324 self.canonicalize_defaults().group_format_controls()
325 }
326
327 pub fn resolve_stat_defaults(mut self, abs_max: Option<f64>) -> Self {
330 fn walk(fields: &mut [ControlSpec], abs_max: Option<f64>) {
331 for spec in fields {
332 match spec {
333 ControlSpec::Group { fields, .. } => walk(fields, abs_max),
334 ControlSpec::Number {
335 default,
336 default_stat: Some(NumberStat::AbsMax),
337 ..
338 } => {
339 if let Some(abs_max) = abs_max {
340 *default = abs_max;
341 }
342 },
343 _ => {},
344 }
345 }
346 }
347
348 walk(&mut self.fields, abs_max);
349 self
350 }
351
352 pub fn group_format_controls(mut self) -> Self {
353 fn is_format(spec: &ControlSpec) -> bool {
354 matches!(
355 spec,
356 ControlSpec::NumberFormat { .. } | ControlSpec::DatetimeFormat { .. }
357 )
358 }
359
360 fn walk(fields: &mut Vec<ControlSpec>) {
361 for spec in fields.iter_mut() {
362 if let ControlSpec::Group { key, fields } = spec
363 && key != "format"
364 {
365 walk(fields);
366 }
367 }
368
369 let first = fields.iter().position(is_format);
370 if let Some(first) = first {
371 let mut formats = vec![];
372 let mut i = first;
373 while i < fields.len() {
374 if is_format(&fields[i]) {
375 formats.push(fields.remove(i));
376 } else {
377 i += 1;
378 }
379 }
380
381 fields.insert(first, ControlSpec::Group {
382 key: "format".to_owned(),
383 fields: formats,
384 });
385 }
386 }
387
388 walk(&mut self.fields);
389 self
390 }
391
392 pub fn canonicalize_defaults(mut self) -> Self {
395 fn canonicalize_specs(fields: &mut Vec<ControlSpec>) {
396 fields.retain_mut(|spec| {
397 let (kind, key, default) = match spec {
398 ControlSpec::Group { fields, .. } => {
399 canonicalize_specs(fields);
400 return !fields.is_empty();
401 },
402 ControlSpec::Alignment {
403 key,
404 default: Some(default),
405 corners: true,
406 } if !default.is_corner() => {
407 tracing::error!(
408 "Dropping `{key}` — default `{}` is not a corner",
409 default.as_str()
410 );
411
412 return false;
413 },
414 ControlSpec::Color { key, default } => (CssKind::Color, key, default),
415 ControlSpec::Palette { key, default, .. } => (CssKind::Palette, key, default),
416 ControlSpec::GradientStops { key, default, .. } => {
417 (CssKind::Gradient, key, default)
418 },
419 _ => return true,
420 };
421
422 match kind.canonicalize(default) {
423 Ok(canonical) => {
424 *default = canonical;
425 true
426 },
427 Err(error) => {
428 tracing::error!("Dropping `{key}` — invalid schema default: {error}");
429 false
430 },
431 }
432 });
433 }
434
435 canonicalize_specs(&mut self.fields);
436 self
437 }
438
439 pub fn enum_variants_of(&self, key: &str) -> Option<&[EnumVariant]> {
441 self.leaf_fields().into_iter().find_map(|spec| match spec {
442 ControlSpec::Enum {
443 key: k, variants, ..
444 } if k == key => Some(variants.as_slice()),
445 _ => None,
446 })
447 }
448
449 pub fn css_kind_of(&self, key: &str) -> Option<CssKind> {
451 self.leaf_fields().into_iter().find_map(|spec| match spec {
452 ControlSpec::Color { key: k, .. } if k == key => Some(CssKind::Color),
453 ControlSpec::Palette { key: k, .. } if k == key => Some(CssKind::Palette),
454 ControlSpec::GradientStops { key: k, .. } if k == key => Some(CssKind::Gradient),
455 _ => None,
456 })
457 }
458}
459
460impl ControlSpec {
461 pub fn serialized_keys(&self) -> Vec<&str> {
467 match self {
468 ControlSpec::DatetimeFormat { .. } => vec!["date_format"],
469 ControlSpec::Font {
470 key,
471 size,
472 bold,
473 italic,
474 ..
475 } => [
476 Some(key.as_str()),
477 size.as_ref().map(|s| s.key.as_str()),
478 bold.as_ref().map(|t| t.key.as_str()),
479 italic.as_ref().map(|t| t.key.as_str()),
480 ]
481 .into_iter()
482 .flatten()
483 .collect(),
484 ControlSpec::NumberSeriesStyle { .. } => vec!["chart_type", "stack"],
485 ControlSpec::Symbols { .. } => vec!["symbols"],
486 ControlSpec::NumberFormat { .. } => vec!["number_format"],
487 ControlSpec::AggregateDepth => vec!["aggregate_depth"],
488 ControlSpec::Enum { key, .. }
489 | ControlSpec::Alignment { key, .. }
490 | ControlSpec::Bool { key, .. }
491 | ControlSpec::Number { key, .. }
492 | ControlSpec::String { key, .. }
493 | ControlSpec::Color { key, .. }
494 | ControlSpec::Palette { key, .. }
495 | ControlSpec::GradientStops { key, .. } => vec![key.as_str()],
496 ControlSpec::Group { fields, .. } => {
497 fields.iter().flat_map(|f| f.serialized_keys()).collect()
498 },
499 }
500 }
501}
502
503#[derive(Clone, Debug, Deserialize, Serialize)]
508pub struct ColumnConfigFieldUpdate {
509 pub keys: Vec<String>,
510 pub value: serde_json::Map<String, Value>,
511}
512
513pub fn filter_to_schema(
517 config: &serde_json::Map<String, Value>,
518 active_keys: &HashSet<String>,
519) -> serde_json::Map<String, Value> {
520 config
521 .iter()
522 .filter(|(k, _)| active_keys.contains(k.as_str()))
523 .map(|(k, v)| (k.clone(), v.clone()))
524 .collect()
525}