diff --git a/docs/code/python/usage.py b/docs/code/python/usage.py index ee6f357..33f25e8 100644 --- a/docs/code/python/usage.py +++ b/docs/code/python/usage.py @@ -9,9 +9,9 @@ print(json.dumps(epd_dict, indent=2)) print("\nEPD as Pydantic model") -epd_pydantic = epdx.convert_ilcd(ilcd_file.read_text(), as_type="pydantic") +epd_pydantic = epdx.convert_ilcd(ilcd_file.read_text(), as_type=epdx.EPD) print(epd_pydantic) print("\nEPD as string") -epd_str = epdx.convert_ilcd(ilcd_file.read_text(), as_type="str") +epd_str = epdx.convert_ilcd(ilcd_file.read_text(), as_type=str) print(epd_str) diff --git a/packages/python/src/epdx/__init__.py b/packages/python/src/epdx/__init__.py index 5b1612d..f58bb6c 100644 --- a/packages/python/src/epdx/__init__.py +++ b/packages/python/src/epdx/__init__.py @@ -33,7 +33,7 @@ def convert_ilcd(data: str | dict, *, as_type: Type[T] = dict) -> T: elif as_type == EPD: return EPD(**json.loads(_epd)) else: - raise NotImplemented("Currently only 'dict', 'str' and 'pydantic' is implemented as_type.") + raise NotImplementedError("Currently only 'dict', 'str' and 'epdx.EPD' is implemented as_type.") class ParsingException(Exception): diff --git a/pyproject.toml b/pyproject.toml index e9a9b43..36a43d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "epdx" description = "EPDx is a library for parsing EPD files into a common exchange format." readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.10" license = { file = "LICENSE" } authors = [ { name = "Christian Kongsgaard", email = "christian@kongsgaard.eu" }, diff --git a/src/epd.rs b/src/epd.rs index dfc2509..3a0b1db 100644 --- a/src/epd.rs +++ b/src/epd.rs @@ -1,17 +1,21 @@ -use std::collections::HashMap; -use chrono::{DateTime, Utc}; use chrono::prelude::*; +use chrono::{DateTime, Utc}; +use pkg_version::*; use schemars::JsonSchema; use serde::{Deserialize, Deserializer, Serialize}; -use pkg_version::*; +use std::collections::HashMap; -use crate::ilcd::{Exchange, ILCD, LCIAResult, ModuleAnie}; +use crate::ilcd::{Exchange, LCIAResult, ModuleAnie, ILCD}; #[cfg(feature = "jsbindings")] use tsify::Tsify; -#[derive(Debug, Serialize, JsonSchema)] -#[cfg_attr(feature = "jsbindings", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +#[derive(Serialize, JsonSchema, Clone)] +#[cfg_attr( + feature = "jsbindings", + derive(Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] pub struct EPD { pub id: String, pub name: String, @@ -59,7 +63,54 @@ pub struct EPD { pub meta_data: Option>, } -#[derive(Debug, Deserialize, Serialize, JsonSchema)] +impl EPD { + pub fn new() -> Self { + Self { + id: "".to_string(), + name: "".to_string(), + declared_unit: Unit::UNKNOWN, + version: "".to_string(), + published_date: Default::default(), + valid_until: Default::default(), + format_version: "".to_string(), + source: None, + reference_service_life: None, + standard: Standard::UNKNOWN, + comment: None, + location: "".to_string(), + subtype: SubType::Generic, + conversions: None, + gwp: None, + odp: None, + ap: None, + ep: None, + pocp: None, + adpe: None, + adpf: None, + penre: None, + pere: None, + perm: None, + pert: None, + penrt: None, + penrm: None, + sm: None, + rsf: None, + nrsf: None, + fw: None, + hwd: None, + nhwd: None, + rwd: None, + cru: None, + mfr: None, + mer: None, + eee: None, + eet: None, + meta_data: None, + } + } +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] #[cfg_attr(feature = "jsbindings", derive(Tsify))] pub enum Unit { M, @@ -88,20 +139,19 @@ impl From<&String> for Unit { "l" => Unit::L, "m2r1" => Unit::M2R1, "tones*km" => Unit::TONES_KM, - _ => Unit::UNKNOWN + _ => Unit::UNKNOWN, } } } -#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] #[cfg_attr(feature = "jsbindings", derive(Tsify))] pub struct Source { pub name: String, pub url: Option, } - -#[derive(Debug, Serialize, JsonSchema)] +#[derive(Debug, Serialize, JsonSchema, Clone)] #[cfg_attr(feature = "jsbindings", derive(Tsify))] pub enum Standard { EN15804A1, @@ -115,11 +165,13 @@ impl From<&String> for Standard { Standard::EN15804A2 } else if value.to_ascii_lowercase().contains("15804") { Standard::EN15804A1 - } else { Standard::UNKNOWN } + } else { + Standard::UNKNOWN + } } } -#[derive(Debug, Serialize, JsonSchema)] +#[derive(Debug, Serialize, JsonSchema, Clone)] #[cfg_attr(feature = "jsbindings", derive(Tsify))] pub enum SubType { Generic, @@ -136,11 +188,13 @@ impl From<&String> for SubType { SubType::Specific } else if value.to_ascii_lowercase().contains("industry") { SubType::Industry - } else { SubType::Generic } + } else { + SubType::Generic + } } } -#[derive(Debug, Deserialize, Serialize, Default, JsonSchema)] +#[derive(Deserialize, Serialize, JsonSchema, Default, Clone)] #[cfg_attr(feature = "jsbindings", derive(Tsify))] pub struct ImpactCategory { pub a1a3: Option, @@ -160,36 +214,108 @@ pub struct ImpactCategory { pub d: Option, } +impl ImpactCategory { + pub fn new() -> Self { + Self { + a1a3: None, + a4: None, + a5: None, + b1: None, + b2: None, + b3: None, + b4: None, + b5: None, + b6: None, + b7: None, + c1: None, + c2: None, + c3: None, + c4: None, + d: None, + } + } + + pub fn add(self: &mut Self, key: &str, value: f64) { + match key.to_lowercase().as_str() { + "a1a3" => self.a1a3 = Some(value), + "a4" => self.a4 = Some(value), + "a5" => self.a5 = Some(value), + "b1" => self.b1 = Some(value), + "b2" => self.b2 = Some(value), + "b3" => self.b3 = Some(value), + "b4" => self.b4 = Some(value), + "b5" => self.b5 = Some(value), + "b6" => self.b6 = Some(value), + "b7" => self.b7 = Some(value), + "c1" => self.c1 = Some(value), + "c2" => self.c2 = Some(value), + "c3" => self.c3 = Some(value), + "c4" => self.c4 = Some(value), + "d" => self.d = Some(value), + _ => (), + } + } +} + impl From<&Vec> for ImpactCategory { fn from(anies: &Vec) -> Self { let mut category = ImpactCategory::default(); for anie in anies { match (&anie.module, &anie.value) { - (Some(module), Some(value)) if module.to_lowercase() == String::from("a1-a3") => category.a1a3 = Some(f64::from(value)), - (Some(module), Some(value)) if module.to_lowercase() == String::from("a4") => category.a4 = Some(f64::from(value)), - (Some(module), Some(value)) if module.to_lowercase() == String::from("a5") => category.a5 = Some(f64::from(value)), - (Some(module), Some(value)) if module.to_lowercase() == String::from("b1") => category.b1 = Some(f64::from(value)), - (Some(module), Some(value)) if module.to_lowercase() == String::from("b2") => category.b2 = Some(f64::from(value)), - (Some(module), Some(value)) if module.to_lowercase() == String::from("b3") => category.b3 = Some(f64::from(value)), - (Some(module), Some(value)) if module.to_lowercase() == String::from("b4") => category.b4 = Some(f64::from(value)), - (Some(module), Some(value)) if module.to_lowercase() == String::from("b5") => category.b5 = Some(f64::from(value)), - (Some(module), Some(value)) if module.to_lowercase() == String::from("b6") => category.b6 = Some(f64::from(value)), - (Some(module), Some(value)) if module.to_lowercase() == String::from("b7") => category.b7 = Some(f64::from(value)), - (Some(module), Some(value)) if module.to_lowercase() == String::from("c1") => category.c1 = Some(f64::from(value)), - (Some(module), Some(value)) if module.to_lowercase() == String::from("c2") => category.c2 = Some(f64::from(value)), - (Some(module), Some(value)) if module.to_lowercase() == String::from("c3") => category.c3 = Some(f64::from(value)), - (Some(module), Some(value)) if module.to_lowercase() == String::from("c4") => category.c4 = Some(f64::from(value)), - (Some(module), Some(value)) if module.to_lowercase() == String::from("d") => category.d = Some(f64::from(value)), - _ => continue + (Some(module), Some(value)) if module.to_lowercase() == String::from("a1-a3") => { + category.a1a3 = Some(f64::from(value)) + } + (Some(module), Some(value)) if module.to_lowercase() == String::from("a4") => { + category.a4 = Some(f64::from(value)) + } + (Some(module), Some(value)) if module.to_lowercase() == String::from("a5") => { + category.a5 = Some(f64::from(value)) + } + (Some(module), Some(value)) if module.to_lowercase() == String::from("b1") => { + category.b1 = Some(f64::from(value)) + } + (Some(module), Some(value)) if module.to_lowercase() == String::from("b2") => { + category.b2 = Some(f64::from(value)) + } + (Some(module), Some(value)) if module.to_lowercase() == String::from("b3") => { + category.b3 = Some(f64::from(value)) + } + (Some(module), Some(value)) if module.to_lowercase() == String::from("b4") => { + category.b4 = Some(f64::from(value)) + } + (Some(module), Some(value)) if module.to_lowercase() == String::from("b5") => { + category.b5 = Some(f64::from(value)) + } + (Some(module), Some(value)) if module.to_lowercase() == String::from("b6") => { + category.b6 = Some(f64::from(value)) + } + (Some(module), Some(value)) if module.to_lowercase() == String::from("b7") => { + category.b7 = Some(f64::from(value)) + } + (Some(module), Some(value)) if module.to_lowercase() == String::from("c1") => { + category.c1 = Some(f64::from(value)) + } + (Some(module), Some(value)) if module.to_lowercase() == String::from("c2") => { + category.c2 = Some(f64::from(value)) + } + (Some(module), Some(value)) if module.to_lowercase() == String::from("c3") => { + category.c3 = Some(f64::from(value)) + } + (Some(module), Some(value)) if module.to_lowercase() == String::from("c4") => { + category.c4 = Some(f64::from(value)) + } + (Some(module), Some(value)) if module.to_lowercase() == String::from("d") => { + category.d = Some(f64::from(value)) + } + _ => continue, } } category } } - -#[derive(Debug, Serialize, JsonSchema)] +#[derive(Serialize, JsonSchema, Clone)] #[cfg_attr(feature = "jsbindings", derive(Tsify))] pub struct Conversion { pub value: f64, @@ -197,23 +323,65 @@ pub struct Conversion { pub meta_data: String, } - impl<'de> Deserialize<'de> for EPD { fn deserialize(deserializer: D) -> Result - where D: Deserializer<'de> + where + D: Deserializer<'de>, { let helper = ILCD::deserialize(deserializer)?; - let subtype = helper.modelling_and_validation.lci_method_and_allocation.other.anies.iter().find(|&anie| anie.name == "subType").unwrap(); - let format_version = format!("{}.{}.{}", pkg_version_major!(), pkg_version_minor!(), pkg_version_patch!()); + let subtype = helper + .modelling_and_validation + .lci_method_and_allocation + .other + .anies + .iter() + .find(|&anie| anie.name == "subType") + .unwrap(); + let format_version = format!( + "{}.{}.{}", + pkg_version_major!(), + pkg_version_minor!(), + pkg_version_patch!() + ); let standard = get_ilcd_standard(&helper); - let (gwp, odp, ap, ep, pocp, adpe, adpf) = collect_from_lcia_result(&helper.lcia_results.lcia_result); + let (gwp, odp, ap, ep, pocp, adpe, adpf) = + collect_from_lcia_result(&helper.lcia_results.lcia_result); - let (declared_unit, conversions, pere, perm, pert, penre, penrm, penrt, sm, rsf, nrsf, fw, hwd, nhwd, rwd, cru, mfr, mer, eee, eet) = collect_from_exchanges(&helper.exchanges.exchange); + let ( + declared_unit, + conversions, + pere, + perm, + pert, + penre, + penrm, + penrt, + sm, + rsf, + nrsf, + fw, + hwd, + nhwd, + rwd, + cru, + mfr, + mer, + eee, + eet, + ) = collect_from_exchanges(&helper.exchanges.exchange); Ok(EPD { id: helper.process_information.data_set_information.uuid, - name: helper.process_information.data_set_information.name.base_name.first().unwrap().value.to_string(), + name: helper + .process_information + .data_set_information + .name + .base_name + .first() + .unwrap() + .value + .to_string(), version: helper.version, format_version, declared_unit, @@ -223,9 +391,31 @@ impl<'de> Deserialize<'de> for EPD { comment: None, meta_data: None, source: None, - published_date: Utc.with_ymd_and_hms(helper.process_information.time.reference_year, 1, 1, 0, 0, 0).unwrap(), - valid_until: Utc.with_ymd_and_hms(helper.process_information.time.data_set_valid_until, 1, 1, 0, 0, 0).unwrap(), - location: helper.process_information.geography.location_of_operation_supply_or_production.location, + published_date: Utc + .with_ymd_and_hms( + helper.process_information.time.reference_year, + 1, + 1, + 0, + 0, + 0, + ) + .unwrap(), + valid_until: Utc + .with_ymd_and_hms( + helper.process_information.time.data_set_valid_until, + 1, + 1, + 0, + 0, + 0, + ) + .unwrap(), + location: helper + .process_information + .geography + .location_of_operation_supply_or_production + .location, subtype: SubType::from(&subtype.value), gwp, odp, @@ -257,13 +447,16 @@ impl<'de> Deserialize<'de> for EPD { } fn get_ilcd_standard(helper: &ILCD) -> Standard { - for compliance in &helper.modelling_and_validation.compliance_declarations.compliance { + for compliance in &helper + .modelling_and_validation + .compliance_declarations + .compliance + { for description in &compliance.reference_to_compliance_system.short_description { match Standard::from(&description.value) { Standard::UNKNOWN => continue, - standard => return standard + standard => return standard, } - } } @@ -271,7 +464,12 @@ fn get_ilcd_standard(helper: &ILCD) -> Standard { } fn get_converted_unit(unit_value: &String) -> Unit { - let value = unit_value.split("/").collect::>().first().unwrap().to_string(); + let value = unit_value + .split("/") + .collect::>() + .first() + .unwrap() + .to_string(); Unit::from(&value) } @@ -282,10 +480,14 @@ fn get_ilcd_conversion(exchange: &Exchange) -> Vec { Some(material_properties) => { for material_property in material_properties { let value = material_property.value.parse().unwrap_or_else(|_| 1.0); - conversions.push(Conversion { value, to: get_converted_unit(&material_property.unit), meta_data: serde_json::to_string(material_property).unwrap() }) + conversions.push(Conversion { + value, + to: get_converted_unit(&material_property.unit), + meta_data: serde_json::to_string(material_property).unwrap(), + }) } } - _ => return conversions + _ => return conversions, } conversions @@ -293,16 +495,31 @@ fn get_ilcd_conversion(exchange: &Exchange) -> Vec { fn get_ilcd_declared_unit(exchange: &Exchange) -> Unit { for flow_property in exchange.flow_properties.as_ref().unwrap() { - match (flow_property.reference_flow_property, &flow_property.reference_unit) { - (Some(reference_flow), Some(reference_unit)) if reference_flow == true => return Unit::from(reference_unit), - _ => continue + match ( + flow_property.reference_flow_property, + &flow_property.reference_unit, + ) { + (Some(reference_flow), Some(reference_unit)) if reference_flow == true => { + return Unit::from(reference_unit) + } + _ => continue, } } Unit::UNKNOWN } -fn collect_from_lcia_result(lcia_result: &Vec) -> (Option, Option, Option, Option, Option, Option, Option) { +fn collect_from_lcia_result( + lcia_result: &Vec, +) -> ( + Option, + Option, + Option, + Option, + Option, + Option, + Option, +) { let mut gwp = None; let mut odp = None; let mut ap = None; @@ -312,17 +529,22 @@ fn collect_from_lcia_result(lcia_result: &Vec) -> (Option gwp = impact_value, + value if value.contains("(GWP)") || value.contains("(GWP-total)") => { + gwp = impact_value + } value if value.contains("(ODP)") => odp = impact_value, value if value.contains("(AP)") => ap = impact_value, value if value.contains("(EP)") => ep = impact_value, value if value.contains("(POCP)") => pocp = impact_value, value if value.contains("(ADPE)") => adpe = impact_value, value if value.contains("(ADPF)") => adpf = impact_value, - _ => continue + _ => continue, } } } @@ -330,7 +552,30 @@ fn collect_from_lcia_result(lcia_result: &Vec) -> (Option) -> (Unit, Vec, Option, Option, Option, Option, Option, Option, Option, Option, Option, Option, Option, Option, Option, Option, Option, Option, Option, Option) { +fn collect_from_exchanges( + exchanges: &Vec, +) -> ( + Unit, + Vec, + Option, + Option, + Option, + Option, + Option, + Option, + Option, + Option, + Option, + Option, + Option, + Option, + Option, + Option, + Option, + Option, + Option, + Option, +) { let mut declared_unit = Unit::UNKNOWN; let mut conversions: Vec = vec![]; let mut pere = None; @@ -362,7 +607,7 @@ fn collect_from_exchanges(exchanges: &Vec) -> (Unit, Vec, for description in &exchange.reference_to_flow_data_set.short_description { let impact_value = match &exchange.other { Some(_anies) => Some(ImpactCategory::from(&_anies.anies)), - _ => continue + _ => continue, }; match &description.value { _description if _description == "Use of renewable primary energy (PERE)" => pere = impact_value, @@ -390,5 +635,26 @@ fn collect_from_exchanges(exchanges: &Vec) -> (Unit, Vec, }; } - (declared_unit, conversions, pere, perm, pert, penre, penrm, penrt, sm, rsf, nrsf, fw, hwd, nhwd, rwd, cru, mfr, mer, eee, eet) -} \ No newline at end of file + ( + declared_unit, + conversions, + pere, + perm, + pert, + penre, + penrm, + penrt, + sm, + rsf, + nrsf, + fw, + hwd, + nhwd, + rwd, + cru, + mfr, + mer, + eee, + eet, + ) +} diff --git a/src/ilcd.rs b/src/ilcd.rs index adfd2af..192f563 100644 --- a/src/ilcd.rs +++ b/src/ilcd.rs @@ -1,8 +1,7 @@ #[allow(dead_code)] -use serde::{Deserialize}; +use serde::Deserialize; use serde::Serialize; - #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct ILCD { @@ -11,7 +10,7 @@ pub struct ILCD { pub exchanges: Exchanges, #[serde(alias = "LCIAResults")] pub lcia_results: LCIAResults, - pub version: String + pub version: String, } #[derive(Deserialize)] @@ -20,7 +19,6 @@ pub struct Exchanges { pub exchange: Vec, } - #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct Exchange { @@ -37,10 +35,9 @@ pub struct Exchange { #[serde(alias = "exchange direction")] pub exchange_direction: Option, - pub other: Option + pub other: Option, } - #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct FlowProperty { @@ -50,7 +47,7 @@ pub struct FlowProperty { pub reference_flow_property: Option, pub reference_unit: Option, #[serde(alias = "unitGroupUUID")] - pub unit_group_uuid: Option + pub unit_group_uuid: Option, } #[derive(Deserialize, Serialize)] @@ -67,7 +64,7 @@ pub struct MaterialProperty { pub struct ModellingAndValidation { #[serde(alias = "LCIMethodAndAllocation")] pub lci_method_and_allocation: LCIMethodAndAllocation, - pub compliance_declarations: ComplianceDeclarations + pub compliance_declarations: ComplianceDeclarations, } #[derive(Deserialize)] @@ -88,10 +85,9 @@ pub struct ReferenceToDescription { pub short_description: Vec, pub _type: String, // pub ref_object_id: Option, - pub version: Option + pub version: Option, } - #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct LCIAResults { @@ -104,23 +100,22 @@ pub struct LCIAResults { pub struct LCIAResult { #[serde(alias = "referenceToLCIAMethodDataSet")] pub reference_to_lcia_method_dataset: ReferenceToLCIAMethodDataSet, - pub other: LCIAAnies + pub other: LCIAAnies, } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct LCIAAnies { - pub anies: Vec + pub anies: Vec, } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct ModuleAnie { pub module: Option, - pub value: Option + pub value: Option, } - #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum AnieValue { @@ -135,7 +130,7 @@ impl From<&AnieValue> for f64 { // Parse the string into a float let float_value = s.parse::().unwrap(); float_value - }, + } AnieValue::ValueObject(_) => { panic!("Cannot convert AnieValue::ValueObject to f64"); } @@ -154,12 +149,12 @@ pub struct ValueObject { #[derive(Deserialize, Debug)] pub enum ModuleValue { Value(String), - Name(ModuleMap) + Name(ModuleMap), } #[derive(Deserialize, Debug)] pub struct ModuleMap { - name: String + name: String, } #[derive(Deserialize, Debug)] @@ -177,14 +172,14 @@ pub struct LCIMethodAndAllocation { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct Anies { - pub anies: Vec + pub anies: Vec, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct Anie { pub name: String, - pub value: String + pub value: String, } #[derive(Deserialize)] @@ -192,19 +187,19 @@ pub struct Anie { pub struct ProcessInformation { pub data_set_information: DataSetInformation, pub time: TimeData, - pub geography: Geography + pub geography: Geography, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct Geography { - pub location_of_operation_supply_or_production: LocationOfOperationSupplyOrProduction + pub location_of_operation_supply_or_production: LocationOfOperationSupplyOrProduction, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct LocationOfOperationSupplyOrProduction { - pub location: String + pub location: String, } #[derive(Deserialize)] @@ -219,7 +214,7 @@ pub struct TimeData { pub struct DataSetInformation { #[serde(alias = "UUID")] pub uuid: String, - pub name: DataSetName + pub name: DataSetName, } #[derive(Deserialize)] @@ -231,5 +226,5 @@ pub struct DataSetName { #[derive(Deserialize, Debug)] pub struct ValueLang { pub value: String, - pub lang: String -} \ No newline at end of file + pub lang: String, +} diff --git a/src/javascript.rs b/src/javascript.rs index 1bcad20..aee9638 100644 --- a/src/javascript.rs +++ b/src/javascript.rs @@ -1,7 +1,6 @@ -use wasm_bindgen::prelude::*; use crate::epd::EPD; use crate::parse; - +use wasm_bindgen::prelude::*; // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global // allocator. @@ -20,7 +19,6 @@ pub fn convertIlcd(json: String) -> Result { let epd = parse::parse_ilcd(json); match epd { Ok(epd) => Ok(epd), - Err(error) => Err(JsError::new(error.to_string().as_str())) + Err(error) => Err(JsError::new(error.to_string().as_str())), } } - diff --git a/src/lcabyg.rs b/src/lcabyg.rs new file mode 100644 index 0000000..312e452 --- /dev/null +++ b/src/lcabyg.rs @@ -0,0 +1,150 @@ +use serde::{Deserialize, Serialize}; + +use crate::epd::{Conversion, ImpactCategory, Source, Standard, SubType, Unit, EPD}; +use crate::utils::get_version; + +#[derive(Deserialize, Serialize)] +pub enum Nodes { + Node(Node), +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub enum Node { + Stage(Stage), +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "snake_case")] +pub struct Stage { + pub id: String, + pub name: Languages, + pub comment: Languages, + pub source: String, + pub valid_to: String, + pub stage: String, + pub stage_unit: String, + pub stage_factor: f64, + pub mass_factor: f64, + pub scale_factor: f64, + pub external_source: String, + pub external_id: String, + pub external_version: String, + pub external_url: String, + pub compliance: String, + pub data_type: String, + pub indicators: StageIndicators, +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct Languages { + pub english: Option, + pub german: Option, + pub norwegian: Option, + pub danish: Option, +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "UPPERCASE")] +pub struct StageIndicators { + pub ser: f64, + pub ep: f64, + pub odp: f64, + pub pocp: f64, + pub per: f64, + pub adpe: f64, + pub ap: f64, + pub gwp: f64, + pub adpf: f64, + pub penr: f64, + pub senr: f64, +} + +impl From<&Vec> for EPD { + fn from(stages: &Vec) -> Self { + let mut ep = ImpactCategory::new(); + let mut odp = ImpactCategory::new(); + let mut pocp = ImpactCategory::new(); + let mut per = ImpactCategory::new(); + let mut adpe = ImpactCategory::new(); + let mut ap = ImpactCategory::new(); + let mut gwp = ImpactCategory::new(); + let mut adpf = ImpactCategory::new(); + let mut penr = ImpactCategory::new(); + + for stage in stages { + let stage_name = if stage.stage == "A1to3" { + "A1A3" + } else { + &stage.stage + }; + + ep.add(stage_name, stage.indicators.ep); + odp.add(stage_name, stage.indicators.odp); + pocp.add(stage_name, stage.indicators.pocp); + per.add(stage_name, stage.indicators.per); + adpe.add(stage_name, stage.indicators.adpe); + ap.add(stage_name, stage.indicators.ap); + gwp.add(stage_name, stage.indicators.gwp); + adpf.add(stage_name, stage.indicators.adpf); + penr.add(stage_name, stage.indicators.penr); + } + let node = &stages[0]; + let epd = EPD { + id: node.id.to_string(), + name: node.name.english.clone().unwrap(), + declared_unit: Unit::from(&node.stage_unit), + version: node.external_version.clone(), + published_date: Default::default(), + valid_until: Default::default(), + comment: node.comment.english.clone(), + source: Some(Source { + name: node.external_source.clone(), + url: Some(node.external_url.clone()), + }), + subtype: SubType::from(&node.data_type), + standard: if node.compliance == "A1" { + Standard::EN15804A1 + } else { + Standard::EN15804A2 + }, + ep: Some(ep), + odp: Some(odp), + pocp: Some(pocp), + pert: Some(per), + adpe: Some(adpe), + ap: Some(ap), + gwp: Some(gwp), + adpf: Some(adpf), + penre: None, + pere: None, + penrt: Some(penr), + penrm: None, + sm: None, + rsf: None, + nrsf: None, + fw: None, + hwd: None, + nhwd: None, + rwd: None, + cru: None, + mfr: None, + mer: None, + eee: None, + eet: None, + format_version: get_version(), + reference_service_life: None, + location: "".to_string(), + conversions: Some(vec![Conversion { + to: Unit::KG, + value: node.mass_factor, + meta_data: "".to_string(), + }]), + perm: None, + meta_data: None, + }; + + epd + } +} diff --git a/src/lib.rs b/src/lib.rs index 70afa1e..63e979b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,9 @@ +pub mod epd; #[allow(dead_code)] pub mod ilcd; -mod utils; +pub mod lcabyg; pub mod parse; -pub mod epd; +mod utils; #[cfg(feature = "jsbindings")] mod javascript; @@ -15,6 +16,6 @@ pub fn convert_ilcd(json: String) -> String { let epd = parse::parse_ilcd(json); match epd { Ok(epd) => serde_json::to_string(&epd).unwrap(), - Err(_) => String::from("") + Err(_) => String::from(""), } -} \ No newline at end of file +} diff --git a/src/parse.rs b/src/parse.rs index f3a2e39..70a2819 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,5 +1,5 @@ -use serde_json::Error; use crate::epd::EPD; +use serde_json::Error; /// Parse a ILCD formatted EPD in an EPDx struct /// @@ -12,6 +12,6 @@ pub fn parse_ilcd(json: String) -> Result { let epd = serde_json::from_str(&json); match epd { Ok(epd) => Ok(epd), - Err(err) => Err(err) + Err(err) => Err(err), } } diff --git a/src/python.rs b/src/python.rs index 485fb17..7bbd5d7 100644 --- a/src/python.rs +++ b/src/python.rs @@ -1,6 +1,6 @@ -use pyo3::prelude::*; -use pyo3::exceptions::PyTypeError; use crate::parse; +use pyo3::exceptions::PyTypeError; +use pyo3::prelude::*; #[cfg(feature = "pybindings")] #[pyfunction] @@ -8,7 +8,7 @@ pub fn _convert_ilcd(json: String) -> PyResult { let epd = parse::parse_ilcd(json); match epd { Ok(epd) => Ok(serde_json::to_string(&epd).unwrap()), - Err(error) => Err(PyTypeError::new_err(error.to_string())) + Err(error) => Err(PyTypeError::new_err(error.to_string())), } } @@ -21,4 +21,4 @@ fn epdx(_py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(_convert_ilcd, m)?)?; Ok(()) -} \ No newline at end of file +} diff --git a/src/schemars.rs b/src/schemars.rs index 55a60df..3330bba 100644 --- a/src/schemars.rs +++ b/src/schemars.rs @@ -1,4 +1,4 @@ -use schemars::{schema_for}; +use schemars::schema_for; extern crate epdx; use epdx::epd; @@ -6,4 +6,4 @@ use epdx::epd; fn main() { let schema = schema_for!(epd::EPD); println!("{}", serde_json::to_string_pretty(&schema).unwrap()); -} \ No newline at end of file +} diff --git a/src/utils.rs b/src/utils.rs index b1d7929..c3ffc15 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,3 +1,5 @@ +use pkg_version::{pkg_version_major, pkg_version_minor, pkg_version_patch}; + pub fn set_panic_hook() { // When the `console_error_panic_hook` feature is enabled, we can call the // `set_panic_hook` function at least once during initialization, and then @@ -8,3 +10,12 @@ pub fn set_panic_hook() { #[cfg(feature = "console_error_panic_hook")] console_error_panic_hook::set_once(); } + +pub fn get_version() -> String { + format!( + "{}.{}.{}", + pkg_version_major!(), + pkg_version_minor!(), + pkg_version_patch!() + ) +} diff --git a/tests/datafixtures/lcabyg/5aa09d72.json b/tests/datafixtures/lcabyg/5aa09d72.json new file mode 100644 index 0000000..5d6ecee --- /dev/null +++ b/tests/datafixtures/lcabyg/5aa09d72.json @@ -0,0 +1,96 @@ +[ + { + "Node": { + "Stage": { + "id": "d61fc8da-1a0d-4baa-a0fa-194c1f8a5218", + "name": { + "English": "Test fase (A1-A3)", + "Danish": "Test fase (A1-A3)", + "German": "Test fase (A1-A3)", + "Norwegian": "Test fase (A1-A3)" + }, + "comment": { + "German": "", + "Danish": "", + "English": "", + "Norwegian": "" + }, + "source": "User", + "valid_to": "2027-01-01", + "stage": "A1to3", + "stage_unit": "M3", + "indicator_unit": "M3", + "stage_factor": 1.0, + "mass_factor": 7850.0, + "indicator_factor": 1.0, + "scale_factor": 1.0, + "external_source": "\u00d6kobau.dat", + "external_id": "5aa09d72-e200-40dc-b8da-959a72e32bc3", + "external_version": "00.02.000", + "external_url": "https://oekobaudat.de/OEKOBAU.DAT/datasetdetail/process.xhtml?uuid=5aa09d72-e200-40dc-b8da-959a72e32bc3&version=00.02.000&stock=OBD_2021_II&lang=en", + "compliance": "A1", + "data_type": "Specific", + "indicators": { + "SER": 0.0, + "EP": 0.1557, + "ODP": 2.148e-12, + "POCP": 0.1334, + "PER": 512.5, + "ADPE": 8.897e-05, + "AP": 1.65, + "GWP": 818.0, + "ADPF": 8430.0, + "PENR": 9067.0, + "SENR": 0.0 + } + } + } + }, + { + "Node": { + "Stage": { + "id": "d62fc8da-1a0d-4baa-a0fa-194c1f8a5218", + "name": { + "English": "Test fase (A4)", + "Danish": "Test fase (A4)", + "German": "Test fase (A4)", + "Norwegian": "Test fase (A4)" + }, + "comment": { + "German": "", + "Danish": "", + "English": "", + "Norwegian": "" + }, + "source": "User", + "valid_to": "2027-01-01", + "stage": "A4", + "stage_unit": "M3", + "indicator_unit": "M3", + "stage_factor": 1.0, + "mass_factor": 7850.0, + "indicator_factor": 1.0, + "scale_factor": 1.0, + "external_source": "\u00d6kobau.dat", + "external_id": "5aa09d72-e200-40dc-b8da-959a72e32bc3", + "external_version": "00.02.000", + "external_url": "https://oekobaudat.de/OEKOBAU.DAT/datasetdetail/process.xhtml?uuid=5aa09d72-e200-40dc-b8da-959a72e32bc3&version=00.02.000&stock=OBD_2021_II&lang=en", + "compliance": "A1", + "data_type": "Specific", + "indicators": { + "SER": 0.0, + "EP": 1.1557, + "ODP": 2.148e-12, + "POCP": 0.1334, + "PER": 612.5, + "ADPE": 8.897e-05, + "AP": 2.65, + "GWP": 918.0, + "ADPF": 8430.0, + "PENR": 9067.0, + "SENR": 0.0 + } + } + } + } +] \ No newline at end of file diff --git a/tests/test_parse_ilcd.rs b/tests/test_parse_ilcd.rs index 0c6edf2..9391799 100644 --- a/tests/test_parse_ilcd.rs +++ b/tests/test_parse_ilcd.rs @@ -1,9 +1,10 @@ #[cfg(test)] mod tests { - use epdx::*; use std::fs; use std::path::Path; - use epdx::epd::{Standard, SubType}; + + use epdx::*; + use epdx::epd::Standard; macro_rules! parse_ilcd_tests { ($($name:ident: $value:expr)*) => { diff --git a/tests/test_parse_lcabyg.rs b/tests/test_parse_lcabyg.rs new file mode 100644 index 0000000..b7642b2 --- /dev/null +++ b/tests/test_parse_lcabyg.rs @@ -0,0 +1,50 @@ +#[cfg(test)] +mod tests { + use std::fs; + use std::path::Path; + + use epdx::*; + use epdx::epd::Standard; + + macro_rules! parse_lcabyg_tests { + ($($name:ident: $value:expr)*) => { + $( + #[test] + fn $name() -> Result<(), String> { + let input = $value; + + let root_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let file_path = root_dir.join("tests/datafixtures/lcabyg").join(input); + let contents = fs::read_to_string(file_path).expect("Should have been able to read the file"); + let nodes: Vec = serde_json::from_str(&contents).unwrap(); + + let mut stages: Vec = vec![]; + for node in nodes { + match node { + lcabyg::Nodes::Node(lcabyg::Node::Stage(_node)) => stages.append(&mut vec![_node]) + } + + }; + let _epd = epd::EPD::from(&stages); + + // Assert EPD Info + assert!(!_epd.id.is_empty()); + assert!(!_epd.name.is_empty()); + assert!(!matches!(_epd.standard, Standard::UNKNOWN)); + + // Assert Impact Category Values + assert!(_epd.gwp.is_some()); + assert!(_epd.odp.is_some()); + assert!(_epd.ap.is_some()); + assert!(_epd.pocp.is_some()); + assert!(_epd.adpe.is_some()); + assert!(_epd.adpf.is_some()); + Ok(()) + } + )* + } +} + parse_lcabyg_tests! { + lcabyg_5aa09d72: "5aa09d72.json" + } +}