ferron_common/util/
parse_q_value_header_grouped.rs1use std::{cmp::Ordering, collections::BTreeSet, str::FromStr};
2
3#[derive(Debug, Clone, PartialEq)]
4struct HeaderValue {
5 value: String,
6 q_value: Option<f32>,
7}
8
9impl FromStr for HeaderValue {
10 type Err = ();
11
12 fn from_str(s: &str) -> Result<Self, Self::Err> {
13 let mut parts = s.split(';').take(2);
14 let value = parts.next().ok_or(())?.trim().to_string();
15
16 let q_value = parts.next().map(|part| {
17 part
18 .trim()
19 .strip_prefix("q=")
20 .unwrap_or("0")
21 .parse::<f32>()
22 .unwrap_or(0.0)
23 });
24
25 Ok(HeaderValue { value, q_value })
26 }
27}
28
29pub fn parse_q_value_header_grouped(header: &str) -> Vec<BTreeSet<String>> {
30 let mut values: Vec<HeaderValue> = header
31 .split(',')
32 .filter_map(|s| HeaderValue::from_str(s.trim()).ok())
33 .collect();
34
35 let mut last_some_q_value = None;
36 for value in values.iter_mut().rev() {
37 if value.q_value.is_none() {
38 value.q_value = Some(last_some_q_value.unwrap_or(1.0));
39 } else {
40 last_some_q_value = value.q_value;
41 }
42 }
43
44 values.sort_by(|a, b| b.q_value.partial_cmp(&a.q_value).unwrap_or(Ordering::Equal));
45
46 let mut grouped: Vec<BTreeSet<String>> = Vec::new();
47 if let Some(first) = values.first() {
48 grouped.push(BTreeSet::from([first.value.clone()]));
49 }
50 for (previous, current) in values.windows(2).map(|w| (&w[0], &w[1])) {
51 if current.q_value == previous.q_value {
52 if let Some(last) = grouped.last_mut() {
53 last.insert(current.value.clone());
54 } else {
55 grouped.push(BTreeSet::from([current.value.clone()]));
57 }
58 } else {
59 grouped.push(BTreeSet::from([current.value.clone()]));
60 }
61 }
62
63 grouped
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn test_parse_q_value_header() {
72 let header = "text/html; q=0.8, text/plain; q=0.5, text/xml; q=0.3";
73 let expected = vec![
74 BTreeSet::from(["text/html".to_string()]),
75 BTreeSet::from(["text/plain".to_string()]),
76 BTreeSet::from(["text/xml".to_string()]),
77 ];
78 assert_eq!(parse_q_value_header_grouped(header), expected);
79 }
80
81 #[test]
82 fn test_parse_q_value_header_with_out_of_order_and_sparse_q_values() {
83 let header = "text/html; q=0.8, application/javascript, text/javascript; q=0.4, text/plain; q=0.5, text/xml; q=0.3";
84 let expected = vec![
85 BTreeSet::from(["text/html".to_string()]),
86 BTreeSet::from(["text/plain".to_string()]),
87 BTreeSet::from(["application/javascript".to_string(), "text/javascript".to_string()]),
88 BTreeSet::from(["text/xml".to_string()]),
89 ];
90 assert_eq!(parse_q_value_header_grouped(header), expected);
91 }
92
93 #[test]
94 fn test_parse_q_value_header_single() {
95 let header = "text/html";
96 let expected = vec![BTreeSet::from(["text/html".to_string()])];
97 assert_eq!(parse_q_value_header_grouped(header), expected);
98 }
99}