Skip to main content

dpp_domain/product_group/
payload.rs

1//! [`ProductGroupPayload`] — what every product-group payload can answer about
2//! itself.
3//!
4//! # Why a trait and not a shared struct
5//!
6//! Eleven payloads declare a `gtin`, nine a `country_of_origin`, six a
7//! `recycled_content_pct`. That looks like duplication and mostly is not: the
8//! *obligation* differs. Eight acts require country of origin and put it in the
9//! schema's `required` block; the battery and electronics acts declare it and do
10//! not require it. A shared base has to pick one, and either choice writes a
11//! false statement about EU law into a Rust struct.
12//!
13//! So the sharing here is on **behaviour**, which is safe, rather than on
14//! **data**, which would encode a legal claim. Each payload keeps its own fields
15//! exactly as its act defines them and answers a common question about them.
16//!
17//! # No default implementations, deliberately
18//!
19//! Every method is required. A default returning `None` would let a newly added
20//! product group inherit "no model identifier" silently, and a registry would be
21//! told that as fact. Requiring the answer makes adding a group a compile error
22//! until someone has read the act and written it down — the same property the
23//! exhaustive `match` this trait replaced was protecting, but now stated in the
24//! group's own file instead of three matches away from it.
25
26/// The questions any product-group payload can answer, whatever its act.
27pub trait ProductGroupPayload {
28    /// The unique product identifier this record carries, in whichever
29    /// EN 18219 clause 5 scheme issued it.
30    ///
31    /// `None` is a real answer: a disclosure covering many products identifies
32    /// no single product the way every other group does.
33    ///
34    /// This is the method a group implements. [`Self::gtin`] is derived from it
35    /// and is not overridden anywhere — asking a payload for its GTIN is asking
36    /// a question only one of the three schemes can answer.
37    fn product_identifier(&self) -> Option<&crate::identifier::ProductIdentifier>;
38
39    /// The GS1 trade item number, when this record's identifier was issued
40    /// under EN 18219 **scheme 1** and not otherwise.
41    ///
42    /// 🚨 `None` now has two meanings and a caller has to tolerate both: this
43    /// group carries no identifier at all, **or** it carries one from scheme 2
44    /// or 3, which are self-issuing and have no GTIN. Before the identifier
45    /// became a scheme this could only mean the first, so a caller reading
46    /// `None` as "not a product" is now wrong — see
47    /// [`Self::product_identifier`] for the answer that always exists.
48    fn gtin(&self) -> Option<&str> {
49        self.product_identifier()?
50            .gtin()
51            .map(crate::identifier::Gtin::as_str)
52    }
53
54    /// The manufacturer's model identifier, where this group's act defines one.
55    ///
56    /// `None` means the act defines no such concept — not that the value is
57    /// missing.
58    fn model_identifier(&self) -> Option<&str>;
59
60    /// Substances of very high concern declared on this payload, where this
61    /// group's schema carries them.
62    ///
63    /// `None` means the schema has no such field — the group was never asked
64    /// for the declaration. `Some(&[])` means it was asked and answered
65    /// "none present", which is a different claim and the one REACH Art. 33
66    /// cares about. The SVHC lints run on the second and stay silent on the
67    /// first, so collapsing them would make an unasked group look cleared.
68    fn svhc_substances(&self) -> Option<&[crate::product_group::SvhcSubstance]>;
69
70    /// The product category this record declares, as the wire string the
71    /// catalog lists in `productCategories`.
72    ///
73    /// # Why the group has to answer this itself
74    ///
75    /// There is no common field to read. Seven groups carry a category and name
76    /// it seven different things — `batteryType`, `productFamily`,
77    /// `productType`, `productCategory`, `tyreClass` — because each act names
78    /// its own axis and none of them agreed to call it the same thing. Which
79    /// field *is* the category is a fact about the act, so it is answered in
80    /// the group's own file, exactly like [`Self::model_identifier`].
81    ///
82    /// # It must be the category axis, not merely a field with few values
83    ///
84    /// The answer is what a caller compares against a credential's declared
85    /// scope, so it has to be the axis that scope is about. A field that
86    /// partitions the group some *other* way — how the metal was made, how old
87    /// the child is — has a small enumerated domain and looks the part, and
88    /// answering with one would let a credential scoped to a category be
89    /// satisfied by something that is not a category. Where the act defines no
90    /// such axis the answer is `None`.
91    ///
92    /// The values themselves are constrained by the group's own schema enum,
93    /// the same gate every other validated field passes. Note that this is
94    /// **wider** than the catalog's `productCategories`, which is a curated
95    /// subset rather than the legal domain — a furniture passport may lawfully
96    /// be a `bed`, which the schema allows and the catalog does not list.
97    ///
98    /// `None` is a real answer, the same way it is for the other two: it says
99    /// this group has no category a passport can state, not that a value is
100    /// missing.
101    fn product_category(&self) -> Option<&str>;
102}