Skip to main content

ferron_common/util/
parse_q_value_header_grouped.rs

1use 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        // The grouped vector is empty...
56        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}