use reqwest::header::HeaderMap;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct SpeakMetadata {
pub request_id: Option<String>,
pub model_name: Option<String>,
pub model_uuid: Option<String>,
pub additional_model_uuids: Option<Vec<String>>,
pub project_id: Option<String>,
pub breaks_applied: Option<u32>,
pub pronunciations_applied: Option<u32>,
pub char_count: Option<u32>,
pub content_type: Option<String>,
pub transfer_encoding: Option<String>,
pub date: Option<String>,
}
impl SpeakMetadata {
pub(crate) fn from_headers(headers: &HeaderMap) -> Self {
let get = |name: &str| {
headers
.get(name)
.and_then(|value| value.to_str().ok())
.map(str::to_owned)
};
SpeakMetadata {
request_id: get("dg-request-id"),
model_name: get("dg-model-name"),
model_uuid: get("dg-model-uuid"),
additional_model_uuids: get("dg-additional-model-uuids").map(|value| {
value
.split(',')
.map(str::trim)
.filter(|uuid| !uuid.is_empty())
.map(str::to_owned)
.collect()
}),
project_id: get("dg-project-id"),
breaks_applied: get("dg-breaks-applied").and_then(|value| value.parse().ok()),
pronunciations_applied: get("dg-pronunciations-applied")
.and_then(|value| value.parse().ok()),
char_count: get("dg-char-count").and_then(|value| value.parse().ok()),
content_type: get("content-type"),
transfer_encoding: get("transfer-encoding"),
date: get("date"),
}
}
pub fn request_id(&self) -> Option<&str> {
self.request_id.as_deref()
}
}
#[cfg(test)]
mod tests {
use reqwest::header::{HeaderMap, HeaderValue};
use super::SpeakMetadata;
type FieldAccessor = fn(&SpeakMetadata) -> Option<String>;
type HeaderCase = (&'static str, &'static str, FieldAccessor);
const HEADER_CASES: &[HeaderCase] = &[
(
"dg-request-id",
"550e8400-e29b-41d4-a716-446655440000",
|m| m.request_id.clone(),
),
("dg-model-name", "aura-2-thalia-en", |m| {
m.model_name.clone()
}),
(
"dg-model-uuid",
"1e9d0f4a-6b0e-4a3c-9a1b-2c3d4e5f6a7b",
|m| m.model_uuid.clone(),
),
("dg-char-count", "42", |m| {
m.char_count.map(|c| c.to_string())
}),
(
"dg-project-id",
"23eb6e85-c3f3-4d91-9595-f9f5b62741b8",
|m| m.project_id.clone(),
),
("dg-breaks-applied", "0", |m| {
m.breaks_applied.map(|c| c.to_string())
}),
("dg-pronunciations-applied", "2", |m| {
m.pronunciations_applied.map(|c| c.to_string())
}),
("content-type", "audio/mpeg", |m| m.content_type.clone()),
("transfer-encoding", "chunked", |m| {
m.transfer_encoding.clone()
}),
("date", "Thu, 11 Sep 2026 10:00:00 GMT", |m| m.date.clone()),
];
fn headers(pairs: &[(&'static str, &str)]) -> HeaderMap {
let mut headers = HeaderMap::new();
for (name, value) in pairs {
headers.insert(*name, HeaderValue::from_str(value).unwrap());
}
headers
}
#[test]
fn each_header_maps_to_exactly_one_field() {
for (name, value, field) in HEADER_CASES {
let metadata = SpeakMetadata::from_headers(&headers(&[(name, value)]));
assert_eq!(
field(&metadata),
Some((*value).to_owned()),
"header `{name}` should populate its field"
);
for (other_name, _, other_field) in HEADER_CASES {
if other_name != name {
assert_eq!(
other_field(&metadata),
None,
"header `{name}` must not populate the `{other_name}` field"
);
}
}
}
}
#[test]
fn all_headers_together() {
let pairs: Vec<(&'static str, &str)> = HEADER_CASES
.iter()
.map(|(name, value, _)| (*name, *value))
.collect();
let metadata = SpeakMetadata::from_headers(&headers(&pairs));
for (name, value, field) in HEADER_CASES {
assert_eq!(
field(&metadata),
Some((*value).to_owned()),
"header `{name}` should populate its field"
);
}
assert_eq!(metadata.char_count, Some(42));
assert_eq!(metadata.request_id(), Some(HEADER_CASES[0].1));
}
#[test]
fn header_names_are_case_insensitive() {
let mut map = HeaderMap::new();
map.insert("DG-Request-Id", HeaderValue::from_static("req-1"));
map.insert("Content-Type", HeaderValue::from_static("audio/wav"));
let metadata = SpeakMetadata::from_headers(&map);
assert_eq!(metadata.request_id.as_deref(), Some("req-1"));
assert_eq!(metadata.content_type.as_deref(), Some("audio/wav"));
}
#[test]
fn additional_model_uuids_splits_the_comma_separated_header() {
let metadata = SpeakMetadata::from_headers(&headers(&[(
"dg-additional-model-uuids",
"0ec06c9b-0aa0-44d0-a001-3ec57d32229e,2e5096c7-7bf1-435e-bbdd-f673f88d0ebd",
)]));
assert_eq!(
metadata.additional_model_uuids.as_deref(),
Some(
&[
"0ec06c9b-0aa0-44d0-a001-3ec57d32229e".to_owned(),
"2e5096c7-7bf1-435e-bbdd-f673f88d0ebd".to_owned(),
][..]
)
);
let metadata =
SpeakMetadata::from_headers(&headers(&[("dg-additional-model-uuids", " a , , b ")]));
assert_eq!(
metadata.additional_model_uuids.as_deref(),
Some(&["a".to_owned(), "b".to_owned()][..])
);
let metadata = SpeakMetadata::from_headers(&HeaderMap::new());
assert_eq!(metadata.additional_model_uuids, None);
}
#[test]
fn non_numeric_applied_counts_are_none() {
let metadata = SpeakMetadata::from_headers(&headers(&[
("dg-breaks-applied", "some"),
("dg-pronunciations-applied", "-1"),
]));
assert_eq!(metadata.breaks_applied, None);
assert_eq!(metadata.pronunciations_applied, None);
}
#[test]
fn missing_headers_yield_default() {
let metadata = SpeakMetadata::from_headers(&HeaderMap::new());
assert_eq!(metadata, SpeakMetadata::default());
assert_eq!(metadata.request_id(), None);
}
#[test]
fn non_numeric_char_count_is_none() {
let metadata = SpeakMetadata::from_headers(&headers(&[("dg-char-count", "lots")]));
assert_eq!(metadata.char_count, None);
let metadata = SpeakMetadata::from_headers(&headers(&[("dg-char-count", "-1")]));
assert_eq!(metadata.char_count, None);
}
}