Skip to content

Commit b13d9f6

Browse files
committed
implement server timing entry parsing
1 parent e4efa9d commit b13d9f6

File tree

1 file changed

+157
-3
lines changed

1 file changed

+157
-3
lines changed

src/trace/server_timing/entry.rs

Lines changed: 157 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
use std::str::FromStr;
12
use std::time::Duration;
23

34
use crate::headers::HeaderValue;
5+
use crate::Status;
6+
use crate::{ensure, format_err};
47

58
/// An individual `ServerTiming` entry.
69
//
@@ -47,8 +50,8 @@ impl Entry {
4750
}
4851

4952
/// The timing description.
50-
pub fn description(&self) -> Option<&String> {
51-
self.desc.as_ref()
53+
pub fn description(&self) -> Option<&str> {
54+
self.desc.as_ref().map(|s| s.as_str())
5255
}
5356
}
5457

@@ -73,12 +76,93 @@ impl From<Entry> for HeaderValue {
7376
}
7477
}
7578

79+
impl FromStr for Entry {
80+
type Err = crate::Error;
81+
// Create an entry from a string. Parsing rules in ABNF are:
82+
//
83+
// ```
84+
// Server-Timing = #server-timing-metric
85+
// server-timing-metric = metric-name *( OWS ";" OWS server-timing-param )
86+
// metric-name = token
87+
// server-timing-param = server-timing-param-name OWS "=" OWS server-timing-param-value
88+
// server-timing-param-name = token
89+
// server-timing-param-value = token / quoted-string
90+
// ```
91+
//
92+
// Source: https://w3c.github.io/server-timing/#the-server-timing-header-field
93+
fn from_str(s: &str) -> Result<Self, Self::Err> {
94+
let mut parts = s.trim().split(';');
95+
96+
// Get the name. This is non-optional.
97+
let name = parts
98+
.next()
99+
.ok_or_else(|| format_err!("Server timing headers must include a name"))?
100+
.trim_end();
101+
102+
// We must extract these values from the k-v pairs that follow.
103+
let mut dur = None;
104+
let mut desc = None;
105+
106+
for mut part in parts {
107+
ensure!(
108+
!part.is_empty(),
109+
"Server timing params cannot end with a trailing `;`"
110+
);
111+
112+
part = part.trim_start();
113+
114+
let mut params = part.split('=');
115+
let name = params
116+
.next()
117+
.ok_or_else(|| format_err!("Server timing params must have a name"))?
118+
.trim_end();
119+
let mut value = params
120+
.next()
121+
.ok_or_else(|| format_err!("Server timing params must have a value"))?
122+
.trim_start();
123+
124+
match name {
125+
"dur" => {
126+
let millis: f64 = value.parse().status(400).map_err(|_| {
127+
format_err!("Server timing duration params must be a valid double-precision floating-point number.")
128+
})?;
129+
dur = Some(Duration::from_secs_f64(millis / 1000.0));
130+
}
131+
"desc" => {
132+
// Ensure quotes line up, and strip them from the resulting output
133+
if value.starts_with('"') {
134+
value = &value[1..value.len()];
135+
ensure!(
136+
value.ends_with('"'),
137+
"Server timing description params must use matching quotes"
138+
);
139+
value = &value[0..value.len() - 1];
140+
} else {
141+
ensure!(
142+
!value.ends_with('"'),
143+
"Server timing description params must use matching quotes"
144+
);
145+
}
146+
desc = Some(value.to_string());
147+
}
148+
_ => continue,
149+
}
150+
}
151+
152+
Ok(Entry {
153+
name: name.to_string(),
154+
dur,
155+
desc,
156+
})
157+
}
158+
}
159+
76160
#[cfg(test)]
77161
mod test {
78162
use super::*;
79163

80164
#[test]
81-
fn create_header_value() -> crate::Result<()> {
165+
fn encode() -> crate::Result<()> {
82166
let name = String::from("Server");
83167
let dur = Duration::from_secs(1);
84168
let desc = String::from("A server timing");
@@ -89,8 +173,78 @@ mod test {
89173
let val: HeaderValue = Entry::new(name.clone(), Some(dur), None)?.into();
90174
assert_eq!(val, "Server; dur=1000");
91175

176+
let val: HeaderValue = Entry::new(name.clone(), None, Some(desc.clone()))?.into();
177+
assert_eq!(val, r#"Server; desc="A server timing""#);
178+
92179
let val: HeaderValue = Entry::new(name.clone(), Some(dur), Some(desc.clone()))?.into();
93180
assert_eq!(val, r#"Server; dur=1000; desc="A server timing""#);
94181
Ok(())
95182
}
183+
184+
#[test]
185+
fn decode() -> crate::Result<()> {
186+
// Metric name only.
187+
assert_entry("Server", "Server", None, None)?;
188+
assert_entry("Server ", "Server", None, None)?;
189+
assert_entry_err(
190+
"Server ;",
191+
"Server timing params cannot end with a trailing `;`",
192+
);
193+
assert_entry_err(
194+
"Server; ",
195+
"Server timing params cannot end with a trailing `;`",
196+
);
197+
198+
// Metric name + param
199+
assert_entry("Server; dur=1000", "Server", Some(1000), None)?;
200+
assert_entry("Server; dur =1000", "Server", Some(1000), None)?;
201+
assert_entry("Server; dur= 1000", "Server", Some(1000), None)?;
202+
assert_entry("Server; dur = 1000", "Server", Some(1000), None)?;
203+
assert_entry_err(
204+
"Server; dur=1000;",
205+
"Server timing params cannot end with a trailing `;`",
206+
);
207+
208+
// Metric name + desc
209+
assert_entry(r#"DB; desc="a db""#, "DB", None, Some("a db"))?;
210+
assert_entry(r#"DB; desc ="a db""#, "DB", None, Some("a db"))?;
211+
assert_entry(r#"DB; desc= "a db""#, "DB", None, Some("a db"))?;
212+
assert_entry(r#"DB; desc = "a db""#, "DB", None, Some("a db"))?;
213+
assert_entry(r#"DB; desc=a_db"#, "DB", None, Some("a_db"))?;
214+
assert_entry_err(
215+
r#"DB; desc="db"#,
216+
"Server timing description params must use matching quotes",
217+
);
218+
assert_entry_err(
219+
"Server; desc=a_db;",
220+
"Server timing params cannot end with a trailing `;`",
221+
);
222+
223+
// Metric name + dur + desc
224+
assert_entry(
225+
r#"Server; dur=1000; desc="a server""#,
226+
"Server",
227+
Some(1000),
228+
Some("a server"),
229+
)?;
230+
assert_entry_err(
231+
r#"Server; dur=1000; desc="a server";"#,
232+
"Server timing params cannot end with a trailing `;`",
233+
);
234+
Ok(())
235+
}
236+
237+
fn assert_entry_err(s: &str, msg: &str) {
238+
let err = Entry::from_str(s).unwrap_err();
239+
assert_eq!(format!("{}", err), msg);
240+
}
241+
242+
/// Assert an entry and all of its fields.
243+
fn assert_entry(s: &str, n: &str, du: Option<u64>, de: Option<&str>) -> crate::Result<()> {
244+
let e = Entry::from_str(s)?;
245+
assert_eq!(e.name(), n);
246+
assert_eq!(e.duration(), du.map(|du| Duration::from_millis(du)));
247+
assert_eq!(e.description(), de);
248+
Ok(())
249+
}
96250
}

0 commit comments

Comments
 (0)