use serde::{Deserialize, Serialize};
use tatara_lisp::DeriveTataraDomain;
#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
#[tatara(keyword = "defruler")]
pub struct RulerSpec {
#[serde(default)]
pub columns: Vec<u32>,
#[serde(default)]
pub filetype: String,
#[serde(default)]
pub style: String,
#[serde(default)]
pub color: String,
#[serde(default)]
pub description: String,
}
pub const KNOWN_STYLES: &[&str] = &["soft", "hard", "dim"];
#[must_use]
pub fn is_known_style(style: &str) -> bool {
KNOWN_STYLES.contains(&style)
}
impl RulerSpec {
#[must_use]
pub fn effective_style(&self) -> &str {
crate::strutil::default_if_empty(&self.style, "soft")
}
#[must_use]
pub fn all_columns_positive(&self) -> bool {
self.columns.iter().all(|c| *c > 0)
}
#[must_use]
pub fn has_valid_color_format(&self) -> bool {
if self.color.is_empty() {
return true;
}
let bytes = self.color.as_bytes();
if bytes.first() != Some(&b'#') {
return false;
}
let rest = &bytes[1..];
if rest.len() != 6 && rest.len() != 8 {
return false;
}
rest.iter()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(b) || (b'A'..=b'F').contains(b))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn style_vocabulary_is_exactly_three_entries() {
assert_eq!(KNOWN_STYLES, &["soft", "hard", "dim"]);
assert!(is_known_style("soft"));
assert!(is_known_style("hard"));
assert!(is_known_style("dim"));
assert!(!is_known_style("bold"));
assert!(!is_known_style(""));
}
#[test]
fn effective_style_falls_back_to_soft() {
let s = RulerSpec::default();
assert_eq!(s.effective_style(), "soft");
let s = RulerSpec { style: "hard".into(), ..Default::default() };
assert_eq!(s.effective_style(), "hard");
}
#[test]
fn all_columns_positive_catches_zero_entries() {
let s = RulerSpec { columns: vec![80, 120], ..Default::default() };
assert!(s.all_columns_positive());
let s = RulerSpec { columns: vec![80, 0, 120], ..Default::default() };
assert!(!s.all_columns_positive());
let s = RulerSpec::default();
assert!(s.all_columns_positive());
}
#[test]
fn color_format_accepts_rgb_and_rgba_hex() {
for ok in ["#000000", "#ffffff", "#4c566a", "#112233aa", "#AABBCC"] {
let s = RulerSpec { color: ok.into(), ..Default::default() };
assert!(s.has_valid_color_format(), "should accept {ok:?}");
}
}
#[test]
fn color_format_rejects_named_or_malformed() {
for bad in ["blue", "#fff", "#123456789", "rgb(0,0,0)", "#zzzzzz", "fff"] {
let s = RulerSpec { color: bad.into(), ..Default::default() };
assert!(!s.has_valid_color_format(), "should reject {bad:?}");
}
}
#[test]
fn empty_color_is_valid_means_theme_default() {
let s = RulerSpec::default();
assert!(s.has_valid_color_format());
assert!(s.color.is_empty());
}
}