use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum BatteryChemistry {
#[serde(rename = "LFP")]
Lfp,
#[serde(rename = "NMC")]
Nmc,
#[serde(rename = "NCA")]
Nca,
#[serde(rename = "LCO")]
Lco,
#[serde(rename = "NiMH")]
NiMh,
#[serde(rename = "NiCd")]
NiCd,
#[serde(rename = "lead-acid")]
LeadAcid,
#[serde(rename = "solid-state")]
SolidState,
#[serde(other)]
Other,
}
impl BatteryChemistry {
pub const fn wire_str(&self) -> &'static str {
match self {
Self::Lfp => "LFP",
Self::Nmc => "NMC",
Self::Nca => "NCA",
Self::Lco => "LCO",
Self::NiMh => "NiMH",
Self::NiCd => "NiCd",
Self::LeadAcid => "lead-acid",
Self::SolidState => "solid-state",
Self::Other => "Other",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn battery_chemistry_wire_str_matches_serde_serialization() {
for chem in [
BatteryChemistry::Lfp,
BatteryChemistry::Nmc,
BatteryChemistry::Nca,
BatteryChemistry::Lco,
BatteryChemistry::NiMh,
BatteryChemistry::NiCd,
BatteryChemistry::LeadAcid,
BatteryChemistry::SolidState,
BatteryChemistry::Other,
] {
let serialized = serde_json::to_value(&chem).unwrap();
assert_eq!(
serialized.as_str().unwrap(),
chem.wire_str(),
"wire_str() disagrees with serde for {chem:?}"
);
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum BatteryType {
Portable,
Industrial,
Ev,
Lmt,
#[serde(rename = "starting-lighting-ignition")]
Sli,
#[serde(other)]
Other,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CarbonFootprintClassError {
#[error("carbon footprint class label must not be empty or blank")]
Empty,
#[error(
"carbon footprint class label '{label}' is {len} characters, \
exceeding the maximum of {max}"
)]
TooLong {
label: String,
len: usize,
max: usize,
},
#[error("carbon footprint class label '{0}' contains a control character")]
ControlCharacter(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct CarbonFootprintClass(String);
impl CarbonFootprintClass {
pub const MAX_LEN: usize = 8;
pub fn new(label: impl Into<String>) -> Result<Self, CarbonFootprintClassError> {
let label = label.into();
if label.trim().is_empty() {
return Err(CarbonFootprintClassError::Empty);
}
if label.chars().any(char::is_control) {
return Err(CarbonFootprintClassError::ControlCharacter(label));
}
let len = label.chars().count();
if len > Self::MAX_LEN {
return Err(CarbonFootprintClassError::TooLong {
label,
len,
max: Self::MAX_LEN,
});
}
Ok(Self(label))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for CarbonFootprintClass {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl TryFrom<String> for CarbonFootprintClass {
type Error = CarbonFootprintClassError;
fn try_from(label: String) -> Result<Self, Self::Error> {
Self::new(label)
}
}
impl From<CarbonFootprintClass> for String {
fn from(class: CarbonFootprintClass) -> Self {
class.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum EnergyEfficiencyClass {
A,
B,
C,
D,
E,
F,
G,
#[serde(other)]
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum ProductionRoute {
BlastFurnace,
ElectricArc,
DirectReduction,
Primary,
SecondaryRecycled,
Mixed,
#[serde(other)]
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum LifecycleStage {
CradleToGate,
CradleToGrave,
CradleToCradle,
GateToGrave,
#[serde(other)]
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum SystemBoundary {
#[serde(rename = "EN-15804")]
En15804,
#[serde(rename = "ISO-14044")]
Iso14044,
#[serde(rename = "GHG-protocol")]
GhgProtocol,
#[serde(other)]
Other,
}
#[cfg(test)]
mod carbon_footprint_class_tests {
use super::*;
#[test]
fn round_trip_preserves_a_label_this_build_has_never_seen() {
for label in ["A", "F", "A+", "A+++", "CLASS-1"] {
let json = serde_json::to_string(&CarbonFootprintClass::new(label).unwrap()).unwrap();
assert_eq!(json, format!("\"{label}\""));
let back: CarbonFootprintClass = serde_json::from_str(&json).unwrap();
assert_eq!(back.as_str(), label, "label must survive the round trip");
}
}
#[test]
fn deserialization_validates_rather_than_silently_accepting() {
for bad in ["\"\"", "\" \"", "\"TOOLONGLABEL\""] {
assert!(
serde_json::from_str::<CarbonFootprintClass>(bad).is_err(),
"should reject {bad}"
);
}
}
#[test]
fn rejects_empty_blank_overlong_and_control_characters() {
assert!(matches!(
CarbonFootprintClass::new(""),
Err(CarbonFootprintClassError::Empty)
));
assert!(matches!(
CarbonFootprintClass::new(" \t "),
Err(CarbonFootprintClassError::Empty)
));
assert!(matches!(
CarbonFootprintClass::new("A\u{7}"),
Err(CarbonFootprintClassError::ControlCharacter(_))
));
let err = CarbonFootprintClass::new("ABCDEFGHI").unwrap_err();
assert!(matches!(
err,
CarbonFootprintClassError::TooLong { len: 9, max: 8, .. }
));
}
#[test]
fn length_bound_matches_the_schema_and_counts_characters() {
assert_eq!(CarbonFootprintClass::MAX_LEN, 8);
assert!(CarbonFootprintClass::new("ABCDEFGH").is_ok());
assert!(CarbonFootprintClass::new("Ä+++").is_ok());
}
#[test]
fn label_is_stored_verbatim_without_case_folding() {
let c = CarbonFootprintClass::new("a+").unwrap();
assert_eq!(c.as_str(), "a+");
assert_eq!(c.to_string(), "a+");
}
}