1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//! Accept header parsing
use super::media_type::MediaType;
/// Represents an Accept header
#[derive(Debug, Clone)]
pub struct AcceptHeader {
/// Parsed media types sorted by quality factor (highest first).
pub media_types: Vec<MediaType>,
}
impl AcceptHeader {
/// Parses an Accept header string into an AcceptHeader struct
///
/// # Examples
///
/// ```
/// use reinhardt_core::negotiation::accept::AcceptHeader;
///
/// let accept = AcceptHeader::parse("application/json, text/html; q=0.9");
/// assert_eq!(accept.media_types.len(), 2);
/// assert_eq!(accept.media_types[0].quality, 1.0);
/// assert_eq!(accept.media_types[1].quality, 0.9);
///
/// let complex = AcceptHeader::parse("text/html, application/json; q=0.8, */*; q=0.1");
/// assert_eq!(complex.media_types.len(), 3);
/// // Sorted by quality
/// assert_eq!(complex.media_types[0].subtype, "html");
/// ```
pub fn parse(header: &str) -> Self {
let mut media_types: Vec<MediaType> = header
.split(',')
.filter_map(|s| MediaType::parse(s.trim()))
.collect();
// Sort by quality (highest first)
// Non-finite values are rejected at parse time; unwrap_or is a safety net
media_types.sort_by(|a, b| {
b.quality
.partial_cmp(&a.quality)
.unwrap_or(std::cmp::Ordering::Equal)
});
Self { media_types }
}
/// Creates an empty AcceptHeader with no media types
///
/// # Examples
///
/// ```
/// use reinhardt_core::negotiation::accept::AcceptHeader;
///
/// let empty = AcceptHeader::empty();
/// assert_eq!(empty.media_types.len(), 0);
/// ```
pub fn empty() -> Self {
Self {
media_types: Vec::new(),
}
}
/// Finds the best matching media type from available options
///
/// # Examples
///
/// ```
/// use reinhardt_core::negotiation::accept::AcceptHeader;
/// use reinhardt_core::negotiation::MediaType;
///
/// let accept = AcceptHeader::parse("application/json, text/html");
/// let available = vec![
/// MediaType::new("text", "html"),
/// MediaType::new("application", "xml"),
/// ];
/// let best = accept.find_best_match(&available);
/// assert!(best.is_some());
/// assert_eq!(best.unwrap().subtype, "html");
///
/// let no_match = AcceptHeader::parse("application/json");
/// let result = no_match.find_best_match(&available);
/// assert!(result.is_none());
/// ```
pub fn find_best_match(&self, available: &[MediaType]) -> Option<MediaType> {
for accepted in &self.media_types {
for available_type in available {
if accepted.matches(available_type) {
return Some(available_type.clone());
}
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[test]
fn test_parse_accept_header() {
let accept = AcceptHeader::parse("application/json, text/html; q=0.9");
assert_eq!(accept.media_types.len(), 2);
assert_eq!(accept.media_types[0].quality, 1.0);
}
#[test]
fn test_find_best_match() {
let accept = AcceptHeader::parse("application/json, text/html");
let available = vec![
MediaType::new("text", "html"),
MediaType::new("application", "xml"),
];
let best = accept.find_best_match(&available);
assert!(best.is_some());
}
#[rstest]
#[case("text/html;q=NaN", 0)]
#[case("text/html;q=NaN, application/json", 1)]
#[case("text/html, application/json;q=NaN", 1)]
fn test_parse_does_not_panic_on_nan_quality(#[case] input: &str, #[case] expected_len: usize) {
// Arrange
// (input provided by rstest case)
// Act
let accept = AcceptHeader::parse(input);
// Assert
assert_eq!(accept.media_types.len(), expected_len);
}
}