use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Rgb {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl Rgb {
pub const fn new(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenSpan {
pub text: String,
#[serde(default)]
pub fg: Option<Rgb>,
#[serde(default)]
pub bg: Option<Rgb>,
#[serde(default)]
pub bold: bool,
#[serde(default)]
pub italic: bool,
}
impl TokenSpan {
pub fn plain<S: Into<String>>(text: S) -> Self {
Self {
text: text.into(),
fg: None,
bg: None,
bold: false,
italic: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HighlightRequest {
#[serde(default)]
pub language: Option<String>,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HighlightResponse {
pub spans: Vec<TokenSpan>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn span_text_concatenation_preserves_input() {
let original = "fn main() {}";
let resp = HighlightResponse {
spans: vec![
TokenSpan {
text: "fn".into(),
fg: Some(Rgb::new(197, 134, 192)),
..TokenSpan::plain("")
},
TokenSpan::plain(" main"),
TokenSpan::plain("() {}"),
],
};
let reconstructed: String = resp.spans.iter().map(|s| s.text.clone()).collect();
assert_eq!(reconstructed, original);
}
#[test]
fn round_trips_through_json() {
let req = HighlightRequest {
language: Some("rust".into()),
content: "let x = 1;".into(),
};
let s = serde_json::to_string(&req).unwrap();
let back: HighlightRequest = serde_json::from_str(&s).unwrap();
assert_eq!(back.language, Some("rust".into()));
assert_eq!(back.content, "let x = 1;");
}
#[test]
fn missing_optional_fields_default_cleanly() {
let json = r#"{"text":"foo"}"#;
let span: TokenSpan = serde_json::from_str(json).unwrap();
assert_eq!(span.text, "foo");
assert!(span.fg.is_none());
assert!(!span.bold);
}
}