dpp_domain/catalog/granularity.rs
1//! [`Granularity`] — the level a passport describes, as fixed by an act.
2
3use serde::{Deserialize, Serialize};
4
5/// The level at which a passport describes a product: a model, a production
6/// batch, or one physical unit.
7///
8/// # Why this lives in the catalog and not in a port
9///
10/// ESPR Art. 9(2)(d) makes the level a **delegated-act decision** — "whether the
11/// digital product passport is to be established at model, batch or item level".
12/// It is therefore a property of the instrument that imposes the passport, and
13/// every consumer downstream is reading that decision rather than making one.
14/// The EU registry is one such consumer, which is why
15/// [`RegistrationGranularity`](crate::ports::registry_sync::RegistrationGranularity)
16/// exists and converts from this type rather than the other way round.
17///
18/// No `Default`, deliberately: an act that has not fixed a level is
19/// [`Option::None`] on the instrument, not a guess at item level. Defaulting
20/// here would silently answer a question only a delegated act can answer.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
22#[serde(rename_all = "lowercase")]
23#[non_exhaustive]
24pub enum Granularity {
25 /// One passport covering every unit sharing a model's specifications.
26 Model,
27 /// One passport covering every unit made in one production run.
28 Batch,
29 /// One passport per physical unit.
30 Item,
31}
32
33impl Granularity {
34 /// The more granular of two levels.
35 ///
36 /// The ordering `Model < Batch < Item` is the derived `Ord`, and matches the
37 /// rule the EU registry applies: where several levels are indicated for one
38 /// product, the most granular wins (IR (EU) 2026/1778 Art. 8(3), and Art.
39 /// 8(4)–(5) linking an item registration back up to batch and model).
40 ///
41 /// This is why applicable instruments fold to a **maximum** rather than
42 /// picking one: two acts naming different levels do not conflict, they
43 /// compound.
44 #[must_use]
45 pub fn most_granular(self, other: Self) -> Self {
46 self.max(other)
47 }
48
49 /// Whether a passport at this level describes a **specification** rather
50 /// than manufactured things.
51 ///
52 /// [`Model`](Self::Model) covers every unit sharing a model's
53 /// specifications, so it describes a type. [`Batch`](Self::Batch) and
54 /// [`Item`](Self::Item) both describe things that were made — one run, or
55 /// one unit.
56 ///
57 /// # Why this is answered here
58 ///
59 /// A projection that has to classify the asset it is describing needs it,
60 /// and this enum is `#[non_exhaustive]`, so a match from another crate must
61 /// carry a wildcard. A wildcard would quietly file a future level as one
62 /// answer or the other — and "not stated" is a distinction this type takes
63 /// care to preserve elsewhere, so it should not be thrown away here.
64 /// Authored beside the variants, the match is exhaustive.
65 #[must_use]
66 pub const fn describes_a_type(self) -> bool {
67 match self {
68 Self::Model => true,
69 Self::Batch | Self::Item => false,
70 }
71 }
72}