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 /// A dump slot's value — a channel variable's or a channel field's — where
35 /// the name names no slot above. The crate locates it and stops there;
36 /// whether the name makes the value sensitive is the consumer's call, from
37 /// the name the classification carries.
38 VariableValue,
39}
40
41impl FieldKind {
42 /// The bare category string.
43 pub fn label(&self) -> &'static str {
44 match self {
45 FieldKind::ChannelName => "channel-name",
46 FieldKind::CallerIdName => "caller-id-name",
47 FieldKind::CallerIdNumber => "caller-id-number",
48 FieldKind::DestinationNumber => "destination-number",
49 FieldKind::CallId => "call-id",
50 FieldKind::Uuid => "uuid",
51 FieldKind::SipUri => "sip-uri",
52 FieldKind::IpAddr => "ip-addr",
53 FieldKind::VariableValue => "variable-value",
54 }
55 }
56}
57
58impl fmt::Display for FieldKind {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 f.pad(self.label())
61 }
62}
63
64/// Which of an entry's texts a range indexes.
65///
66/// An entry has two coordinate systems: [`LogEntry::message`](crate::LogEntry)
67/// is header-stripped, while each attached line is the full physical line,
68/// session-UUID prefix included.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
70pub enum FieldLocation {
71 /// The entry's primary message.
72 Message,
73 /// The i-th attached line, indexed as [`AttachedLines::get`](crate::AttachedLines::get) takes it.
74 Attached(usize),
75}
76
77/// A located byte range and what it holds.
78///
79/// Ranges always index raw line text, never a reassembled [`Block`](crate::Block).
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct Field {
82 pub kind: FieldKind,
83 pub at: FieldLocation,
84 pub range: Range<usize>,
85}
86
87/// Why a rewrite could not be applied.
88///
89/// Every span handed to [`apply_fields`] is validated, replaced or not, so a
90/// malformed one fails the same way regardless of what the callback returns.
91#[non_exhaustive]
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub enum RenderError {
94 /// The range runs past the end of the text it was applied to.
95 OutOfBounds {
96 at: FieldLocation,
97 range: Range<usize>,
98 len: usize,
99 },
100 /// An endpoint falls inside a multi-byte character.
101 NotOnCharBoundary {
102 at: FieldLocation,
103 range: Range<usize>,
104 },
105 /// Two replaced spans overlap without one containing the other, so there is
106 /// no rewrite that honours both.
107 OverlappingSpans {
108 at: FieldLocation,
109 first: Range<usize>,
110 second: Range<usize>,
111 },
112}
113
114impl fmt::Display for RenderError {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 match self {
117 RenderError::OutOfBounds { at, range, len } => write!(
118 f,
119 "span {}..{} is past the end of {at} ({len} bytes)",
120 range.start, range.end
121 ),
122 RenderError::NotOnCharBoundary { at, range } => write!(
123 f,
124 "span {}..{} splits a character in {at}",
125 range.start, range.end
126 ),
127 RenderError::OverlappingSpans { at, first, second } => write!(
128 f,
129 "replaced spans {}..{} and {}..{} partially overlap in {at}",
130 first.start, first.end, second.start, second.end
131 ),
132 }
133 }
134}
135
136impl std::error::Error for RenderError {}
137
138impl fmt::Display for FieldLocation {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 match self {
141 FieldLocation::Message => f.pad("the message"),
142 FieldLocation::Attached(i) => write!(f, "attached line {i}"),
143 }
144 }
145}
146
147/// An entry's text after a rewrite, one string per render unit.
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct RenderedEntry {
150 pub message: String,
151 pub attached: Vec<String>,
152}
153/// Rank deciding which kind sorts first when two spans start together; the more
154/// specific kind wins, so a contained generic span follows its container.
155pub(super) fn kind_rank(kind: FieldKind) -> u8 {
156 match kind {
157 FieldKind::ChannelName => 0,
158 FieldKind::CallerIdName => 1,
159 FieldKind::CallerIdNumber => 2,
160 FieldKind::DestinationNumber => 3,
161 FieldKind::CallId => 4,
162 FieldKind::SipUri => 5,
163 FieldKind::IpAddr => 6,
164 FieldKind::VariableValue => 7,
165 FieldKind::Uuid => 8,
166 }
167}