Skip to main content

freeswitch_log_parser/fields/
kind.rs

1//! The span vocabulary: what a located field is, where it lives, and how a
2//! rewrite of it can fail.
3
4use std::fmt;
5use std::ops::Range;
6
7/// What a located span holds.
8///
9/// A kind names the *slot* the value sits in, not the value's shape — a
10/// [`CallerIdName`](FieldKind::CallerIdName) frequently holds a number, and the
11/// consumer decides what that means.
12#[non_exhaustive]
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub enum FieldKind {
15    /// An endpoint channel name (`sofia/<profile>/<user>@<host>`, `loopback/...`).
16    ChannelName,
17    /// Caller-id display name.
18    CallerIdName,
19    /// Caller-id number.
20    CallerIdNumber,
21    /// The number the dialplan is routing to.
22    DestinationNumber,
23    /// A SIP `Call-ID`.
24    CallId,
25    /// A channel UUID appearing anywhere in the text.
26    Uuid,
27    /// A SIP URI. No shape emits one yet — the variant is reserved for URI
28    /// positions FreeSWITCH itself frames; URIs in channel-variable values are
29    /// deliberately not classified here.
30    SipUri,
31    /// An IP address, at positions the classifier frames (a channel name's host,
32    /// an inbound INVITE's source).
33    IpAddr,
34}
35
36impl FieldKind {
37    /// The bare category string.
38    pub fn label(&self) -> &'static str {
39        match self {
40            FieldKind::ChannelName => "channel-name",
41            FieldKind::CallerIdName => "caller-id-name",
42            FieldKind::CallerIdNumber => "caller-id-number",
43            FieldKind::DestinationNumber => "destination-number",
44            FieldKind::CallId => "call-id",
45            FieldKind::Uuid => "uuid",
46            FieldKind::SipUri => "sip-uri",
47            FieldKind::IpAddr => "ip-addr",
48        }
49    }
50}
51
52impl fmt::Display for FieldKind {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        f.pad(self.label())
55    }
56}
57
58/// Which of an entry's texts a range indexes.
59///
60/// An entry has two coordinate systems: [`LogEntry::message`](crate::LogEntry)
61/// is header-stripped, while each attached line is the full physical line,
62/// session-UUID prefix included.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
64pub enum FieldLocation {
65    /// The entry's primary message.
66    Message,
67    /// The i-th attached line, indexed as [`AttachedLines::get`](crate::AttachedLines::get) takes it.
68    Attached(usize),
69}
70
71/// A located byte range and what it holds.
72///
73/// Ranges always index raw line text, never a reassembled [`Block`](crate::Block).
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct Field {
76    pub kind: FieldKind,
77    pub at: FieldLocation,
78    pub range: Range<usize>,
79}
80
81/// Why a rewrite could not be applied.
82///
83/// Every span handed to [`apply_fields`] is validated, replaced or not, so a
84/// malformed one fails the same way regardless of what the callback returns.
85#[non_exhaustive]
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum RenderError {
88    /// The range runs past the end of the text it was applied to.
89    OutOfBounds {
90        at: FieldLocation,
91        range: Range<usize>,
92        len: usize,
93    },
94    /// An endpoint falls inside a multi-byte character.
95    NotOnCharBoundary {
96        at: FieldLocation,
97        range: Range<usize>,
98    },
99    /// Two replaced spans overlap without one containing the other, so there is
100    /// no rewrite that honours both.
101    OverlappingSpans {
102        at: FieldLocation,
103        first: Range<usize>,
104        second: Range<usize>,
105    },
106}
107
108impl fmt::Display for RenderError {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        match self {
111            RenderError::OutOfBounds { at, range, len } => write!(
112                f,
113                "span {}..{} is past the end of {at} ({len} bytes)",
114                range.start, range.end
115            ),
116            RenderError::NotOnCharBoundary { at, range } => write!(
117                f,
118                "span {}..{} splits a character in {at}",
119                range.start, range.end
120            ),
121            RenderError::OverlappingSpans { at, first, second } => write!(
122                f,
123                "replaced spans {}..{} and {}..{} partially overlap in {at}",
124                first.start, first.end, second.start, second.end
125            ),
126        }
127    }
128}
129
130impl std::error::Error for RenderError {}
131
132impl fmt::Display for FieldLocation {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        match self {
135            FieldLocation::Message => f.pad("the message"),
136            FieldLocation::Attached(i) => write!(f, "attached line {i}"),
137        }
138    }
139}
140
141/// An entry's text after a rewrite, one string per render unit.
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct RenderedEntry {
144    pub message: String,
145    pub attached: Vec<String>,
146}
147/// Rank deciding which kind sorts first when two spans start together; the more
148/// specific kind wins, so a contained generic span follows its container.
149pub(super) fn kind_rank(kind: FieldKind) -> u8 {
150    match kind {
151        FieldKind::ChannelName => 0,
152        FieldKind::CallerIdName => 1,
153        FieldKind::CallerIdNumber => 2,
154        FieldKind::DestinationNumber => 3,
155        FieldKind::CallId => 4,
156        FieldKind::SipUri => 5,
157        FieldKind::IpAddr => 6,
158        FieldKind::Uuid => 7,
159    }
160}