Skip to main content

dpp_calc/kernel/
ruleset.rs

1//! Ruleset identity and validity period types used across all `dpp-calc` calculators.
2
3use chrono::NaiveDate;
4use serde::{Deserialize, Serialize};
5
6/// Opaque machine-readable identifier for a regulatory ruleset.
7///
8/// Examples: `"repairability-heuristic-v1"`, `"battery-cfb"`. Always a
9/// compile-time literal in every concrete `Ruleset` impl in this crate — never
10/// constructed from external/deserialized input — hence `&'static str` rather
11/// than `String`: every ruleset's `id()`/`version()` becomes a plain `static`
12/// instead of `OnceLock`-wrapped lazy-init ceremony.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
14pub struct RulesetId(pub &'static str);
15
16/// Semver-shaped version for a ruleset, tracking parameter changes across
17/// delegated-act amendments.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
19pub struct RulesetVersion(pub &'static str);
20
21/// Whether, and from when, a ruleset version is legally in force.
22///
23/// The two variants are not "now" and "later" — they are "we know the date" and
24/// "the date is not knowable yet". EU staged obligations are routinely written
25/// as *"from <date> **or** N months after the entry into force of
26/// <act>, **whichever is the latest**"* — see Regulation (EU) 2023/1542
27/// Art. 7(1)–(3), Art. 8(1) and Art. 10(2)–(3). Until that act enters into
28/// force the application date is genuinely unknown, and asserting one anyway
29/// (a far-future sentinel, say) states a fact the regulation does not.
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub enum Effectivity {
32    /// In force from a known calendar date, until an optional end date.
33    InForce {
34        /// First day this ruleset version applies (inclusive).
35        from: NaiveDate,
36        /// Last day it applies (inclusive), or `None` if open-ended.
37        until: Option<NaiveDate>,
38    },
39    /// The instrument that will date this ruleset has not entered into force,
40    /// so no application date can be stated.
41    Pending {
42        /// The empowerment being waited on, e.g.
43        /// `"EU 2023/1542 Art. 10(5), first subparagraph"`.
44        empowerment: &'static str,
45        /// The Commission's own deadline to adopt, where the regulation sets
46        /// one. A deadline is **not** an application date — it can pass unmet,
47        /// and several already have.
48        adoption_deadline: Option<NaiveDate>,
49    },
50}
51
52impl Effectivity {
53    /// In force from `from`, open-ended.
54    #[must_use]
55    pub const fn open(from: NaiveDate) -> Self {
56        Self::InForce { from, until: None }
57    }
58
59    /// In force for a closed period.
60    #[must_use]
61    pub const fn closed(from: NaiveDate, until: NaiveDate) -> Self {
62        Self::InForce {
63            from,
64            until: Some(until),
65        }
66    }
67
68    /// Awaiting an instrument that has not entered into force.
69    #[must_use]
70    pub const fn pending(empowerment: &'static str, adoption_deadline: Option<NaiveDate>) -> Self {
71        Self::Pending {
72            empowerment,
73            adoption_deadline,
74        }
75    }
76
77    /// Whether this ruleset governs `date`.
78    ///
79    /// A [`Pending`](Self::Pending) ruleset governs no date at all — not even a
80    /// far-future one.
81    #[must_use]
82    pub fn is_active_on(&self, date: NaiveDate) -> bool {
83        match self {
84            Self::InForce { from, until } => date >= *from && until.is_none_or(|u| date <= u),
85            Self::Pending { .. } => false,
86        }
87    }
88
89    /// Error unless this ruleset governs `date`.
90    ///
91    /// Distinguishes three outcomes that must never be conflated: a ruleset that
92    /// is **not yet effective** on a known date, one that has **expired**, and
93    /// one whose date is **undetermined** because the instrument dating it has
94    /// not entered into force.
95    pub fn ensure_active_on(
96        &self,
97        id: &RulesetId,
98        date: NaiveDate,
99    ) -> Result<(), crate::error::CalcError> {
100        match self {
101            Self::Pending { empowerment, .. } => {
102                Err(crate::error::CalcError::RulesetUndetermined {
103                    id: id.0.to_owned(),
104                    empowerment: (*empowerment).to_owned(),
105                })
106            }
107            Self::InForce { from, until } => {
108                if date < *from {
109                    return Err(crate::error::CalcError::RulesetNotYetEffective {
110                        id: id.0.to_owned(),
111                        from: from.to_string(),
112                    });
113                }
114                if let Some(until) = until
115                    && date > *until
116                {
117                    return Err(crate::error::CalcError::RulesetExpired {
118                        id: id.0.to_owned(),
119                        until: until.to_string(),
120                    });
121                }
122                Ok(())
123            }
124        }
125    }
126}
127
128/// Structured legal citation for a regulatory ruleset.
129///
130/// Embedded in every [`Ruleset`] implementation so the authoritative source can
131/// be located programmatically — without reading source comments or external docs.
132/// This is the primary audit anchor for notified bodies.
133#[derive(Debug, Clone, Serialize)]
134pub struct RegulatoryBasis {
135    /// EU regulation or directive number (e.g. `"EU 2023/1669"`).
136    pub regulation: &'static str,
137    /// Relevant article and/or annex (e.g. `"Annex II, Annex III"`).
138    pub article: &'static str,
139    /// Harmonised standard used for the methodology (e.g. `"EN 45554:2021"`).
140    pub standard: Option<&'static str>,
141    /// JRC or other technical study underpinning the parameters (e.g. `"JRC128649"`).
142    pub technical_study: Option<&'static str>,
143    /// EUR-Lex or Official Journal URL for the authoritative source text.
144    pub source_url: Option<&'static str>,
145    /// Base ID of the successor ruleset version, if this one has been superseded.
146    /// Set when the ruleset's `Effectivity::InForce.until` is populated.
147    pub superseded_by: Option<&'static str>,
148}
149
150/// Common interface for all regulatory rulesets embedded in this crate.
151///
152/// There is deliberately no today-relative convenience method. A ruleset is
153/// selected against the date the governing law attached to the product — see
154/// [`AssessmentClock`](crate::clock::AssessmentClock) — and offering a
155/// `…_today()` helper alongside would make the wrong answer the shorter one to
156/// write.
157pub trait Ruleset {
158    fn id(&self) -> &RulesetId;
159    fn version(&self) -> &RulesetVersion;
160    fn effectivity(&self) -> &Effectivity;
161    /// Structured citation for the EU regulation that mandates this calculation.
162    fn regulatory_basis(&self) -> &RegulatoryBasis;
163}