use std::{fmt, marker::PhantomData};
use csscolorparser::Color as CssColor;
use num_traits::real::Real;
use serde::{
de::{Error, SeqAccess, Unexpected, Visitor},
Deserialize, Deserializer,
};
use crate::{
utils::{Alignment, FontSize},
Color,
};
fn color_from_str<'de, D>(value: &str) -> Result<Color, D::Error>
where
D: Deserializer<'de>,
{
csscolorparser::parse(value)
.map(|c| CssColor::to_rgba8(&c))
.map(|[r, g, b, a]| Color { r, g, b, a })
.map_err(|_| D::Error::invalid_value(Unexpected::Str(value), &"a CSS color value"))
}
fn de_color<'de, D>(deserializer: D) -> Result<Option<Color>, D::Error>
where
D: Deserializer<'de>,
{
Option::<String>::deserialize(deserializer)?
.as_deref()
.map(color_from_str::<D>)
.transpose()
}
fn de_nl_delimited_colors<'de, D>(deserializer: D) -> Result<Option<Vec<Option<Color>>>, D::Error>
where
D: Deserializer<'de>,
{
Option::<String>::deserialize(deserializer)?
.map(|string| {
string
.lines()
.map(|c| (!c.is_empty()).then(|| color_from_str::<D>(c)).transpose())
.collect()
})
.transpose()
}
#[derive(Deserialize, Default, Debug, Clone)]
pub(crate) struct KleBackground {
pub name: Option<String>,
pub style: Option<String>,
}
#[derive(Deserialize, Default, Debug, Clone)]
#[serde(default, rename_all = "camelCase")]
pub(crate) struct KleMetadata {
pub author: Option<String>,
#[serde(deserialize_with = "de_color")]
pub backcolor: Option<Color>,
pub background: Option<KleBackground>,
pub name: Option<String>,
pub notes: Option<String>,
pub radii: Option<String>,
pub switch_mount: Option<String>,
pub switch_brand: Option<String>,
pub switch_type: Option<String>,
pub css: Option<String>,
pub pcb: Option<bool>,
pub plate: Option<bool>,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(default)]
pub(crate) struct KlePropsObject<T = f64>
where
T: Real,
{
pub x: Option<T>,
pub y: Option<T>,
pub w: Option<T>,
pub h: Option<T>,
pub x2: Option<T>,
pub y2: Option<T>,
pub w2: Option<T>,
pub h2: Option<T>,
pub r: Option<T>,
pub rx: Option<T>,
pub ry: Option<T>,
pub l: Option<bool>,
pub n: Option<bool>,
pub d: Option<bool>,
pub g: Option<bool>,
pub sm: Option<String>,
pub sb: Option<String>,
pub st: Option<String>,
#[serde(deserialize_with = "de_color")]
pub c: Option<Color>,
#[serde(deserialize_with = "de_nl_delimited_colors")]
pub t: Option<Vec<Option<Color>>>,
pub a: Option<Alignment>,
pub p: Option<String>,
pub f: Option<FontSize>,
pub f2: Option<FontSize>,
pub fa: Option<Vec<FontSize>>,
}
impl<T> Default for KlePropsObject<T>
where
T: Real,
{
fn default() -> Self {
Self {
x: None,
y: None,
w: None,
h: None,
x2: None,
y2: None,
w2: None,
h2: None,
r: None,
rx: None,
ry: None,
l: None,
n: None,
d: None,
g: None,
sm: None,
sb: None,
st: None,
c: None,
t: None,
a: None,
p: None,
f: None,
f2: None,
fa: None,
}
}
}
#[derive(Deserialize, Debug, Clone)]
#[serde(untagged)]
pub(crate) enum KleLegendsOrProps<T = f64>
where
T: Real,
{
Props(Box<KlePropsObject<T>>),
Legend(String),
}
#[derive(Debug, Clone)]
pub(crate) struct KleKeyboard<T = f64>
where
T: Real,
{
pub meta: KleMetadata,
pub layout: Vec<Vec<KleLegendsOrProps<T>>>,
}
impl<'de, T> Deserialize<'de> for KleKeyboard<T>
where
T: Real + Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<KleKeyboard<T>, D::Error>
where
D: Deserializer<'de>,
{
struct KleFileVisitor<T>(PhantomData<T>);
impl<'de, T> Visitor<'de> for KleFileVisitor<T>
where
T: Real + Deserialize<'de>,
{
type Value = KleKeyboard<T>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a sequence")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum MapOrSeq<T>
where
T: Real,
{
Seq(Vec<KleLegendsOrProps<T>>),
Map(Box<KleMetadata>),
}
let mut layout = Vec::with_capacity(seq.size_hint().unwrap_or(0).min(4096));
let meta = match seq.next_element()? {
Some(MapOrSeq::Map(meta)) => *meta,
Some(MapOrSeq::Seq(row)) => {
layout.push(row);
KleMetadata::default()
}
None => KleMetadata::default(),
};
while let Some(row) = seq.next_element()? {
layout.push(row);
}
Ok(Self::Value { meta, layout })
}
}
deserializer.deserialize_seq(KleFileVisitor(PhantomData))
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
use serde_json::{Deserializer, Error};
#[test]
fn test_de_color() {
let colors = [
("rebeccapurple", Color::new(102, 51, 153, 255)),
("aliceblue", Color::new(240, 248, 255, 255)),
("#f09", Color::new(255, 0, 153, 255)),
("#ff0099", Color::new(255, 0, 153, 255)),
("f09", Color::new(255, 0, 153, 255)),
("ff0099", Color::new(255, 0, 153, 255)),
("rgb(255 0 153)", Color::new(255, 0, 153, 255)),
("rgb(255 0 153 / 80%)", Color::new(255, 0, 153, 204)),
("hsl(150 30% 60%)", Color::new(122, 184, 153, 255)),
("hsl(150 30% 60% / 0.8)", Color::new(122, 184, 153, 204)),
("hwb(12 50% 0%)", Color::new(255, 153, 128, 255)),
("hwb(194 0% 0% / 0.5)", Color::new(0, 195, 255, 128)),
];
for (css, res) in colors {
let color = de_color(&mut Deserializer::from_str(&format!(r#""{css}""#)))
.unwrap()
.unwrap();
assert_eq!(color, res);
}
}
#[test]
fn test_de_nl_delimited_colors() {
let colors = de_nl_delimited_colors(&mut Deserializer::from_str(r##""#f00\n\n#ba9""##));
assert_matches!(colors, Ok(Some(v)) if v.len() == 3 && v[1].is_none());
let colors = de_nl_delimited_colors(&mut Deserializer::from_str(r##""#abc\\n#bad""##));
assert_matches!(colors, Err(Error { .. }));
let colors = de_nl_delimited_colors(&mut Deserializer::from_str("null"));
assert_matches!(colors, Ok(None));
}
#[test]
fn test_deserialize_kle_keyboard() {
let result1: KleKeyboard = serde_json::from_str(
r#"[
{
"name": "test",
"unknown": "key"
},
[
{
"a": 4,
"unknown2": "key"
},
"A",
"B",
"C"
],
[
"D"
]
]"#,
)
.unwrap();
assert_matches!(result1.meta.name, Some(name) if name == "test");
assert_eq!(result1.layout.len(), 2);
assert_eq!(result1.layout[0].len(), 4);
assert_matches!(result1.layout[0][0], KleLegendsOrProps::Props(_));
assert_matches!(result1.layout[0][1], KleLegendsOrProps::Legend(_));
let result2: KleKeyboard = serde_json::from_str(r#"[["A"]]"#).unwrap();
assert!(result2.meta.name.is_none());
assert_eq!(result2.layout.len(), 1);
let result3: KleKeyboard = serde_json::from_str(r#"[{"notes": "'tis a test"}]"#).unwrap();
assert_matches!(result3.meta.notes, Some(notes) if notes == "'tis a test");
assert_eq!(result3.layout.len(), 0);
let result4: KleKeyboard = serde_json::from_str(r"[]").unwrap();
assert!(result4.meta.name.is_none());
assert_eq!(result4.layout.len(), 0);
assert_matches!(serde_json::from_str::<KleKeyboard>("null"), Err(_));
}
}