Skip to main content

dpp_digital_link/linktype/
negotiate.rs

1//! Link descriptors and the content-negotiation algorithm.
2
3use dpp_domain::AccessTier;
4use serde::{Deserialize, Serialize};
5
6use super::media_type::DppMediaType;
7use super::request::ResolutionRequest;
8use super::vocabulary::Gs1LinkType;
9
10/// Describes one available representation of a DPP resource.
11///
12/// A resolver builds a list of these for each product, and the negotiation
13/// logic selects the best match for the request.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct LinkDescriptor {
17    /// The URL of this representation.
18    pub href: String,
19    /// Link type (GS1 vocabulary).
20    pub link_type: Gs1LinkType,
21    /// Media type served at this URL.
22    pub media_type: DppMediaType,
23    /// Minimum access tier required to view this resource.
24    pub min_access_tier: AccessTier,
25    /// Human-readable title.
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub title: Option<String>,
28    /// ISO 639-1 language code (e.g., `"en"`, `"de"`).
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub language: Option<String>,
31}
32
33/// Negotiate the best link descriptor for a given request.
34///
35/// Priority:
36/// 1. Match link type exactly (if specified).
37/// 2. Among matches, prefer the requested media type.
38/// 3. Filter by access tier (return only resources the caller can see).
39/// 4. If nothing matches, return `None`.
40pub fn negotiate<'a>(
41    available: &'a [LinkDescriptor],
42    request: &ResolutionRequest,
43) -> Option<&'a LinkDescriptor> {
44    let caller_tier = request.access_tier.as_ref().unwrap_or(&AccessTier::Public);
45
46    // Filter by access tier
47    let accessible: Vec<&LinkDescriptor> = available
48        .iter()
49        .filter(|d| d.min_access_tier <= *caller_tier)
50        .collect();
51
52    if accessible.is_empty() {
53        return None;
54    }
55
56    // If link type is specified, filter by it; no accessible match → None
57    let by_link_type: Vec<&LinkDescriptor> = if let Some(ref lt) = request.link_type {
58        let matched: Vec<_> = accessible
59            .iter()
60            .filter(|d| d.link_type == *lt)
61            .copied()
62            .collect();
63        if matched.is_empty() {
64            return None;
65        }
66        matched
67    } else {
68        accessible
69    };
70
71    let candidates = &by_link_type;
72
73    // If media type is specified, prefer it
74    if let Some(ref mt) = request.media_type
75        && let Some(exact) = candidates.iter().find(|d| d.media_type == *mt)
76    {
77        return Some(exact);
78    }
79
80    // Return first available candidate
81    candidates.first().copied()
82}