use super::act::Instrument;
use super::binding::InstrumentBinding;
use super::obligation::ObligationDate;
use super::reference::InstrumentRef;
use super::status::InstrumentStatus;
use crate::catalog::error::CatalogError;
use crate::catalog::granularity::Granularity;
use crate::catalog::retention::RetentionBasis;
struct EmbeddedInstrument {
id: &'static str,
json: &'static str,
}
const EMBEDDED: &[EmbeddedInstrument] = &[
EmbeddedInstrument {
id: "espr",
json: include_str!("../../instruments/espr.json"),
},
EmbeddedInstrument {
id: "espr-horizontal-repairability",
json: include_str!("../../instruments/espr-horizontal-repairability.json"),
},
EmbeddedInstrument {
id: "espr-horizontal-eee-recyclability",
json: include_str!("../../instruments/espr-horizontal-eee-recyclability.json"),
},
EmbeddedInstrument {
id: "battery-reg-2023-1542",
json: include_str!("../../instruments/battery-reg-2023-1542.json"),
},
EmbeddedInstrument {
id: "toy-safety-2025-2509",
json: include_str!("../../instruments/toy-safety-2025-2509.json"),
},
EmbeddedInstrument {
id: "detergents-2026-405",
json: include_str!("../../instruments/detergents-2026-405.json"),
},
EmbeddedInstrument {
id: "cpr-2024-3110",
json: include_str!("../../instruments/cpr-2024-3110.json"),
},
EmbeddedInstrument {
id: "elv-2026-1738",
json: include_str!("../../instruments/elv-2026-1738.json"),
},
EmbeddedInstrument {
id: "ecodesign-energy-labelling-mobile",
json: include_str!("../../instruments/ecodesign-energy-labelling-mobile.json"),
},
EmbeddedInstrument {
id: "ppwr-2025-40",
json: include_str!("../../instruments/ppwr-2025-40.json"),
},
EmbeddedInstrument {
id: "unsold-goods-format-2026-2",
json: include_str!("../../instruments/unsold-goods-format-2026-2.json"),
},
EmbeddedInstrument {
id: "unsold-goods-derogations-2026-296",
json: include_str!("../../instruments/unsold-goods-derogations-2026-296.json"),
},
];
pub const EMBEDDED_COUNT: usize = EMBEDDED.len();
pub struct InstrumentCatalog {
entries: Vec<Instrument>,
}
impl InstrumentCatalog {
#[must_use]
pub fn new() -> Self {
let entries = EMBEDDED
.iter()
.map(|m| {
let instrument: Instrument = serde_json::from_str(m.json).unwrap_or_else(|e| {
panic!("embedded instrument manifest '{}' is invalid: {e}", m.id)
});
assert_eq!(
instrument.id, m.id,
"manifest id '{}' does not match its file id '{}'",
instrument.id, m.id
);
instrument
})
.collect();
Self { entries }
}
#[must_use]
pub fn get(&self, id: &str) -> Option<&Instrument> {
self.entries.iter().find(|i| i.id == id)
}
#[must_use]
pub fn all(&self) -> &[Instrument] {
&self.entries
}
#[must_use]
pub fn bindings_for(&self, product_group: &str) -> Vec<(&Instrument, &InstrumentBinding)> {
self.entries
.iter()
.filter_map(|i| i.binding(product_group).map(|b| (i, b)))
.collect()
}
#[must_use]
pub fn determinable_for(&self, product_group: &str) -> Vec<(&Instrument, &InstrumentBinding)> {
self.bindings_for(product_group)
.into_iter()
.filter(|(_, b)| b.allows_determination())
.collect()
}
#[must_use]
pub fn passport_required_for(&self, product_group: &str) -> bool {
self.entries
.iter()
.any(|i| i.requires_passport_for(product_group))
}
#[must_use]
pub fn passport_obligation_live(&self, product_group: &str) -> bool {
!self.determinable_for(product_group).is_empty()
&& self.passport_required_for(product_group)
}
#[must_use]
pub fn passport_due_for(&self, product_group: &str) -> Option<&ObligationDate> {
self.entries
.iter()
.filter_map(|i| i.passport_for(product_group)?.applies_from())
.min_by(|a, b| a.date.cmp(&b.date))
}
#[must_use]
pub fn retention_for(&self, product_group: &str) -> Option<(u32, RetentionBasis)> {
let figures: Vec<(u32, RetentionBasis)> = self
.entries
.iter()
.filter_map(|i| i.retention_for(product_group))
.collect();
let years = figures.iter().map(|(y, _)| *y).max()?;
let basis = if figures.iter().all(|(_, b)| *b == RetentionBasis::Sourced) {
RetentionBasis::Sourced
} else {
RetentionBasis::Assumed
};
Some((years, basis))
}
#[must_use]
pub fn granularity_for(&self, product_group: &str) -> Option<Granularity> {
self.entries
.iter()
.filter_map(|i| i.granularity_for(product_group))
.reduce(Granularity::most_granular)
}
#[must_use]
pub fn instrument_refs_for(&self, product_group: &str) -> Vec<InstrumentRef> {
self.bindings_for(product_group)
.into_iter()
.map(|(instrument, _)| InstrumentRef::from_catalog(instrument.id.clone()))
.collect()
}
#[must_use]
pub fn product_group_keys(&self) -> Vec<&str> {
let mut keys: Vec<&str> = self
.entries
.iter()
.flat_map(|i| i.product_groups.iter())
.map(|b| b.product_group.as_str())
.collect();
keys.sort_unstable();
keys.dedup();
keys
}
#[must_use]
pub fn currency_checked_before(&self, date: &str) -> Vec<&Instrument> {
self.entries
.iter()
.filter(|i| i.status == InstrumentStatus::Adopted)
.filter(|i| i.currency.as_ref().is_none_or(|c| c.is_older_than(date)))
.collect()
}
#[must_use]
pub fn ids(&self) -> Vec<&str> {
let mut ids: Vec<&str> = self.entries.iter().map(|i| i.id.as_str()).collect();
ids.sort_unstable();
ids
}
pub fn register(&mut self, instrument: Instrument) -> Result<(), CatalogError> {
if self.get(&instrument.id).is_some() {
return Err(CatalogError::AlreadyExists(instrument.id));
}
self.entries.push(instrument);
Ok(())
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
impl Default for InstrumentCatalog {
fn default() -> Self {
Self::new()
}
}