use std::sync::OnceLock;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Design {
pub project_name: String,
pub logo_initial: String,
pub primary_color: String,
pub accent_color: String,
pub density: Density,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Density {
#[default]
Comfortable,
Compact,
}
impl Default for Design {
fn default() -> Self {
Self {
project_name: "RustIO".to_string(),
logo_initial: "R".to_string(),
primary_color: "#2563eb".to_string(),
accent_color: "#2563eb".to_string(),
density: Density::Comfortable,
}
}
}
impl Design {
pub fn load() -> Self {
let path = std::path::Path::new("rustio.design.json");
if let Ok(bytes) = std::fs::read(path) {
if let Ok(parsed) = serde_json::from_slice::<Design>(&bytes) {
return parsed;
}
}
Self::default()
}
pub fn global() -> &'static Self {
static INSTANCE: OnceLock<Design> = OnceLock::new();
INSTANCE.get_or_init(Design::load)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_values_are_reasonable() {
let d = Design::default();
assert_eq!(d.project_name, "RustIO");
assert_eq!(d.logo_initial, "R");
assert!(d.primary_color.starts_with('#'));
assert!(matches!(d.density, Density::Comfortable));
}
#[test]
fn density_serializes_as_snake_case() {
let d = Density::Comfortable;
let s = serde_json::to_string(&d).unwrap();
assert_eq!(s, "\"comfortable\"");
let d2 = Density::Compact;
let s2 = serde_json::to_string(&d2).unwrap();
assert_eq!(s2, "\"compact\"");
}
#[test]
fn parse_rejects_unknown_fields() {
let json = r#"{"project_name":"X","surprise":"yes"}"#;
let parsed = serde_json::from_str::<Design>(json);
assert!(
parsed.is_err(),
"deny_unknown_fields must reject `surprise`"
);
}
#[test]
fn parse_accepts_partial_config() {
let json = r##"{"project_name":"Workflowdesk","primary_color":"#1e40af"}"##;
let d: Design = serde_json::from_str(json).unwrap();
assert_eq!(d.project_name, "Workflowdesk");
assert_eq!(d.primary_color, "#1e40af");
assert_eq!(d.logo_initial, "R");
assert_eq!(d.accent_color, "#2563eb");
}
#[test]
fn default_palette_is_blue_as_of_0_10_1() {
let d = Design::default();
assert_eq!(d.primary_color, "#2563eb");
assert_eq!(d.accent_color, "#2563eb");
}
}