This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-messages / src / template.rs
5.1 kB 188 lines
1use std::fmt; 2 3const SPACER: &str = "\u{200b}"; 4 5pub trait Key: Copy + 'static { 6 const PLACEHOLDERS: &'static [(&'static str, Self)]; 7} 8 9#[derive(Debug, Clone, Copy)] 10pub enum NoKeys {} 11 12impl Key for NoKeys { 13 const PLACEHOLDERS: &'static [(&'static str, Self)] = &[]; 14} 15 16pub trait Shape { 17 type Repr<K>; 18} 19 20#[derive(Debug)] 21pub struct Lines; 22 23impl Shape for Lines { 24 type Repr<K> = Vec<Vec<Segment<K>>>; 25} 26 27#[derive(Debug)] 28pub struct Line; 29 30impl Shape for Line { 31 type Repr<K> = Vec<Segment<K>>; 32} 33 34#[derive(Debug, thiserror::Error, PartialEq, Eq)] 35pub enum TemplateError { 36 #[error("{field}: unknown placeholder {{{name}}}")] 37 UnknownPlaceholder { field: &'static str, name: String }, 38 #[error("{field}: unclosed {{ in template")] 39 UnclosedBrace { field: &'static str }, 40 #[error("{field}: stray }} in template")] 41 StrayBrace { field: &'static str }, 42 #[error("{field}: template mustn't be empty")] 43 Empty { field: &'static str }, 44 #[error("{field}: template must be a single line")] 45 Multiline { field: &'static str }, 46} 47 48#[derive(Debug)] 49pub enum Segment<K> { 50 Literal(String), 51 Placeholder(K), 52} 53 54pub struct Template<K, S: Shape> { 55 repr: S::Repr<K>, 56} 57 58impl<K, S: Shape> fmt::Debug for Template<K, S> 59where 60 S::Repr<K>: fmt::Debug, 61{ 62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 63 f.debug_struct("Template") 64 .field("repr", &self.repr) 65 .finish() 66 } 67} 68 69impl<K: Key> Template<K, Lines> { 70 pub fn parse_lines(field: &'static str, texts: &[String]) -> Result<Self, TemplateError> { 71 Ok(Self { 72 repr: texts 73 .iter() 74 .map(|text| line_segments(field, text)) 75 .collect::<Result<_, _>>()?, 76 }) 77 } 78 79 pub fn lines(&self, resolve: impl Fn(K) -> String) -> Vec<String> { 80 self.repr 81 .iter() 82 .map(|segments| { 83 let rendered = render_segments(segments, &resolve); 84 match rendered.is_empty() { 85 true => SPACER.to_string(), 86 false => rendered, 87 } 88 }) 89 .collect() 90 } 91} 92 93impl<K: Key> Template<K, Line> { 94 // A reject reason is a Line - `receive-pack` gives it as 95 // "ng <ref> <reason>\n" inside one `pkt-line`. 96 // `line_segments` won't accept newlines 97 // so the reason stays on that one line. 98 pub fn parse(field: &'static str, text: &str) -> Result<Self, TemplateError> { 99 match text.is_empty() { 100 true => Err(TemplateError::Empty { field }), 101 false => Ok(Self { 102 repr: line_segments(field, text)?, 103 }), 104 } 105 } 106 107 pub fn line(&self, resolve: impl Fn(K) -> String) -> String { 108 render_segments(&self.repr, &resolve) 109 } 110} 111 112impl Template<NoKeys, Lines> { 113 pub fn text_lines(&self) -> Vec<String> { 114 self.lines(|key| match key {}) 115 } 116} 117 118impl Template<NoKeys, Line> { 119 pub fn text(&self) -> String { 120 self.line(|key| match key {}) 121 } 122} 123 124fn line_segments<K: Key>( 125 field: &'static str, 126 text: &str, 127) -> Result<Vec<Segment<K>>, TemplateError> { 128 match text.contains('\n') { 129 true => Err(TemplateError::Multiline { field }), 130 false => segments(field, text), 131 } 132} 133 134fn render_segments<K: Key>(segments: &[Segment<K>], resolve: &dyn Fn(K) -> String) -> String { 135 segments 136 .iter() 137 .map(|segment| match segment { 138 Segment::Literal(text) => text.clone(), 139 Segment::Placeholder(key) => resolve(*key), 140 }) 141 .collect() 142} 143 144fn segments<K: Key>(field: &'static str, text: &str) -> Result<Vec<Segment<K>>, TemplateError> { 145 let Some(at) = text.find(['{', '}']) else { 146 return Ok(literal(text)); 147 }; 148 let (before, rest) = text.split_at(at); 149 let (parsed, remainder) = brace(field, rest)?; 150 Ok(literal(before) 151 .into_iter() 152 .chain(parsed) 153 .chain(segments(field, remainder)?) 154 .collect()) 155} 156 157fn literal<K>(text: &str) -> Vec<Segment<K>> { 158 match text.is_empty() { 159 true => Vec::new(), 160 false => vec![Segment::Literal(text.to_string())], 161 } 162} 163 164type Braced<'a, K> = (Option<Segment<K>>, &'a str); 165 166fn brace<'a, K: Key>(field: &'static str, rest: &'a str) -> Result<Braced<'a, K>, TemplateError> { 167 if let Some(after) = rest.strip_prefix("{{") { 168 return Ok((Some(Segment::Literal("{".to_string())), after)); 169 } 170 if let Some(after) = rest.strip_prefix("}}") { 171 return Ok((Some(Segment::Literal("}".to_string())), after)); 172 } 173 if rest.starts_with('}') { 174 return Err(TemplateError::StrayBrace { field }); 175 } 176 let close = rest 177 .find('}') 178 .ok_or(TemplateError::UnclosedBrace { field })?; 179 let name = &rest[1..close]; 180 K::PLACEHOLDERS 181 .iter() 182 .find(|(candidate, _)| *candidate == name) 183 .map(|(_, key)| (Some(Segment::Placeholder(*key)), &rest[close + 1..])) 184 .ok_or_else(|| TemplateError::UnknownPlaceholder { 185 field, 186 name: name.to_string(), 187 }) 188}