1mod spec;
7
8use crate::types::{Record, RecordExt};
9use spec::Spec;
10use std::collections::HashMap;
11
12type Transformer = Box<dyn Fn(&Record, &String) -> String + Send + Sync>;
15
16#[derive(Debug, Clone)]
19pub struct FieldFormat {
20 name: String,
21 resolve: fn(&str) -> &str,
22}
23impl FieldFormat {
24 fn new(name: String, resolve: fn(&str) -> &str) -> Self {
25 Self { name, resolve }
26 }
27}
28
29pub struct Formatter {
36 fields: HashMap<String, FieldFormat>,
37 transformer: Option<Transformer>,
38 segments: Vec<Segment>,
39 pub time_format: &'static str,
42}
43
44#[derive(Debug)]
47enum Segment {
48 Literal(String),
49 Field { name: String, spec: Spec },
50}
51
52impl Formatter {
53 pub fn new(pattern: &str) -> Self {
56 Self {
57 fields: HashMap::new(),
58 transformer: None,
59 segments: Self::parse(pattern),
60 time_format: "%Y-%m-%d %H:%M:%S",
61 }
62 }
63
64 pub fn add_field(&mut self, name: &str, resolve: fn(&str) -> &str) -> &mut Self {
68 self.fields.insert(
69 name.to_string(),
70 FieldFormat::new(name.to_string(), resolve),
71 );
72 self
73 }
74
75 pub fn format(&self, record: &Record) -> String {
80 let mut result = String::with_capacity(64);
81 for segment in self.segments.iter() {
82 match segment {
83 Segment::Literal(literal) => result.push_str(literal),
84 Segment::Field { name, spec } => {
85 let raw = match self.fields.get(name) {
86 Some(f) => (f.resolve)(record.get_or_default(f.name.as_str())),
87 None => record.get_or_default(name),
88 };
89 result.push_str(&spec.apply(raw));
90 }
91 }
92 }
93 result.push('\n');
94 if let Some(transformer) = &self.transformer {
95 result = transformer(record, &result);
96 }
97 result
98 }
99
100 pub fn set_transformer(
103 &mut self,
104 transformer: impl Fn(&Record, &String) -> String + Send + Sync + 'static,
105 ) {
106 self.transformer = Some(Box::new(move |record, result| transformer(record, result)));
107 }
108
109 pub fn set_time_format(&mut self, time_format: &'static str) {
112 self.time_format = time_format;
113 }
114
115 fn parse(pattern: &str) -> Vec<Segment> {
121 let mut segments = Vec::new();
122 let mut rest = pattern;
123 while !rest.is_empty() {
124 if let Some(stripped) = rest.strip_prefix("%(") {
125 if let Some(close) = stripped.find(")") {
126 let name = &stripped[..close];
127 let tail = &stripped[close + 1..];
128
129 let (spec, end) = Spec::parse(tail);
130
131 segments.push(Segment::Field {
132 name: name.to_string(),
133 spec,
134 });
135
136 rest = &tail[end..];
137 } else {
138 segments.push(Segment::Literal(rest.to_string()));
139 break;
140 }
141 } else {
142 let end = rest.find("%(").unwrap_or(rest.len());
143 segments.push(Segment::Literal(rest[..end].to_string()));
144 rest = &rest[end..];
145 }
146 }
147
148 segments
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn substitutes_fields_and_literals() {
158 let formatter = Formatter::new("%(timestamp) - %(message)");
159 let record = HashMap::from([
160 ("timestamp", "2026-01-01 12:00:00"),
161 ("message", "Hello, world!"),
162 ]);
163 assert_eq!(
165 formatter.format(&record),
166 "2026-01-01 12:00:00 - Hello, world!\n"
167 );
168 }
169
170 #[test]
171 fn missing_field_renders_empty() {
172 let formatter = Formatter::new("[%(missing)]");
173 let record: Record = HashMap::new();
174 assert_eq!(formatter.format(&record), "[]\n");
175 }
176
177 #[test]
178 fn preserves_literal_immediately_after_spec() {
179 let formatter = Formatter::new("[%(name)-7.4] %(message)");
181 let record = HashMap::from([("name", "ab"), ("message", "hi")]);
182 assert_eq!(formatter.format(&record), "[ab ] hi\n");
185 }
186
187 #[test]
188 fn right_aligns_and_truncates_via_spec() {
189 let formatter = Formatter::new("%(level)8.3");
190 let record = HashMap::from([("level", "WARNING")]);
191 assert_eq!(formatter.format(&record), " WAR\n");
193 }
194
195 #[test]
196 fn field_transform_is_applied_before_spec() {
197 let mut formatter = Formatter::new("%(level)");
198 formatter.add_field("level", |_| "INFO");
199 let record = HashMap::from([("level", "info")]);
200 assert_eq!(formatter.format(&record), "INFO\n");
201 }
202
203 #[test]
204 fn unterminated_placeholder_is_literal() {
205 let formatter = Formatter::new("oops %(name");
206 let record = HashMap::from([("name", "x")]);
207 assert_eq!(formatter.format(&record), "oops %(name\n");
208 }
209
210 #[test]
211 fn transformer_post_processes_line() {
212 let mut formatter = Formatter::new("%(message)");
213 formatter.set_transformer(|_record, line| format!(">> {}", line));
214 let record = HashMap::from([("message", "hi")]);
215 assert_eq!(formatter.format(&record), ">> hi\n");
216 }
217}