//! Display format templates. Parses `{field.path}` //! placeholders in a template string and renders them //! against item JSON values. use serde_json::Value; use crate::item::resolve_field_path; /// A compiled format template. Segments are either literal /// text or field path references that get resolved against /// item values at render time. #[derive(Debug, Clone)] pub struct FormatTemplate { segments: Vec, } #[derive(Debug, Clone)] enum Segment { Literal(String), Field(String), } impl FormatTemplate { /// Parse a format string like `"{label} - {sublabel}"`. /// Unmatched `{` or `}` are treated as literals. pub fn parse(template: &str) -> Self { let mut segments = Vec::new(); let mut current = String::new(); let mut chars = template.chars().peekable(); while let Some(c) = chars.next() { if c == '{' { // Look for closing brace let mut field = String::new(); let mut found_close = false; for c2 in chars.by_ref() { if c2 == '}' { found_close = true; break; } field.push(c2); } if found_close && !field.is_empty() { if !current.is_empty() { segments.push(Segment::Literal(std::mem::take(&mut current))); } segments.push(Segment::Field(field)); } else { // Malformed: treat as literal current.push('{'); current.push_str(&field); if found_close { current.push('}'); } } } else { current.push(c); } } if !current.is_empty() { segments.push(Segment::Literal(current)); } Self { segments } } /// Render this template against a JSON value. Missing /// fields produce empty strings. pub fn render(&self, value: &Value) -> String { let mut out = String::new(); for seg in &self.segments { match seg { Segment::Literal(s) => out.push_str(s), Segment::Field(path) => { if let Some(v) = resolve_field_path(value, path) { match v { Value::String(s) => out.push_str(s), Value::Number(n) => out.push_str(&n.to_string()), Value::Bool(b) => out.push_str(&b.to_string()), Value::Null => {} other => out.push_str(&other.to_string()), } } } } } out } } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn simple_field() { let t = FormatTemplate::parse("{label}"); assert_eq!(t.render(&json!({"label": "Firefox"})), "Firefox"); } #[test] fn literal_and_fields() { let t = FormatTemplate::parse("{label} - {sublabel}"); let v = json!({"label": "Firefox", "sublabel": "Web Browser"}); assert_eq!(t.render(&v), "Firefox - Web Browser"); } #[test] fn missing_field_renders_empty() { let t = FormatTemplate::parse("{label} ({version})"); let v = json!({"label": "Firefox"}); assert_eq!(t.render(&v), "Firefox ()"); } #[test] fn nested_dotted_path() { let t = FormatTemplate::parse("{meta.resolution.width}x{meta.resolution.height}"); let v = json!({"meta": {"resolution": {"width": 3840, "height": 2160}}}); assert_eq!(t.render(&v), "3840x2160"); } #[test] fn plain_text_only() { let t = FormatTemplate::parse("just text"); assert_eq!(t.render(&json!({})), "just text"); } #[test] fn empty_template() { let t = FormatTemplate::parse(""); assert_eq!(t.render(&json!({"label": "x"})), ""); } #[test] fn unclosed_brace_is_literal() { let t = FormatTemplate::parse("hello {world"); assert_eq!(t.render(&json!({})), "hello {world"); } #[test] fn empty_braces_are_literal() { let t = FormatTemplate::parse("hello {}"); assert_eq!(t.render(&json!({})), "hello {}"); } #[test] fn string_value_item() { let t = FormatTemplate::parse("{label}"); // String values don't have object fields assert_eq!(t.render(&json!("hello")), ""); } }