use serde::{Deserialize, Serialize};
#[cfg(feature = "schema")]
use schemars::JsonSchema;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ElementStyle {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub padding: Option<PaddingValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub background: Option<BackgroundValue>,
#[serde(
rename = "cornerRadius",
default,
skip_serializing_if = "Option::is_none"
)]
pub corner_radius: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub opacity: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub frame: Option<FrameConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub border: Option<BorderConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shadow: Option<ShadowConfig>,
#[serde(rename = "clipShape", default, skip_serializing_if = "Option::is_none")]
pub clip_shape: Option<ClipShape>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub flex: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(untagged)]
pub enum ColorValue {
Solid(String),
Adaptive {
light: String,
dark: String,
},
}
impl From<&str> for ColorValue {
fn from(value: &str) -> Self {
ColorValue::Solid(value.to_string())
}
}
impl From<String> for ColorValue {
fn from(value: String) -> Self {
ColorValue::Solid(value)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum ClipShape {
Circle,
Capsule,
Rectangle,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum TextStyle {
LargeTitle,
Title,
Title2,
Title3,
Headline,
Subheadline,
Body,
Callout,
Footnote,
Caption,
Caption2,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(untagged)]
pub enum BackgroundValue {
Solid(String),
Gradient(GradientConfig),
Adaptive {
light: String,
dark: String,
},
}
impl From<&str> for BackgroundValue {
fn from(value: &str) -> Self {
BackgroundValue::Solid(value.to_string())
}
}
impl From<String> for BackgroundValue {
fn from(value: String) -> Self {
BackgroundValue::Solid(value)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct GradientConfig {
#[serde(rename = "gradientType")]
pub gradient_type: GradientType,
pub colors: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub direction: Option<GradientDirection>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum GradientType {
Linear,
Radial,
Angular,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum GradientDirection {
TopToBottom,
BottomToTop,
LeadingToTrailing,
TrailingToLeading,
TopLeadingToBottomTrailing,
TopTrailingToBottomLeading,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ShadowConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub radius: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub x: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub y: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(untagged)]
pub enum PaddingValue {
Uniform(f64),
Edges {
#[serde(default, skip_serializing_if = "Option::is_none")]
top: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
bottom: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
leading: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
trailing: Option<f64>,
},
}
impl From<f64> for PaddingValue {
fn from(value: f64) -> Self {
PaddingValue::Uniform(value)
}
}
impl From<f32> for PaddingValue {
fn from(value: f32) -> Self {
PaddingValue::Uniform(f64::from(value))
}
}
impl From<i32> for PaddingValue {
fn from(value: i32) -> Self {
PaddingValue::Uniform(f64::from(value))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct FrameConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub width: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub height: Option<f64>,
#[serde(rename = "maxWidth", default, skip_serializing_if = "Option::is_none")]
pub max_width: Option<FrameDimension>,
#[serde(rename = "maxHeight", default, skip_serializing_if = "Option::is_none")]
pub max_height: Option<FrameDimension>,
}
#[derive(Debug, Clone)]
pub enum FrameDimension {
Fixed(f64),
Infinity,
}
#[cfg(feature = "schema")]
impl JsonSchema for FrameDimension {
fn schema_name() -> String {
"FrameDimension".into()
}
fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
use schemars::schema::{
InstanceType, Metadata, Schema, SchemaObject, SingleOrVec, SubschemaValidation,
};
Schema::Object(SchemaObject {
metadata: Some(Box::new(Metadata {
description: Some(
"A frame dimension — either a fixed point value or `\"infinity\"`.".into(),
),
..Default::default()
})),
subschemas: Some(Box::new(SubschemaValidation {
any_of: Some(vec![
SchemaObject {
metadata: Some(Box::new(Metadata {
description: Some("Fixed point value.".into()),
..Default::default()
})),
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Number))),
format: Some("double".into()),
..Default::default()
}
.into(),
SchemaObject {
metadata: Some(Box::new(Metadata {
description: Some("Keyword `\"infinity\"`.".into()),
..Default::default()
})),
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
enum_values: Some(vec!["infinity".into()]),
..Default::default()
}
.into(),
]),
..Default::default()
})),
..Default::default()
})
}
}
impl Serialize for FrameDimension {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Fixed(v) => serializer.serialize_f64(*v),
Self::Infinity => serializer.serialize_str("infinity"),
}
}
}
impl<'de> Deserialize<'de> for FrameDimension {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Helper {
Num(f64),
Str(String),
}
match Helper::deserialize(deserializer)? {
Helper::Num(v) => Ok(Self::Fixed(v)),
Helper::Str(s) if s == "infinity" => Ok(Self::Infinity),
Helper::Str(s) => Err(serde::de::Error::custom(format!(
"unknown frame dimension keyword: {s}"
))),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct BorderConfig {
pub color: String,
#[serde(default = "default_border_width")]
pub width: f64,
}
fn default_border_width() -> f64 {
1.0
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum FontWeight {
Ultralight,
Thin,
Light,
Regular,
Medium,
Semibold,
Bold,
Heavy,
Black,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum FontDesign {
Default,
Monospaced,
Rounded,
Serif,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum TextAlignment {
Leading,
Center,
Trailing,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum HorizontalAlignment {
Leading,
Center,
Trailing,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum VerticalAlignment {
Top,
Center,
Bottom,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum ContentMode {
Fit,
Fill,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum ProgressStyle {
Linear,
Circular,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum GaugeStyle {
Circular,
Linear,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum DateStyle {
Time,
Date,
Relative,
Offset,
Timer,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum ChartType {
#[default]
Bar,
Line,
Area,
Pie,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum ShapeType {
#[default]
Circle,
Capsule,
Rectangle,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum TimerCounting {
Up,
Down,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn color_value_roundtrip_solid_and_adaptive() {
let solid = ColorValue::Solid("#ff0000".into());
let s = serde_json::to_string(&solid).unwrap();
let back: ColorValue = serde_json::from_str(&s).unwrap();
assert!(matches!(back, ColorValue::Solid(ref x) if x == "#ff0000"));
let adaptive = ColorValue::Adaptive {
light: "#fff".into(),
dark: "#000".into(),
};
let s = serde_json::to_string(&adaptive).unwrap();
let back: ColorValue = serde_json::from_str(&s).unwrap();
assert!(matches!(
back,
ColorValue::Adaptive {
ref light,
ref dark
} if light == "#fff" && dark == "#000"
));
}
#[test]
fn padding_value_roundtrip() {
let u = PaddingValue::Uniform(8.0);
let s = serde_json::to_string(&u).unwrap();
assert_eq!(s, "8.0");
let back: PaddingValue = serde_json::from_str(&s).unwrap();
assert!(matches!(back, PaddingValue::Uniform(v) if (v - 8.0).abs() < f64::EPSILON));
let edges = PaddingValue::Edges {
top: Some(1.0),
bottom: Some(2.0),
leading: Some(3.0),
trailing: None,
};
let s = serde_json::to_string(&edges).unwrap();
let back: PaddingValue = serde_json::from_str(&s).unwrap();
assert!(matches!(back, PaddingValue::Edges { .. }));
}
#[test]
fn element_style_default_is_empty_object() {
let s = serde_json::to_string(&ElementStyle::default()).unwrap();
assert_eq!(s, "{}");
}
#[test]
fn border_config_default_width() {
let b: BorderConfig =
serde_json::from_str(r##"{"color":"#ffffff"}"##).unwrap();
assert!((b.width - 1.0).abs() < f64::EPSILON);
assert_eq!(b.color, "#ffffff");
}
#[test]
fn chart_shape_defaults() {
assert!(matches!(ChartType::default(), ChartType::Bar));
assert!(matches!(ShapeType::default(), ShapeType::Circle));
}
#[test]
fn enums_deserialize_camel_case() {
let w: FontWeight = serde_json::from_str(r#""semibold""#).unwrap();
assert!(matches!(w, FontWeight::Semibold));
let a: TextAlignment = serde_json::from_str(r#""trailing""#).unwrap();
assert!(matches!(a, TextAlignment::Trailing));
let t: TimerCounting = serde_json::from_str(r#""down""#).unwrap();
assert!(matches!(t, TimerCounting::Down));
}
#[test]
fn frame_dimension_accepts_infinity() {
let d: FrameDimension = serde_json::from_str(r#""infinity""#).unwrap();
assert!(matches!(d, FrameDimension::Infinity));
}
#[test]
fn frame_dimension_rejects_unknown_keyword() {
assert!(serde_json::from_str::<FrameDimension>(r#""auto""#).is_err());
}
}