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"`.
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct RulesetId(pub String);
11
12/// Semver-shaped version for a ruleset, tracking parameter changes across
13/// delegated-act amendments.
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct RulesetVersion(pub String);
16
17/// The calendar range within which a ruleset version is legally valid.
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19pub struct EffectiveDateBound {
20    /// First day this ruleset version applies (inclusive).
21    pub from: NaiveDate,
22    /// Last day this ruleset version applies (inclusive), or `None` if open-ended.
23    pub until: Option<NaiveDate>,
24}
25
26impl EffectiveDateBound {
27    pub fn open(from: NaiveDate) -> Self {
28        Self { from, until: None }
29    }
30
31    pub fn is_active_on(&self, date: NaiveDate) -> bool {
32        date >= self.from && self.until.is_none_or(|u| date <= u)
33    }
34
35    /// Error if `date` falls outside the effective period, distinguishing a
36    /// ruleset that is **not yet effective** (before `from`) from one that has
37    /// **expired** (after `until`) — so a pending `2100` stub is never reported
38    /// as "expired".
39    pub fn ensure_active_on(
40        &self,
41        id: &RulesetId,
42        date: NaiveDate,
43    ) -> Result<(), crate::error::CalcError> {
44        if date < self.from {
45            return Err(crate::error::CalcError::RulesetNotYetEffective {
46                id: id.0.clone(),
47                from: self.from.to_string(),
48            });
49        }
50        if let Some(until) = self.until
51            && date > until
52        {
53            return Err(crate::error::CalcError::RulesetExpired {
54                id: id.0.clone(),
55                until: until.to_string(),
56            });
57        }
58        Ok(())
59    }
60}
61
62/// Structured legal citation for a regulatory ruleset.
63///
64/// Embedded in every [`Ruleset`] implementation so the authoritative source can
65/// be located programmatically — without reading source comments or external docs.
66/// This is the primary audit anchor for notified bodies.
67#[derive(Debug, Clone, Serialize)]
68pub struct RegulatoryBasis {
69    /// EU regulation or directive number (e.g. `"EU 2023/1669"`).
70    pub regulation: &'static str,
71    /// Relevant article and/or annex (e.g. `"Annex II, Annex III"`).
72    pub article: &'static str,
73    /// Harmonised standard used for the methodology (e.g. `"EN 45554:2021"`).
74    pub standard: Option<&'static str>,
75    /// JRC or other technical study underpinning the parameters (e.g. `"JRC128649"`).
76    pub technical_study: Option<&'static str>,
77    /// EUR-Lex or Official Journal URL for the authoritative source text.
78    pub source_url: Option<&'static str>,
79    /// Base ID of the successor ruleset version, if this one has been superseded.
80    /// Set when `effective_dates.until` is populated.
81    pub superseded_by: Option<&'static str>,
82}
83
84/// Common interface for all regulatory rulesets embedded in this crate.
85pub trait Ruleset {
86    fn id(&self) -> &RulesetId;
87    fn version(&self) -> &RulesetVersion;
88    fn effective_dates(&self) -> &EffectiveDateBound;
89    /// Structured citation for the EU regulation that mandates this calculation.
90    fn regulatory_basis(&self) -> &RegulatoryBasis;
91}