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}