freeswitch_log_parser/fields/render.rs
1//! Applying replacements to located spans.
2
3use std::ops::Range;
4
5use super::collect::{message_fields, raw_line_fields};
6use super::kind::{span_order, Field, FieldLocation, RenderError, RenderedEntry};
7
8/// Rewrite the spans of one text, returning the result.
9///
10/// `f` receives each field and the text it covers, and returns the replacement
11/// or `None` to leave it alone. Every field must index `text` — filter by
12/// [`Field::at`] before calling, since a range from another location means
13/// nothing here.
14///
15/// Nested spans are how the parser reports an address inside a channel name, so
16/// a replacement contained in another replacement is dropped: the outer rewrite
17/// already covers those bytes. Two replacements that overlap only partially have
18/// no such reading and are [`RenderError::OverlappingSpans`].
19///
20/// Bounds and character boundaries are checked for every field, so this never
21/// panics on a hand-built span.
22pub fn apply_fields<'a>(
23 text: &str,
24 fields: impl IntoIterator<Item = &'a Field>,
25 f: impl Fn(&Field, &str) -> Option<String>,
26) -> Result<String, RenderError> {
27 let mut replacements: Vec<(FieldLocation, Range<usize>, String)> = Vec::new();
28
29 for field in fields {
30 let range = field.range.clone();
31 if range.end > text.len() {
32 return Err(RenderError::OutOfBounds {
33 at: field.at,
34 range,
35 len: text.len(),
36 });
37 }
38 if !text.is_char_boundary(range.start) || !text.is_char_boundary(range.end) {
39 return Err(RenderError::NotOnCharBoundary {
40 at: field.at,
41 range,
42 });
43 }
44 if let Some(new) = f(field, &text[range.clone()]) {
45 replacements.push((field.at, range, new));
46 }
47 }
48
49 replacements.sort_by(|(_, a, _), (_, b, _)| span_order(a, b));
50
51 let mut out = String::with_capacity(text.len());
52 let mut cursor = 0;
53 let mut accepted: Option<Range<usize>> = None;
54
55 for (at, range, new) in replacements {
56 if let Some(prev) = &accepted {
57 if range.start < prev.end {
58 // Contained in a rewrite that already covers these bytes.
59 if range.end <= prev.end {
60 continue;
61 }
62 return Err(RenderError::OverlappingSpans {
63 at,
64 first: prev.clone(),
65 second: range,
66 });
67 }
68 }
69 out.push_str(&text[cursor..range.start]);
70 out.push_str(&new);
71 cursor = range.end;
72 accepted = Some(range);
73 }
74 out.push_str(&text[cursor..]);
75 Ok(out)
76}
77
78impl crate::stream::LogEntry {
79 /// Locate every field this entry carries, across its message and each raw
80 /// attached line.
81 ///
82 /// Recomputed per call and never stored — an entry nobody interrogates pays
83 /// nothing. Ranges index [`message`](crate::LogEntry::message) or the
84 /// attached line named by [`Field::at`], never a reassembled
85 /// [`Block`](crate::Block); [`uuid`](crate::LogEntry::uuid) is a field of the
86 /// entry rather than message text, so no span covers it.
87 ///
88 /// Ordering within one location is [`message_fields`]'s; locations follow
89 /// the message, then attached lines in order.
90 pub fn fields(&self) -> Vec<Field> {
91 let mut out = message_fields(&self.message);
92 for (i, line) in self.attached.iter().enumerate() {
93 out.extend(raw_line_fields(line, FieldLocation::Attached(i)));
94 }
95 out
96 }
97
98 /// Whether the logger's write buffer cut short the text this span indexes,
99 /// leaving the span itself incomplete.
100 ///
101 /// The cut always falls at the end of the text, so a span that stops before
102 /// it survived whole. A closed `[value]` is spanned inside its brackets and
103 /// so can never reach the end of its line; only a value the cut left
104 /// unterminated can. A span at a location this entry does not have is not
105 /// truncated — it indexes nothing here.
106 pub fn is_truncated(&self, field: &Field) -> bool {
107 if !self.cut_texts.contains(&field.at) {
108 return false;
109 }
110 let len = match field.at {
111 FieldLocation::Message => self.message.len(),
112 FieldLocation::Attached(i) => match self.attached.get(i) {
113 Some(line) => line.len(),
114 None => return false,
115 },
116 };
117 field.range.end == len
118 }
119
120 /// Rewrite this entry's fields, returning one string per render unit.
121 ///
122 /// `f` is called with each field and the text it covers; `None` leaves it
123 /// as it was. The message and each attached line are rewritten separately
124 /// because they are separate texts — see [`apply_fields`] for how nested and
125 /// overlapping replacements resolve.
126 ///
127 /// [`uuid`](crate::LogEntry::uuid), [`timestamp`](crate::LogEntry::timestamp)
128 /// and the rest of the header are entry fields rather than message text, so
129 /// a consumer rendering them handles them itself.
130 pub fn render_with(
131 &self,
132 f: impl Fn(&Field, &str) -> Option<String>,
133 ) -> Result<RenderedEntry, RenderError> {
134 let fields = self.fields();
135
136 let message = apply_fields(
137 &self.message,
138 fields.iter().filter(|x| x.at == FieldLocation::Message),
139 &f,
140 )?;
141
142 let attached = self
143 .attached
144 .iter()
145 .enumerate()
146 .map(|(i, line)| {
147 apply_fields(
148 line,
149 fields.iter().filter(|x| x.at == FieldLocation::Attached(i)),
150 &f,
151 )
152 })
153 .collect::<Result<Vec<_>, _>>()?;
154
155 Ok(RenderedEntry { message, attached })
156 }
157}