Skip to main content

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::{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, _)| {
50        (a.start, std::cmp::Reverse(a.end)).cmp(&(b.start, std::cmp::Reverse(b.end)))
51    });
52
53    let mut out = String::with_capacity(text.len());
54    let mut cursor = 0;
55    let mut accepted: Option<Range<usize>> = None;
56
57    for (at, range, new) in replacements {
58        if let Some(prev) = &accepted {
59            if range.start < prev.end {
60                // Contained in a rewrite that already covers these bytes.
61                if range.end <= prev.end {
62                    continue;
63                }
64                return Err(RenderError::OverlappingSpans {
65                    at,
66                    first: prev.clone(),
67                    second: range,
68                });
69            }
70        }
71        out.push_str(&text[cursor..range.start]);
72        out.push_str(&new);
73        cursor = range.end;
74        accepted = Some(range);
75    }
76    out.push_str(&text[cursor..]);
77    Ok(out)
78}
79
80impl crate::stream::LogEntry {
81    /// Locate every field this entry carries, across its message and each raw
82    /// attached line.
83    ///
84    /// Recomputed per call and never stored — an entry nobody interrogates pays
85    /// nothing. Ranges index [`message`](crate::LogEntry::message) or the
86    /// attached line named by [`Field::at`], never a reassembled
87    /// [`Block`](crate::Block); [`uuid`](crate::LogEntry::uuid) is a field of the
88    /// entry rather than message text, so no span covers it.
89    ///
90    /// Ordering within one location is [`message_fields`]'s; locations follow
91    /// the message, then attached lines in order.
92    pub fn fields(&self) -> Vec<Field> {
93        let mut out = message_fields(&self.message);
94        for (i, line) in self.attached.iter().enumerate() {
95            out.extend(raw_line_fields(line, FieldLocation::Attached(i)));
96        }
97        out
98    }
99
100    /// Whether the logger's write buffer cut short the text this span indexes,
101    /// leaving the span itself incomplete.
102    ///
103    /// The cut always falls at the end of the text, so a span that stops before
104    /// it survived whole. A closed `[value]` is spanned inside its brackets and
105    /// so can never reach the end of its line; only a value the cut left
106    /// unterminated can. A span at a location this entry does not have is not
107    /// truncated — it indexes nothing here.
108    pub fn is_truncated(&self, field: &Field) -> bool {
109        if !self.cut_texts.contains(&field.at) {
110            return false;
111        }
112        let len = match field.at {
113            FieldLocation::Message => self.message.len(),
114            FieldLocation::Attached(i) => match self.attached.get(i) {
115                Some(line) => line.len(),
116                None => return false,
117            },
118        };
119        field.range.end == len
120    }
121
122    /// Rewrite this entry's fields, returning one string per render unit.
123    ///
124    /// `f` is called with each field and the text it covers; `None` leaves it
125    /// as it was. The message and each attached line are rewritten separately
126    /// because they are separate texts — see [`apply_fields`] for how nested and
127    /// overlapping replacements resolve.
128    ///
129    /// [`uuid`](crate::LogEntry::uuid), [`timestamp`](crate::LogEntry::timestamp)
130    /// and the rest of the header are entry fields rather than message text, so
131    /// a consumer rendering them handles them itself.
132    pub fn render_with(
133        &self,
134        f: impl Fn(&Field, &str) -> Option<String>,
135    ) -> Result<RenderedEntry, RenderError> {
136        let fields = self.fields();
137
138        let message = apply_fields(
139            &self.message,
140            fields.iter().filter(|x| x.at == FieldLocation::Message),
141            &f,
142        )?;
143
144        let attached = self
145            .attached
146            .iter()
147            .enumerate()
148            .map(|(i, line)| {
149                apply_fields(
150                    line,
151                    fields.iter().filter(|x| x.at == FieldLocation::Attached(i)),
152                    &f,
153                )
154            })
155            .collect::<Result<Vec<_>, _>>()?;
156
157        Ok(RenderedEntry { message, attached })
158    }
159}