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}