1use std::collections::BTreeMap;
2
3use ciborium::Value;
4
5use crate::error::MoqTraceError;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum Perspective {
10 Client,
12 Server,
14 Observer,
16}
17
18impl Perspective {
19 fn as_str(self) -> &'static str {
20 match self {
21 Perspective::Client => "client",
22 Perspective::Server => "server",
23 Perspective::Observer => "observer",
24 }
25 }
26
27 fn from_str(s: &str) -> Result<Self, MoqTraceError> {
28 match s {
29 "client" => Ok(Perspective::Client),
30 "server" => Ok(Perspective::Server),
31 "observer" => Ok(Perspective::Observer),
32 other => Err(MoqTraceError::InvalidHeader(format!("unknown perspective: {other}"))),
33 }
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
41pub enum DetailLevel {
42 Control,
44 Headers,
46 HeadersSizes,
48 HeadersData,
50 Full,
52}
53
54impl DetailLevel {
55 fn as_str(self) -> &'static str {
56 match self {
57 DetailLevel::Control => "control",
58 DetailLevel::Headers => "headers",
59 DetailLevel::HeadersSizes => "headers+sizes",
60 DetailLevel::HeadersData => "headers+data",
61 DetailLevel::Full => "full",
62 }
63 }
64
65 fn from_str(s: &str) -> Result<Self, MoqTraceError> {
66 match s {
67 "control" => Ok(DetailLevel::Control),
68 "headers" => Ok(DetailLevel::Headers),
69 "headers+sizes" => Ok(DetailLevel::HeadersSizes),
70 "headers+data" => Ok(DetailLevel::HeadersData),
71 "full" => Ok(DetailLevel::Full),
72 other => Err(MoqTraceError::InvalidHeader(format!("unknown detail level: {other}"))),
73 }
74 }
75}
76
77#[derive(Debug, Clone, PartialEq)]
79pub struct TraceHeader {
80 pub protocol: String,
82 pub perspective: Perspective,
84 pub detail: DetailLevel,
86 pub start_time: u64,
88 pub end_time: Option<u64>,
91 pub transport: Option<String>,
93 pub source: Option<String>,
95 pub endpoint: Option<String>,
97 pub session_id: Option<String>,
99 pub custom: Option<BTreeMap<String, Value>>,
101}
102
103impl From<&TraceHeader> for Value {
104 fn from(h: &TraceHeader) -> Self {
105 let mut pairs: Vec<(Value, Value)> = vec![
106 (Value::Text("protocol".into()), Value::Text(h.protocol.clone())),
107 (Value::Text("perspective".into()), Value::Text(h.perspective.as_str().into())),
108 (Value::Text("detail".into()), Value::Text(h.detail.as_str().into())),
109 (Value::Text("startTime".into()), Value::Integer(h.start_time.into())),
110 ];
111
112 if let Some(end_time) = h.end_time {
113 pairs.push((Value::Text("endTime".into()), Value::Integer(end_time.into())));
114 }
115 if let Some(ref transport) = h.transport {
116 pairs.push((Value::Text("transport".into()), Value::Text(transport.clone())));
117 }
118 if let Some(ref source) = h.source {
119 pairs.push((Value::Text("source".into()), Value::Text(source.clone())));
120 }
121 if let Some(ref endpoint) = h.endpoint {
122 pairs.push((Value::Text("endpoint".into()), Value::Text(endpoint.clone())));
123 }
124 if let Some(ref session_id) = h.session_id {
125 pairs.push((Value::Text("sessionId".into()), Value::Text(session_id.clone())));
126 }
127 if let Some(ref custom) = h.custom {
128 let custom_pairs: Vec<(Value, Value)> =
129 custom.iter().map(|(k, v)| (Value::Text(k.clone()), v.clone())).collect();
130 pairs.push((Value::Text("custom".into()), Value::Map(custom_pairs)));
131 }
132
133 Value::Map(pairs)
134 }
135}
136
137impl TryFrom<Value> for TraceHeader {
138 type Error = MoqTraceError;
139
140 fn try_from(value: Value) -> Result<Self, MoqTraceError> {
141 let pairs = match value {
142 Value::Map(pairs) => pairs,
143 _ => return Err(MoqTraceError::InvalidHeader("header is not a CBOR map".into())),
144 };
145
146 let get_text = |pairs: &[(Value, Value)], key: &str| -> Option<String> {
147 pairs.iter().find_map(|(k, v)| {
148 if k.as_text() == Some(key) {
149 v.as_text().map(|s| s.to_string())
150 } else {
151 None
152 }
153 })
154 };
155
156 let get_integer = |pairs: &[(Value, Value)], key: &str| -> Option<u64> {
157 pairs.iter().find_map(|(k, v)| {
158 if k.as_text() == Some(key) {
159 v.as_integer().and_then(|i| u64::try_from(i).ok())
160 } else {
161 None
162 }
163 })
164 };
165
166 let protocol = get_text(&pairs, "protocol")
167 .ok_or_else(|| MoqTraceError::InvalidHeader("missing 'protocol'".into()))?;
168 let perspective_str = get_text(&pairs, "perspective")
169 .ok_or_else(|| MoqTraceError::InvalidHeader("missing 'perspective'".into()))?;
170 let detail_str = get_text(&pairs, "detail")
171 .ok_or_else(|| MoqTraceError::InvalidHeader("missing 'detail'".into()))?;
172 let start_time = get_integer(&pairs, "startTime")
173 .ok_or_else(|| MoqTraceError::InvalidHeader("missing 'startTime'".into()))?;
174
175 let custom = pairs.iter().find_map(|(k, v)| {
176 if k.as_text() == Some("custom") {
177 if let Value::Map(custom_pairs) = v {
178 let map: BTreeMap<String, Value> = custom_pairs
179 .iter()
180 .filter_map(|(ck, cv)| ck.as_text().map(|s| (s.to_string(), cv.clone())))
181 .collect();
182 Some(map)
183 } else {
184 None
185 }
186 } else {
187 None
188 }
189 });
190
191 Ok(TraceHeader {
192 protocol,
193 perspective: Perspective::from_str(&perspective_str)?,
194 detail: DetailLevel::from_str(&detail_str)?,
195 start_time,
196 end_time: get_integer(&pairs, "endTime"),
197 transport: get_text(&pairs, "transport"),
198 source: get_text(&pairs, "source"),
199 endpoint: get_text(&pairs, "endpoint"),
200 session_id: get_text(&pairs, "sessionId"),
201 custom,
202 })
203 }
204}