Skip to main content

dpp_digital_link/linktype/
vocabulary.rs

1//! GS1 Web Vocabulary link types for Digital Link resolution.
2
3use serde::{Deserialize, Serialize};
4
5/// GS1 defined link types for Digital Link resolution.
6///
7/// These follow the GS1 Web Vocabulary link type definitions.
8/// See: <https://ref.gs1.org/voc/linkTypes>
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub enum Gs1LinkType {
12    /// `gs1:pip` — Product Information Page (default for consumers).
13    ProductInformationPage,
14    /// `gs1:epil` — Electronic Product Information Leaflet.
15    ElectronicLeaflet,
16    /// `gs1:sustainabilityInfo` — Sustainability and environmental data.
17    SustainabilityInfo,
18    /// `gs1:recyclingInfo` — Recycling instructions and collection points.
19    RecyclingInfo,
20    /// `gs1:masterData` — Master data about the product (GS1 GDSN).
21    MasterData,
22    /// `gs1:certificationInfo` — Certification and conformity data.
23    CertificationInfo,
24    /// `gs1:instructions` — Usage, repair, or disassembly instructions.
25    Instructions,
26    /// `gs1:safetyInfo` — Safety data sheets, SVHC declarations.
27    SafetyInfo,
28    /// `gs1:traceability` — Supply chain traceability data.
29    Traceability,
30    /// `gs1:dpp` — EU Digital Product Passport (full DPP payload).
31    /// This is the ESPR-specific link type for accessing the complete DPP.
32    DigitalProductPassport,
33    /// `odal:predecessor` — the passport this record derives from (cross-operator
34    /// second-life successor linkage). Odal-owned vocabulary: the GS1 Web
35    /// Vocabulary has no lineage relation.
36    Predecessor,
37    /// `odal:successor` — the reverse of [`Self::Predecessor`]. Reserved: served
38    /// only once a reverse-lineage lookup exists to populate it.
39    Successor,
40    /// `odal:hasComponent` — a constituent passport in this product's bill of
41    /// materials (the assembly points down to a component). Odal-owned vocabulary.
42    HasComponent,
43    /// `odal:isComponentOf` — the reverse of [`Self::HasComponent`]. Reserved:
44    /// served only once a reverse component index exists to populate it.
45    IsComponentOf,
46    /// Custom / unknown link type (stored as the raw URI).
47    Custom(String),
48}
49
50impl Gs1LinkType {
51    /// Parse a link type from a GS1 vocabulary URI or shorthand.
52    pub fn parse(s: &str) -> Self {
53        match s {
54            "gs1:pip" | "https://ref.gs1.org/voc/pip" => Self::ProductInformationPage,
55            "gs1:epil" | "https://ref.gs1.org/voc/epil" => Self::ElectronicLeaflet,
56            "gs1:sustainabilityInfo" | "https://ref.gs1.org/voc/sustainabilityInfo" => {
57                Self::SustainabilityInfo
58            }
59            "gs1:recyclingInfo" | "https://ref.gs1.org/voc/recyclingInfo" => Self::RecyclingInfo,
60            "gs1:masterData" | "https://ref.gs1.org/voc/masterData" => Self::MasterData,
61            "gs1:certificationInfo" | "https://ref.gs1.org/voc/certificationInfo" => {
62                Self::CertificationInfo
63            }
64            "gs1:instructions" | "https://ref.gs1.org/voc/instructions" => Self::Instructions,
65            "gs1:safetyInfo" | "https://ref.gs1.org/voc/safetyInfo" => Self::SafetyInfo,
66            "gs1:traceability" | "https://ref.gs1.org/voc/traceability" => Self::Traceability,
67            "gs1:dpp" | "https://ref.gs1.org/voc/dpp" => Self::DigitalProductPassport,
68            "odal:predecessor" | "https://ref.odal-node.io/voc/predecessor" => Self::Predecessor,
69            "odal:successor" | "https://ref.odal-node.io/voc/successor" => Self::Successor,
70            "odal:hasComponent" | "https://ref.odal-node.io/voc/hasComponent" => Self::HasComponent,
71            "odal:isComponentOf" | "https://ref.odal-node.io/voc/isComponentOf" => {
72                Self::IsComponentOf
73            }
74            other => Self::Custom(other.to_owned()),
75        }
76    }
77
78    /// Return the canonical GS1 vocabulary URI for this link type.
79    pub fn as_gs1_uri(&self) -> &str {
80        match self {
81            Self::ProductInformationPage => "https://ref.gs1.org/voc/pip",
82            Self::ElectronicLeaflet => "https://ref.gs1.org/voc/epil",
83            Self::SustainabilityInfo => "https://ref.gs1.org/voc/sustainabilityInfo",
84            Self::RecyclingInfo => "https://ref.gs1.org/voc/recyclingInfo",
85            Self::MasterData => "https://ref.gs1.org/voc/masterData",
86            Self::CertificationInfo => "https://ref.gs1.org/voc/certificationInfo",
87            Self::Instructions => "https://ref.gs1.org/voc/instructions",
88            Self::SafetyInfo => "https://ref.gs1.org/voc/safetyInfo",
89            Self::Traceability => "https://ref.gs1.org/voc/traceability",
90            Self::DigitalProductPassport => "https://ref.gs1.org/voc/dpp",
91            Self::Predecessor => "https://ref.odal-node.io/voc/predecessor",
92            Self::Successor => "https://ref.odal-node.io/voc/successor",
93            Self::HasComponent => "https://ref.odal-node.io/voc/hasComponent",
94            Self::IsComponentOf => "https://ref.odal-node.io/voc/isComponentOf",
95            Self::Custom(uri) => uri.as_str(),
96        }
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn every_known_link_type_round_trips_via_canonical_uri() {
106        let all = [
107            Gs1LinkType::ProductInformationPage,
108            Gs1LinkType::ElectronicLeaflet,
109            Gs1LinkType::SustainabilityInfo,
110            Gs1LinkType::RecyclingInfo,
111            Gs1LinkType::MasterData,
112            Gs1LinkType::CertificationInfo,
113            Gs1LinkType::Instructions,
114            Gs1LinkType::SafetyInfo,
115            Gs1LinkType::Traceability,
116            Gs1LinkType::DigitalProductPassport,
117            Gs1LinkType::Predecessor,
118            Gs1LinkType::Successor,
119            Gs1LinkType::HasComponent,
120            Gs1LinkType::IsComponentOf,
121        ];
122        for lt in all {
123            let uri = lt.as_gs1_uri();
124            assert_eq!(Gs1LinkType::parse(uri), lt, "canonical URI must round-trip");
125        }
126    }
127
128    #[test]
129    fn shorthand_and_full_uris_parse_equivalently() {
130        assert_eq!(
131            Gs1LinkType::parse("gs1:dpp"),
132            Gs1LinkType::DigitalProductPassport
133        );
134        assert_eq!(
135            Gs1LinkType::parse("gs1:pip"),
136            Gs1LinkType::ProductInformationPage
137        );
138        assert_eq!(
139            Gs1LinkType::parse("gs1:safetyInfo"),
140            Gs1LinkType::SafetyInfo
141        );
142    }
143
144    #[test]
145    fn unknown_link_type_becomes_custom() {
146        let custom = Gs1LinkType::parse("https://example.com/voc/warranty");
147        assert_eq!(
148            custom,
149            Gs1LinkType::Custom("https://example.com/voc/warranty".to_owned())
150        );
151        assert_eq!(custom.as_gs1_uri(), "https://example.com/voc/warranty");
152    }
153}