Skip to main content

dpp_digital_link/linktype/
request.rs

1//! Resolution request and HTTP `Accept`-header media-type negotiation.
2
3use dpp_domain::AccessTier;
4
5use super::media_type::DppMediaType;
6use super::vocabulary::Gs1LinkType;
7
8/// A parsed resolution request combining the Digital Link with negotiation hints.
9#[derive(Debug, Clone)]
10pub struct ResolutionRequest {
11    /// The requested link type (from `?linkType=` query parameter).
12    pub link_type: Option<Gs1LinkType>,
13    /// The requested media type (from `Accept` header).
14    pub media_type: Option<DppMediaType>,
15    /// The access tier context (from authentication / credential).
16    /// `None` means public access.
17    pub access_tier: Option<AccessTier>,
18}
19
20impl ResolutionRequest {
21    /// Build a resolution request from an HTTP `Accept` header string.
22    ///
23    /// Parses q-values per RFC 9110 ยง12.4 and selects the highest-priority
24    /// non-wildcard media type. Wildcards (`*/*`, `application/*`) are
25    /// deprioritised below explicit types at the same q-value.
26    pub fn from_accept_header(accept: &str) -> Self {
27        Self {
28            link_type: None,
29            media_type: parse_best_media_type(accept),
30            access_tier: None,
31        }
32    }
33}
34
35/// Parse an RFC 9110 `Accept` header and return the highest-priority
36/// non-wildcard media type, or `None` for `*/*`-only requests.
37fn parse_best_media_type(accept: &str) -> Option<DppMediaType> {
38    let mut entries: Vec<(String, f32)> = accept
39        .split(',')
40        .filter_map(|entry| {
41            let mut parts = entry.trim().splitn(2, ';');
42            let media = parts.next()?.trim().to_owned();
43            if media.is_empty() {
44                return None;
45            }
46            let q = parts
47                .next()
48                .and_then(|p| p.trim().strip_prefix("q="))
49                .and_then(|q| q.parse::<f32>().ok())
50                .unwrap_or(1.0);
51            Some((media, q))
52        })
53        .collect();
54
55    // Sort by q descending; at equal q, explicit types sort before wildcards.
56    entries.sort_by(|a, b| {
57        b.1.partial_cmp(&a.1)
58            .unwrap_or(std::cmp::Ordering::Equal)
59            .then_with(|| {
60                let a_wild = a.0.contains('*');
61                let b_wild = b.0.contains('*');
62                match (a_wild, b_wild) {
63                    (false, true) => std::cmp::Ordering::Less,
64                    (true, false) => std::cmp::Ordering::Greater,
65                    _ => std::cmp::Ordering::Equal,
66                }
67            })
68    });
69
70    entries
71        .iter()
72        .filter(|(m, q)| *q > 0.0 && !m.contains('*'))
73        .map(|(m, _)| DppMediaType::parse(m))
74        .next()
75}