use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use crate::color::{ColorDef, ColorSpace, InterpolationMode};
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Spec {
pub scales: Scales,
pub colors: BTreeMap<String, ColorDef>,
pub semantic: BTreeMap<String, SemanticValue>,
pub fonts: Fonts,
pub space: Vec<String>,
pub dimensions: Vec<String>,
pub borders: Borders,
pub shadows: Shadows,
#[serde(rename = "textShadows")]
pub text_shadows: BTreeMap<String, TextShadowElevation>,
pub ink: InkConfig,
pub glows: BTreeMap<String, Glow>,
pub gradients: Vec<Gradient>,
pub transitions: BTreeMap<String, String>,
pub z: BTreeMap<String, serde_json::Value>,
pub opacity: BTreeMap<String, f64>,
#[serde(default)]
pub overlay: BTreeMap<String, String>,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(untagged)]
pub enum SemanticValue {
PaletteRef(String, u32),
Raw(String),
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Scales {
pub shades: Vec<u32>,
pub sizes: Vec<String>,
pub elevations: Vec<String>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Fonts {
pub families: BTreeMap<String, Vec<String>>,
pub sizes: Vec<String>,
pub weights: Vec<u32>,
#[serde(rename = "lineHeights")]
pub line_heights: Vec<f64>,
#[serde(rename = "letterSpacing")]
pub letter_spacing: BTreeMap<String, String>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Borders {
pub radius: BTreeMap<String, String>,
pub widths: Vec<String>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Shadows {
pub colors: BTreeMap<String, String>,
pub elevations: BTreeMap<String, Elevation>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Elevation {
pub layers: u32,
pub offsets: Vec<(f64, f64)>,
pub opacity: f64,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Glow {
pub color: String,
pub radii: Vec<u32>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TextShadowElevation {
pub blur: Vec<u32>,
pub opacity: f64,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct InkConfig {
pub light: (String, u32),
pub dark: (String, u32),
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Gradient {
pub name: String,
#[serde(rename = "type")]
pub gradient_type: String,
pub angle: u32,
pub stops: Vec<(String, u32)>,
#[serde(default)]
pub blend: Option<ColorSpace>,
#[serde(default)]
pub mode: Option<InterpolationMode>,
#[serde(default = "default_gradient_samples")]
pub samples: usize,
}
fn default_gradient_samples() -> usize {
7
}
#[cfg(test)]
mod tests {
use super::*;
fn minimal_json() -> &'static str {
r#"{
"scales": { "shades": [100], "sizes": ["sm"], "elevations": ["low"] },
"colors": {},
"semantic": {},
"fonts": {
"families": {},
"sizes": ["1rem"],
"weights": [400],
"lineHeights": [1.5],
"letterSpacing": {}
},
"space": ["1rem"],
"dimensions": ["1rem"],
"borders": { "radius": {}, "widths": [] },
"shadows": { "colors": {}, "elevations": {} },
"textShadows": {},
"ink": { "light": ["x", 0], "dark": ["x", 0] },
"glows": {},
"gradients": [],
"transitions": {},
"z": {},
"opacity": {}
}"#
}
#[test]
fn spec_deserializes_with_minimal_fields() {
let spec: Spec = serde_json::from_str(minimal_json()).unwrap();
assert_eq!(spec.scales.shades, vec![100]);
assert!(spec.overlay.is_empty());
}
#[test]
fn spec_overlay_defaults_to_empty() {
let spec: Spec = serde_json::from_str(minimal_json()).unwrap();
assert!(spec.overlay.is_empty());
}
#[test]
fn spec_roundtrips_through_serde() {
let spec: Spec = serde_json::from_str(minimal_json()).unwrap();
let json = serde_json::to_string(&spec).unwrap();
let again: Spec = serde_json::from_str(&json).unwrap();
assert_eq!(again.scales.shades, vec![100]);
}
#[test]
fn semantic_value_palette_ref_deserializes() {
let v: SemanticValue = serde_json::from_str(r#"["primary", 500]"#).unwrap();
match v {
SemanticValue::PaletteRef(name, shade) => {
assert_eq!(name, "primary");
assert_eq!(shade, 500);
}
SemanticValue::Raw(_) => panic!("expected PaletteRef"),
}
}
#[test]
fn semantic_value_raw_deserializes() {
let v: SemanticValue = serde_json::from_str(r##""#abcdef""##).unwrap();
match v {
SemanticValue::Raw(s) => assert_eq!(s, "#abcdef"),
SemanticValue::PaletteRef(_, _) => panic!("expected Raw"),
}
}
}