diff --git a/Makefile b/Makefile index 3564aad..ba59b0e 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,10 @@ build: specs/index.html specs/faq.html cp -r $^ specs/diagrams ${RELEASE_DIR}/ cp -r TR ${RELEASE_DIR}/ + ## and now build the rust docs... + make gen/target/doc/ileap_extension/index.html + cp -r gen/target/doc ${RELEASE_DIR}/rustdocs + specs/index.html: specs/index.bs ${DIAGRAMS} bikeshed spec $< $@ @@ -29,6 +33,10 @@ clean: ${MMDC}: npm install @mermaid-js/mermaid-cli +gen/target/doc/ileap_extension/index.html: + cd gen && cargo doc --no-deps --document-private-items --all-features + +.PHONY: gen/target/doc/ileap_extension/index.html azure-upload-preview: build az storage blob upload-batch \ diff --git a/gen/Cargo.lock b/gen/Cargo.lock index 86f6622..8f4c926 100644 --- a/gen/Cargo.lock +++ b/gen/Cargo.lock @@ -127,9 +127,9 @@ checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "cc" -version = "1.1.14" +version = "1.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d2eb3cd3d1bf4529e31c215ee6f93ec5a3d536d9f578f93d9d33ee19562932" +checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" dependencies = [ "shlex", ] @@ -173,6 +173,16 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "log", + "regex", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -240,6 +250,8 @@ version = "0.1.0" dependencies = [ "chrono", "pact-data-model", + "quickcheck", + "quickcheck_macros", "regex", "rust_decimal", "rust_decimal_macros", @@ -331,9 +343,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ "toml_edit", ] @@ -390,6 +402,28 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "quickcheck" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" +dependencies = [ + "env_logger", + "log", + "rand", +] + +[[package]] +name = "quickcheck_macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b22a693222d716a9587786f37ac3f6b4faedb5b80c23914e7303ff5a1d8016e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quote" version = "1.0.37" @@ -683,9 +717,9 @@ checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" -version = "0.21.1" +version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ "indexmap", "toml_datetime", @@ -850,9 +884,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.5.40" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] diff --git a/gen/Cargo.toml b/gen/Cargo.toml index ede8ad7..a7ec013 100644 --- a/gen/Cargo.toml +++ b/gen/Cargo.toml @@ -14,4 +14,6 @@ serde_json = "1.0.96" rust_decimal = "1.35.0" rust_decimal_macros = "^1.20" regex = "1.10.4" +quickcheck = "1" +quickcheck_macros = "1" uuid = { version = "1.8", features = ["v4", "serde"] } diff --git a/gen/schemas/hoc.json b/gen/schemas/hoc.json index ce22183..7645cc0 100644 --- a/gen/schemas/hoc.json +++ b/gen/schemas/hoc.json @@ -135,9 +135,13 @@ ] }, "energyConsumptionUnit": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/EnergyConsumptionUnit" + }, + { + "type": "null" + } ] }, "feedstocks": { @@ -168,6 +172,15 @@ "Electric" ] }, + "EnergyConsumptionUnit": { + "type": "string", + "enum": [ + "l", + "kg", + "kWh", + "MJ" + ] + }, "Feedstock": { "type": "object", "required": [ @@ -178,11 +191,14 @@ "$ref": "#/definitions/FeedstockType" }, "feedstockPercentage": { - "type": [ - "number", - "null" - ], - "format": "double" + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] }, "regionProvenance": { "type": [ diff --git a/gen/schemas/tad.json b/gen/schemas/tad.json index b852542..1d0427e 100644 --- a/gen/schemas/tad.json +++ b/gen/schemas/tad.json @@ -126,11 +126,14 @@ "$ref": "#/definitions/FeedstockType" }, "feedstockPercentage": { - "type": [ - "number", - "null" - ], - "format": "double" + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] }, "regionProvenance": { "type": [ diff --git a/gen/schemas/toc.json b/gen/schemas/toc.json index 91bd058..78cc4a3 100644 --- a/gen/schemas/toc.json +++ b/gen/schemas/toc.json @@ -146,9 +146,13 @@ ] }, "energyConsumptionUnit": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/EnergyConsumptionUnit" + }, + { + "type": "null" + } ] }, "feedstocks": { @@ -179,6 +183,15 @@ "Electric" ] }, + "EnergyConsumptionUnit": { + "type": "string", + "enum": [ + "l", + "kg", + "kWh", + "MJ" + ] + }, "Feedstock": { "type": "object", "required": [ @@ -189,11 +202,14 @@ "$ref": "#/definitions/FeedstockType" }, "feedstockPercentage": { - "type": [ - "number", - "null" - ], - "format": "double" + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] }, "regionProvenance": { "type": [ diff --git a/gen/src/arbitrary_impls.rs b/gen/src/arbitrary_impls.rs new file mode 100644 index 0000000..55f1bd5 --- /dev/null +++ b/gen/src/arbitrary_impls.rs @@ -0,0 +1,691 @@ +use core::str; + +use crate::*; +use chrono::Duration; +use quickcheck::Arbitrary; +use rust_decimal::prelude::ToPrimitive; +use rust_decimal::Decimal; + +#[derive(Clone)] +pub struct LowerAToZNumDash(String); + +impl LowerAToZNumDash { + pub fn len(&self) -> usize { + self.0.len() + } +} + +impl Arbitrary for LowerAToZNumDash { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let maybe_string: Vec = Vec::arbitrary(g) + .into_iter() + .map(|v: u8| { + let i = v % 37; + + match i { + 0 => b'-', + 1..=10 => i + 47, + _ => i + 86, + } + }) + .collect(); + + Self(str::from_utf8(&maybe_string).unwrap().to_string()) + } + + fn shrink(&self) -> Box> { + let s = self.0.clone(); + let range = 0..self.len(); + let shrunk: Vec<_> = range + .into_iter() + .map(|len| Self(s[0..len].to_string())) + .collect(); + Box::new(shrunk.into_iter()) + } +} + +fn formatted_arbitrary_string(fixed: &str, g: &mut quickcheck::Gen) -> String { + fixed.to_string() + &LowerAToZNumDash::arbitrary(g).0 +} + +impl Arbitrary for ShipmentFootprint { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + ShipmentFootprint { + // Using u16 to avoid unreadably large numbers. + mass: format!("{}", u16::arbitrary(g)), + shipment_id: formatted_arbitrary_string("shipment-", g), + tces: NonEmptyVec::::arbitrary(g), + // Currently None for simplicity. + volume: None, + number_of_items: None, + type_of_items: None, + } + } +} + +impl Arbitrary for Hoc { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + fn gen_diff_transport_modes( + g: &mut quickcheck::Gen, + ) -> (Option, Option) { + let inbound = Some(TransportMode::arbitrary(g)); + let mut outbound = Some(TransportMode::arbitrary(g)); + + while inbound == outbound { + outbound = Some(TransportMode::arbitrary(g)); + } + + (inbound, outbound) + } + + let hub_type = HubType::arbitrary(g); + + let (inbound_transport_mode, outbound_transport_mode) = match hub_type { + // TODO: verify whether Transshipment and StorageAndTransshipment require different + // inbound and outbound transport modes. + HubType::Transshipment => gen_diff_transport_modes(g), + HubType::StorageAndTransshipment => gen_diff_transport_modes(g), + HubType::Warehouse => (Some(TransportMode::Road), Some(TransportMode::Road)), + HubType::LiquidBulkTerminal => ( + Some(TransportMode::arbitrary(g)), + Some(TransportMode::arbitrary(g)), + ), + HubType::MaritimeContainerterminal => { + let inbound = Option::::arbitrary(g); + let outbound = Option::::arbitrary(g); + + let (inbound, outbound) = match (inbound.clone(), outbound.clone()) { + (None, None) => (inbound, outbound), + (Some(TransportMode::Sea), _) => (inbound, outbound), + (_, Some(TransportMode::Sea)) => (inbound, outbound), + _ => ( + Option::::arbitrary(g), + Option::::arbitrary(g), + ), + }; + + (inbound, outbound) + } + }; + + Hoc { + hoc_id: formatted_arbitrary_string("hoc-", g), + is_verified: bool::arbitrary(g), + is_accredited: bool::arbitrary(g), + hub_type, + temperature_control: Option::::arbitrary(g), + inbound_transport_mode, + outbound_transport_mode, + packaging_or_tr_eq_type: Option::::arbitrary(g), + energy_carriers: NonEmptyVec::::arbitrary(g), + co2e_intensity_wtw: arbitrary_wrapped_decimal(g), + co2e_intensity_ttw: arbitrary_wrapped_decimal(g), + co2e_intensity_throughput: HocCo2eIntensityThroughput::arbitrary(g), + // Currently None for simplicity. + description: None, + hub_location: None, + packaging_or_tr_eq_amount: None, + } + } +} + +impl Arbitrary for HocCo2eIntensityThroughput { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let hoc_co2e_intensity_throughput = &[ + HocCo2eIntensityThroughput::TEU, + HocCo2eIntensityThroughput::Tonnes, + ]; + + g.choose(hoc_co2e_intensity_throughput).unwrap().to_owned() + } +} + +impl Arbitrary for HubType { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let hub_type = &[ + HubType::Transshipment, + HubType::StorageAndTransshipment, + HubType::Warehouse, + HubType::LiquidBulkTerminal, + HubType::MaritimeContainerterminal, + ]; + + g.choose(hub_type).unwrap().to_owned() + } +} + +impl Arbitrary for PackagingOrTrEqType { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let packaging_or_tr_eq_type = &[ + PackagingOrTrEqType::Box, + PackagingOrTrEqType::Pallet, + PackagingOrTrEqType::Container, + ]; + + g.choose(packaging_or_tr_eq_type).unwrap().to_owned() + } +} + +fn arbitrary_option_factor(g: &mut quickcheck::Gen) -> Option { + let rand_num = u8::arbitrary(g) % 10 + 1; + let rand_factor: Decimal = Decimal::new(rand_num as i64, 1); + + Some(rand_factor.to_string()) +} + +impl Arbitrary for Toc { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let mode = TransportMode::arbitrary(g); + + let (air_shipping_option, flight_length) = match mode { + TransportMode::Air => ( + Option::::arbitrary(g), + Option::::arbitrary(g), + ), + _ => (None, None), + }; + + Toc { + toc_id: formatted_arbitrary_string("toc-", g), + is_verified: bool::arbitrary(g), + is_accredited: bool::arbitrary(g), + mode, + load_factor: arbitrary_option_factor(g), + empty_distance_factor: arbitrary_option_factor(g), + temperature_control: Option::::arbitrary(g), + truck_loading_sequence: Option::::arbitrary(g), + air_shipping_option, + flight_length, + energy_carriers: NonEmptyVec::::arbitrary(g), + co2e_intensity_wtw: arbitrary_wrapped_decimal(g), + co2e_intensity_ttw: arbitrary_wrapped_decimal(g), + co2e_intensity_throughput: TocCo2eIntensityThroughput::arbitrary(g), + // Currently None for simplicity. + description: None, + glec_data_quality_index: None, + } + } +} + +impl Arbitrary for TocCo2eIntensityThroughput { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let toc_co2e_intensity_throughput = &[ + TocCo2eIntensityThroughput::Tkm, + TocCo2eIntensityThroughput::TEUkm, + ]; + + g.choose(toc_co2e_intensity_throughput).unwrap().to_owned() + } +} + +impl Arbitrary for NonEmptyVec { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + // Restricting to 1..5 elements. + let num = u8::arbitrary(g) % 5 + 1; + + let mut vec = vec![]; + for _ in 0..num { + vec.push(T::arbitrary(g)); + } + NonEmptyVec(vec) + } +} + +impl Arbitrary for TransportMode { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let transport_mode = &[ + TransportMode::Road, + TransportMode::Rail, + TransportMode::Air, + TransportMode::Sea, + TransportMode::InlandWaterway, + ]; + + g.choose(transport_mode).unwrap().to_owned() + } +} + +impl Arbitrary for TemperatureControl { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let temperature_control = &[ + TemperatureControl::Ambient, + TemperatureControl::Refrigerated, + TemperatureControl::Mixed, + ]; + + g.choose(temperature_control).unwrap().to_owned() + } +} + +impl Arbitrary for TruckLoadingSequence { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let truck_loading_sequence = &[TruckLoadingSequence::Ftl, TruckLoadingSequence::Ltl]; + + g.choose(truck_loading_sequence).unwrap().to_owned() + } +} + +impl Arbitrary for AirShippingOption { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let air_shipping_option = &[ + AirShippingOption::BellyFreight, + AirShippingOption::Freighter, + ]; + + g.choose(air_shipping_option).unwrap().to_owned() + } +} + +impl Arbitrary for FlightLength { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let flight_length = &[FlightLength::ShortHaul, FlightLength::LongHaul]; + + g.choose(flight_length).unwrap().to_owned() + } +} + +impl Arbitrary for EnergyCarrier { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let energy_carrier = EnergyCarrierType::arbitrary(g); + + let feedstocks = match Option::>::arbitrary(g) { + Some(mut feedstocks) => { + let total_percentage: Decimal = feedstocks + .iter() + .map(|f| { + f.feedstock_percentage + .as_ref() + .map(|p| p.0) + .unwrap_or(Decimal::from(0)) + }) + .sum(); + + if total_percentage > Decimal::from(1) { + for feedstock in &mut feedstocks { + feedstock.feedstock_percentage = None + } + } + + // TODO: verify which feedstocks make sense for each energy carrier. + feedstocks = feedstocks + .iter() + .filter(|f| match energy_carrier { + EnergyCarrierType::Diesel => f.feedstock == FeedstockType::Fossil, + EnergyCarrierType::Hvo => { + f.feedstock == FeedstockType::Fossil + || f.feedstock == FeedstockType::NaturalGas + || f.feedstock == FeedstockType::CookingOil + } + EnergyCarrierType::Petrol => { + f.feedstock == FeedstockType::Fossil + || f.feedstock == FeedstockType::NaturalGas + || f.feedstock == FeedstockType::CookingOil + } + EnergyCarrierType::Cng => { + f.feedstock == FeedstockType::Fossil + || f.feedstock == FeedstockType::NaturalGas + || f.feedstock == FeedstockType::CookingOil + } + EnergyCarrierType::Lng => { + f.feedstock == FeedstockType::Fossil + || f.feedstock == FeedstockType::NaturalGas + || f.feedstock == FeedstockType::CookingOil + } + EnergyCarrierType::Lpg => { + f.feedstock == FeedstockType::Fossil + || f.feedstock == FeedstockType::NaturalGas + || f.feedstock == FeedstockType::CookingOil + } + EnergyCarrierType::Hfo => { + f.feedstock == FeedstockType::Fossil + || f.feedstock == FeedstockType::NaturalGas + || f.feedstock == FeedstockType::CookingOil + } + EnergyCarrierType::Mgo => { + f.feedstock == FeedstockType::Fossil + || f.feedstock == FeedstockType::NaturalGas + || f.feedstock == FeedstockType::CookingOil + } + EnergyCarrierType::AviationFuel => { + f.feedstock == FeedstockType::Fossil + || f.feedstock == FeedstockType::NaturalGas + || f.feedstock == FeedstockType::CookingOil + } + EnergyCarrierType::Hydrogen => f.feedstock == FeedstockType::CookingOil, + EnergyCarrierType::Methanol => { + f.feedstock == FeedstockType::Fossil + || f.feedstock == FeedstockType::NaturalGas + || f.feedstock == FeedstockType::CookingOil + } + EnergyCarrierType::Electric => { + f.feedstock == FeedstockType::Grid + || f.feedstock == FeedstockType::RenewableElectricity + } + }) + .cloned() + .collect::>(); + + Some(feedstocks) + } + None => None, + }; + + EnergyCarrier { + energy_carrier, + feedstocks, + energy_consumption: arbitrary_option_wrapped_decimal(g), + energy_consumption_unit: Option::::arbitrary(g), + emission_factor_wtw: arbitrary_wrapped_decimal(g), + emission_factor_ttw: arbitrary_wrapped_decimal(g), + } + } +} + +impl Arbitrary for EnergyCarrierType { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let energy_carrier = &[ + EnergyCarrierType::Diesel, + EnergyCarrierType::Hvo, + EnergyCarrierType::Petrol, + EnergyCarrierType::Cng, + EnergyCarrierType::Lng, + EnergyCarrierType::Lpg, + EnergyCarrierType::Hfo, + EnergyCarrierType::Mgo, + EnergyCarrierType::AviationFuel, + EnergyCarrierType::Hydrogen, + EnergyCarrierType::Methanol, + EnergyCarrierType::Electric, + ]; + + g.choose(energy_carrier).unwrap().to_owned() + } +} + +impl Arbitrary for EnergyConsumptionUnit { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let energy_consumption_unit = &[ + EnergyConsumptionUnit::KWh, + EnergyConsumptionUnit::MJ, + EnergyConsumptionUnit::Kg, + EnergyConsumptionUnit::L, + ]; + + g.choose(energy_consumption_unit).unwrap().to_owned() + } +} + +impl Arbitrary for Feedstock { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let feedstock_percentage = arbitrary_option_wrapped_decimal(g); + + let feedstock_percentage = match feedstock_percentage { + None => None, + Some(f) => { + let decimal = (f.0 / Decimal::from(u16::MAX)).round_dp(1); + Some(WrappedDecimal::from(decimal)) + } + }; + + Feedstock { + feedstock: FeedstockType::arbitrary(g), + feedstock_percentage, + // Currently None for simplicity. + region_provenance: None, + } + } +} + +impl Arbitrary for FeedstockType { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let feedstock = &[ + FeedstockType::Fossil, + FeedstockType::NaturalGas, + FeedstockType::Grid, + FeedstockType::RenewableElectricity, + FeedstockType::CookingOil, + ]; + + g.choose(feedstock).unwrap().to_owned() + } +} + +impl Arbitrary for GlecDataQualityIndex { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + GlecDataQualityIndex(u8::arbitrary(g) % 5) + } +} + +impl Arbitrary for Tce { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let toc_id = if bool::arbitrary(g) { + Some(formatted_arbitrary_string("toc-", g)) + } else { + None + }; + + let hoc_id = match toc_id { + Some(_) => None, + None => Some(formatted_arbitrary_string("hoc-", g)), + }; + + let mass = arbitrary_wrapped_decimal(g); + let glec_distance = GlecDistance::arbitrary(g); + + let distance = match &glec_distance { + GlecDistance::Actual(d) => d, + GlecDistance::Gcd(d) => d, + GlecDistance::Sfd(d) => d, + }; + + let transport_activity = WrappedDecimal::from(mass.0 * distance.0); + + let departure_at = + Option::>::from(Utc::now() + Duration::days(u8::arbitrary(g) as i64)); + + let arrival_at = match departure_at { + None => None, + Some(departure) => { + // Assuming an average speed of 100 km/h, calculate the arrival time based on the + // distance, rounded. + let hours = (distance.0 / Decimal::from(100)).round().to_i64().unwrap(); + + Some(departure + Duration::hours(hours)) + } + }; + + Tce { + tce_id: formatted_arbitrary_string("tce-", g), + // Empty vec by default, populated by the generator function on main. + prev_tce_ids: Some(vec![]), + toc_id, + hoc_id, + shipment_id: formatted_arbitrary_string("shipment-", g), + consignment_id: Some(formatted_arbitrary_string("consignment-", g)), + mass, + packaging_or_tr_eq_type: Option::::arbitrary(g), + packaging_or_tr_eq_amount: Option::::arbitrary(g), + distance: glec_distance, + // TODO: origin and destination are currently None to avoid an inconsistencies with the + // distance field. In order to fix this, we need to ensure that either the distance is + // calculated from the origin and destination or that the origin and destination are set + // based on the distance. + origin: None, + destination: None, + transport_activity, + departure_at, + arrival_at, + incoterms: Option::::arbitrary(g), + // co2eWTW and co2eTTW are populated by the generator function on main, based on the + // emissions profile of the TOC/HOC. + co2e_wtw: Decimal::from(0).into(), + co2e_ttw: Decimal::from(0).into(), + // Currently None for simplicity. + flight_no: None, + voyage_no: None, + nox_ttw: None, + sox_ttw: None, + ch4_ttw: None, + pm_ttw: None, + } + } +} + +impl Arbitrary for GlecDistance { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let glec_distance = &[ + // Dividing u16 by 2 to avoid unreadably large data. + GlecDistance::Actual(Decimal::from(u16::arbitrary(g) / 2).into()), + GlecDistance::Gcd(Decimal::from(u16::arbitrary(g) / 2).into()), + GlecDistance::Sfd(Decimal::from(u16::arbitrary(g) / 2).into()), + ]; + + g.choose(glec_distance).unwrap().to_owned() + } +} + +impl Arbitrary for Location { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Location { + street: Option::::arbitrary(g), + zip: Option::::arbitrary(g), + city: String::arbitrary(g), + country: GeographicScope::Country { + geography_country: pact_data_model::ISO3166CC(String::arbitrary(g)), + }, + iata: Option::::arbitrary(g), + locode: Option::::arbitrary(g), + uic: Option::::arbitrary(g), + lat: arbitrary_option_wrapped_decimal(g), + lng: arbitrary_option_wrapped_decimal(g), + } + } +} + +impl Arbitrary for IataCode { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let mut s = String::new(); + + for _ in 0..3 { + let ascii_capital = ((u8::arbitrary(g) % 26) + 65) as char; + s.push(ascii_capital) + } + + IataCode::from(s) + } +} + +impl Arbitrary for Locode { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let mut s = String::new(); + + for _ in 0..5 { + // 65..90 - ASCII A to Z + let ascii_capital = ((u8::arbitrary(g) % 26) + 65) as char; + s.push(ascii_capital) + } + + Locode::from(s) + } +} + +impl Arbitrary for UicCode { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let mut s = String::new(); + + for _ in 0..2 { + let int = (u8::arbitrary(g) % 9) + 1; + s.push(int as char) + } + + UicCode::from(s) + } +} + +fn arbitrary_wrapped_decimal(g: &mut quickcheck::Gen) -> WrappedDecimal { + Decimal::from(u16::arbitrary(g)).round_dp(2).into() +} + +fn arbitrary_option_wrapped_decimal(g: &mut quickcheck::Gen) -> Option { + let option = &[Some(arbitrary_wrapped_decimal(g)), None]; + + g.choose(option).unwrap().to_owned() +} + +impl Arbitrary for Incoterms { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let incoterms = &[ + Incoterms::Exw, + Incoterms::Fca, + Incoterms::Cpt, + Incoterms::Cip, + Incoterms::Dap, + Incoterms::Dpu, + Incoterms::Ddp, + Incoterms::Fas, + Incoterms::Fob, + Incoterms::Cfr, + Incoterms::Cif, + ]; + + g.choose(incoterms).unwrap().to_owned() + } +} + +#[cfg(test)] +mod tests { + use crate::{Hoc, ShipmentFootprint, Tce, Toc}; + use quickcheck_macros::quickcheck; + + #[quickcheck] + fn ser_and_deser_tce(tce: Tce) -> bool { + let serialized = serde_json::to_string(&tce).unwrap(); + let deserialized = serde_json::from_str::(&serialized).unwrap(); + + println!("tce: {tce:?}"); + println!("serialized: {serialized}"); + println!("deserialized: {deserialized:?}"); + + deserialized == tce + } + + #[quickcheck] + fn ser_and_deser_toc(toc: Toc) -> bool { + let serialized = serde_json::to_string(&toc).unwrap(); + let deserialized = serde_json::from_str::(&serialized).unwrap(); + + if deserialized != toc { + println!("toc: {toc:?}"); + println!("deserialized: {deserialized:?}"); + } + + deserialized == toc + // true + } + + #[quickcheck] + fn ser_and_deser_hoc(hoc: Hoc) -> bool { + let serialized = serde_json::to_string(&hoc).unwrap(); + let deserialized = serde_json::from_str::(&serialized).unwrap(); + + if deserialized != hoc { + println!("toc: {hoc:?}"); + println!("deserialized: {deserialized:?}"); + } + + deserialized == hoc + } + + #[quickcheck] + fn ser_and_deser_ship_foot(ship_foot: ShipmentFootprint) { + let serialized = serde_json::to_string(&ship_foot).unwrap(); + let deserialized = serde_json::from_str::(&serialized).unwrap(); + + if deserialized != ship_foot { + println!("ship_foot: {ship_foot:?}"); + println!("deserialized: {deserialized:?}"); + } + + assert_eq!(deserialized, ship_foot); + } +} diff --git a/gen/src/lib.rs b/gen/src/lib.rs index 71404ec..e85283e 100644 --- a/gen/src/lib.rs +++ b/gen/src/lib.rs @@ -1,16 +1,24 @@ //! iLEAP Data Model Extension data model -use chrono::{Date, DateTime, Utc}; +use chrono::{DateTime, Utc}; + use pact_data_model::{ CarbonFootprint, CharacterizationFactors, CompanyIdSet, CrossSectoralStandard, CrossSectoralStandardSet, DataModelExtension, DeclaredUnit, ExemptedEmissionsPercent, GeographicScope, IpccCharacterizationFactorsSource, PfId, PfStatus, ProductFootprint, ProductIdSet, SpecVersionString, Urn, VersionInteger, WrappedDecimal, }; -use rust_decimal::{prelude::ToPrimitive, Decimal}; +use quickcheck::{Arbitrary, Gen}; +use rust_decimal::Decimal; + use schemars::JsonSchema; use serde::{Deserialize, Serialize}; + +mod pact_integration; +pub use pact_integration::*; use uuid::Uuid; +mod arbitrary_impls; + #[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ShipmentFootprint { @@ -166,6 +174,7 @@ pub enum FlightLength { #[derive(Debug, Serialize, Deserialize, JsonSchema, PartialEq, Clone)] #[serde(rename_all = "camelCase")] +// TODO: use a floating point or a decimal instead. pub struct GlecDataQualityIndex(pub u8); #[derive(Debug, Serialize, Deserialize, JsonSchema, PartialEq, Clone)] @@ -240,9 +249,8 @@ pub struct Tad { pub packaging_or_tr_eq_type: Option, #[serde(skip_serializing_if = "Option::is_none")] pub packaging_or_tr_eq_amount: Option, - // TODO: verify whether the absence of this property is intended. - // #[serde(skip_serializing_if = "Option::is_none")] - // pub energy_carrier: EnergyCarrier, + // TODO: verify whether the absence of this property is intended. #[serde(skip_serializing_if = + // "Option::is_none")] pub energy_carrier: EnergyCarrier, #[serde(skip_serializing_if = "Option::is_none")] pub feedstocks: Option>, } @@ -259,6 +267,16 @@ pub enum GlecDistance { Sfd(WrappedDecimal), } +impl GlecDistance { + pub fn get_distance(&self) -> Decimal { + match self { + GlecDistance::Actual(decimal) => decimal.0, + GlecDistance::Gcd(decimal) => decimal.0, + GlecDistance::Sfd(decimal) => decimal.0, + } + } +} + #[derive(Debug, Serialize, Deserialize, JsonSchema, PartialEq, Clone)] #[serde(rename_all = "camelCase")] pub struct Location { @@ -306,7 +324,7 @@ pub struct EnergyCarrier { #[serde(skip_serializing_if = "Option::is_none")] pub energy_consumption: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub energy_consumption_unit: Option, + pub energy_consumption_unit: Option, #[serde(rename = "emissionFactorWTW")] pub emission_factor_wtw: WrappedDecimal, #[serde(rename = "emissionFactorTTW")] @@ -336,12 +354,22 @@ pub enum EnergyCarrierType { Electric, } +#[derive(Debug, Serialize, Deserialize, JsonSchema, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub enum EnergyConsumptionUnit { + L, + Kg, + KWh, + #[serde(rename = "MJ")] + MJ, +} + #[derive(Debug, Serialize, Deserialize, JsonSchema, PartialEq, Clone)] #[serde(rename_all = "camelCase")] pub struct Feedstock { pub feedstock: FeedstockType, #[serde(skip_serializing_if = "Option::is_none")] - pub feedstock_percentage: Option, + pub feedstock_percentage: Option, #[serde(skip_serializing_if = "Option::is_none")] pub region_provenance: Option, } @@ -383,7 +411,7 @@ impl From for Locode { if s.len() == 5 { Locode(s) } else { - panic!("LOCODE must be 5 characters long") + panic!("LOCODE must be 5 characters long, got '{s}'") } } } @@ -512,14 +540,13 @@ pub fn to_pcf( .tces .0 .iter() - .fold(Decimal::from(0), |acc, tce| acc + tce.co2e_wtw.0) - .into(), + .fold(Decimal::from(0), |acc, tce| acc + tce.transport_activity.0), p_cf_excluding_biogenic: shipment .tces .0 .iter() .fold(Decimal::from(0), |acc, tce| acc + tce.co2e_wtw.0) - .into(), + .round_dp(2), }, ILeapType::Toc(ref toc) => MappedFields { product_id_type: "toc".to_string(), @@ -570,7 +597,7 @@ pub fn to_pcf( spec_version: SpecVersionString("2.2.0".to_string()), preceding_pf_ids: None, version: VersionInteger(1), - created: DateTime::from(Utc::now()), + created: Utc::now(), updated: None, status: PfStatus::Active, status_comment: None, @@ -599,7 +626,7 @@ pub fn to_pcf( i_luc_ghg_emissions: None, biogenic_carbon_withdrawal: None, aircraft_ghg_emissions: None, - characterization_factors: characterization_factors, + characterization_factors, ipcc_characterization_factors_sources: characterization_factors_sources.into(), cross_sectoral_standards_used: CrossSectoralStandardSet(vec![ CrossSectoralStandard::ISO14083, @@ -607,8 +634,8 @@ pub fn to_pcf( product_or_sector_specific_rules: None, // TODO: get clarity on whether GLEC should be specified biogenic_accounting_methodology: None, boundary_processes_description: "".to_string(), - reference_period_start: DateTime::from(Utc::now()), - reference_period_end: DateTime::from(Utc::now() + chrono::Duration::days(364)), + reference_period_start: Utc::now(), // TODO: turn into parameter. + reference_period_end: (Utc::now() + chrono::Duration::days(364)), geographic_scope: None, secondary_emission_factor_sources: None, exempted_emissions_percent: ExemptedEmissionsPercent(0.into()), @@ -633,3 +660,155 @@ pub fn to_pcf( }]), } } + +// TODO: invert logic to generate a list of HOCs and TOCs and only then generate TCEs, improving +// readability and demo data quality, as suggested by Martin. +pub fn gen_rnd_demo_data(size: u8) -> Vec { + let mut og = Gen::new(size as usize); + + let mut shipment_footprints = vec![]; + let mut tocs = vec![]; + let mut hocs = vec![]; + + let num_of_shipments = u8::arbitrary(&mut og) % size + 1; + for _ in 0..num_of_shipments { + let mut ship_foot = ShipmentFootprint::arbitrary(&mut og); + + let mut tces: Vec = vec![]; + let mut prev_tces: Vec = vec![]; + + let mut i = 0; + let limit = u8::arbitrary(&mut og) % size + 1; + // TODO: improve code through pair programming with Martin. + loop { + let mut tce = Tce::arbitrary(&mut og); + + if let Some(prev_tce) = tces.last() { + // Updates prevTceIds for the current TCE + prev_tces.push(prev_tce.tce_id.clone()); + tce.prev_tce_ids = Some(prev_tces.clone()); + + // Avoids having two HOCs follow one another + if prev_tce.hoc_id.is_some() && tce.hoc_id.is_some() { + tce = Tce::arbitrary(&mut og); + } + }; + + if i == 0 || i == limit - 1 && tce.hoc_id.is_some() { + tce = Tce::arbitrary(&mut og); + } + + if tce.hoc_id.is_some() { + // Avoids having an HOC as the first or the last TCE + + let mut hoc = Hoc::arbitrary(&mut og); + + hoc.hoc_id = tce.hoc_id.clone().unwrap(); + + tce.hoc_id = Some(hoc.hoc_id.clone()); + + tce.distance = GlecDistance::Actual(Decimal::from(0).into()); + tce.transport_activity = Decimal::from(0).into(); + + tce.co2e_wtw = + WrappedDecimal::from((hoc.co2e_intensity_wtw.0 * tce.mass.0).round_dp(2)); + tce.co2e_ttw = + WrappedDecimal::from((hoc.co2e_intensity_ttw.0 * tce.mass.0).round_dp(2)); + + let hoc = to_pcf( + ILeapType::Hoc(hoc), + "SINE Foundation", + "urn:sine:example", + Some(HocTeuContainerSize::Normal), + Some(vec![CharFactors::Ar6]), + ); + + hocs.push(hoc); + } + + if tce.toc_id.is_some() { + let mut toc = Toc::arbitrary(&mut og); + toc.toc_id = tce.toc_id.clone().unwrap(); + + tce.transport_activity = (tce.mass.0 * tce.distance.get_distance()) + .round_dp(2) + .into(); + + tce.toc_id = Some(toc.toc_id.clone()); + + tce.co2e_wtw = WrappedDecimal::from( + (toc.co2e_intensity_wtw.0 * tce.transport_activity.0).round_dp(2), + ); + tce.co2e_ttw = WrappedDecimal::from( + (toc.co2e_intensity_ttw.0 * tce.transport_activity.0).round_dp(2), + ); + + let toc = to_pcf( + ILeapType::Toc(toc), + "SINE Foundation", + "urn:sine:example", + None, + Some(vec![CharFactors::Ar6]), + ); + + tocs.push(toc.clone()); + } + + tce.shipment_id.clone_from(&ship_foot.shipment_id); + + tces.push(tce); + + i += 1; + if i == limit { + break; + } + } + + ship_foot.tces = NonEmptyVec::from(tces); + + let ship_foot = to_pcf( + ILeapType::ShipmentFootprint(ship_foot), + "SINE Foundation", + "urn:sine:example", + Some(HocTeuContainerSize::Normal), + Some(vec![CharFactors::Ar6]), + ); + + shipment_footprints.push(ship_foot); + } + + vec![shipment_footprints, tocs, hocs] + .into_iter() + .flatten() + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_gen_rnd_demo_data() { + let footprints = gen_rnd_demo_data(10); + + for footprint in footprints.iter() { + if let Some(extensions) = &footprint.extensions { + for extension in extensions.iter() { + if let Some(ship_foot) = extension.data.get("ShipmentFootprint") { + let ship_foot = + serde_json::from_value::(ship_foot.to_owned()) + .unwrap(); + for tce in ship_foot.tces.0.iter() { + assert!( + tce.toc_id.is_some() ^ tce.hoc_id.is_some(), + "Either tocId or hocId, but not both, must be provided." + ); + } + } + } + } + } + + println!("{footprints:#?}"); + } +} diff --git a/gen/src/main.rs b/gen/src/main.rs index 1848665..2274a90 100644 --- a/gen/src/main.rs +++ b/gen/src/main.rs @@ -28,8 +28,8 @@ fn generate_schema() -> Result<(), Error> { let schema = schema_for!(T); - let schema_json = - to_string_pretty(&schema).unwrap_or_else(|_| panic!("Failed to serialize {type_name} schema")); + let schema_json = to_string_pretty(&schema) + .unwrap_or_else(|_| panic!("Failed to serialize {type_name} schema")); let mut schema_file = File::create(format!("./schemas/{schema_name}.json"))?; diff --git a/gen/src/pact_integration.rs b/gen/src/pact_integration.rs new file mode 100644 index 0000000..08a864a --- /dev/null +++ b/gen/src/pact_integration.rs @@ -0,0 +1,457 @@ +use chrono::Utc; +use pact_data_model::{ + CarbonFootprint, CharacterizationFactors, CompanyIdSet, CrossSectoralStandard, + CrossSectoralStandardSet, DataModelExtension, DeclaredUnit, ExemptedEmissionsPercent, + IpccCharacterizationFactorsSource, PfId, PfStatus, PositiveDecimal, ProductFootprint, + ProductIdSet, SpecVersionString, Urn, VersionInteger, +}; +use rust_decimal::Decimal; +use serde::Serialize; +use uuid::Uuid; + +use crate::{Hoc, HocCo2eIntensityThroughput, ShipmentFootprint, Toc}; + +/*pub enum HocTeuContainerSize { + Normal, + Light, + Heavy, +}*/ + +/* fn get_teu_co2e_intensity_wtw( + hoc_co2e_intensity_wtw: Decimal, + hoc_container_size: &Option, +) -> Decimal { + match hoc_container_size { + Some(HocTeuContainerSize::Normal) => hoc_co2e_intensity_wtw * Decimal::from(10000), + Some(HocTeuContainerSize::Light) => hoc_co2e_intensity_wtw * Decimal::from(6000), + Some(HocTeuContainerSize::Heavy) => hoc_co2e_intensity_wtw * Decimal::from(14050), + None => { + println!("Warning: HOC TEU container size not specified, using normal container"); + hoc_co2e_intensity_wtw * Decimal::from(10000) + } + } +} */ + +pub struct PactMappedFields { + product_id_type: &'static str, + data_schema_id: &'static str, + id: String, + product_name_company: String, + declared_unit: DeclaredUnit, + unitary_product_amount: Decimal, + p_cf_excluding_biogenic: Decimal, +} + +impl From<&ShipmentFootprint> for PactMappedFields { + fn from(shipment: &ShipmentFootprint) -> Self { + PactMappedFields { + product_id_type: "shipment", + data_schema_id: "shipment-footprint", + id: shipment.shipment_id.clone(), + product_name_company: format!("ShipmentFootprint with id {}", shipment.shipment_id), + declared_unit: DeclaredUnit::TonKilometer, + unitary_product_amount: shipment + .tces + .0 + .iter() + .fold(Decimal::from(0), |acc, tce| acc + tce.transport_activity.0), + p_cf_excluding_biogenic: shipment + .tces + .0 + .iter() + .fold(Decimal::from(0), |acc, tce| acc + tce.co2e_wtw.0), + } + } +} + +impl From<&Hoc> for PactMappedFields { + fn from(hoc: &Hoc) -> Self { + PactMappedFields { + product_id_type: "hoc", + data_schema_id: "hoc", + id: hoc.hoc_id.clone(), + product_name_company: format!("HOC with ID {}", hoc.hoc_id), + declared_unit: DeclaredUnit::Kilogram, + unitary_product_amount: Decimal::from(1000), + p_cf_excluding_biogenic: match hoc.co2e_intensity_throughput { + HocCo2eIntensityThroughput::TEU => { + panic!("HOC with TEU throughput is not supported, yet") + } + HocCo2eIntensityThroughput::Tonnes => hoc.co2e_intensity_wtw.0, + }, + } + } +} + +impl From<&Toc> for PactMappedFields { + fn from(toc: &Toc) -> Self { + PactMappedFields { + product_id_type: "toc", + data_schema_id: "toc", + id: toc.toc_id.clone(), + product_name_company: format!("TOC with ID {}", toc.toc_id), + declared_unit: DeclaredUnit::TonKilometer, + unitary_product_amount: Decimal::from(1), + p_cf_excluding_biogenic: toc.co2e_intensity_wtw.0, + } + } +} + +/** + * Converts an iLEAP type into a PACT Data Model's ProductFootprint. + * + * To do so, additional propertiers are needed: + * - company_name: the name of the company that is responsible for the product + * - company_urn: the URN of the company that is responsible for the product + * - characterization_factors: the optional IPCC characterization factors that were used in the calculation of the carbon footprint (TOC, HOC, ShipmentFootprint). If not defined `AR5` will be used. + */ +pub fn to_pcf( + ileap_type: &T, + company_name: &str, + company_urn: &str, + // hoc_container_size: Option, + characterization_factors: Option>, +) -> ProductFootprint +where + T: Serialize, + PactMappedFields: for<'a> From<&'a T>, +{ + // Massage the optional IPCC characterization factors into a tuple of the actual factors and the + // IPCC Characterization Factor sources + let (characterization_factors, characterization_factors_sources) = + to_char_factors(characterization_factors); + + // Extract the properties necessary to turn the iLEAP type into a ProductFootprint. + // Note: this conversion at this point is "static" and does not require any additional data. + // However, the current implementation requires the HOC data type to declare its throughput + // in tonnes (i.e. /not/ in `TEU`) – otherwise the current implementation goes nuclear. + // We are investingating whether the iLEAP Data model needs to be updated for the `TEU` unit case. + // This function will be updated as we move along. + let PactMappedFields { + product_id_type, + data_schema_id, + id, + product_name_company, + declared_unit, + unitary_product_amount, + p_cf_excluding_biogenic, + } = ileap_type.into(); + + // Fasten your seatbelts, we are about to create a ProductFootprint... + ProductFootprint { + id: PfId(Uuid::new_v4()), + spec_version: SpecVersionString("2.2.0".to_string()), + preceding_pf_ids: None, + version: VersionInteger(1), + created: Utc::now(), + updated: None, + status: PfStatus::Active, + status_comment: None, + validity_period_start: None, + validity_period_end: None, + company_name: company_name.to_string().into(), + company_ids: CompanyIdSet(vec![Urn::from(company_urn.to_string())]), + product_description: "".to_string(), + product_ids: ProductIdSet(vec![Urn::from(format!( + "urn:pathfinder:product:customcode:vendor-assigned:{product_id_type}:{id}" + ))]), + product_category_cpc: String::from("83117").into(), + product_name_company: product_name_company.into(), + comment: "".to_string(), + pcf: CarbonFootprint { + declared_unit, + unitary_product_amount: unitary_product_amount.into(), + p_cf_excluding_biogenic: p_cf_excluding_biogenic.into(), + p_cf_including_biogenic: None, + fossil_ghg_emissions: p_cf_excluding_biogenic.into(), + fossil_carbon_content: PositiveDecimal::from(Decimal::from(0)), + biogenic_carbon_content: PositiveDecimal::from(Decimal::from(0)), + d_luc_ghg_emissions: None, + land_management_ghg_emissions: None, + other_biogenic_ghg_emissions: None, + i_luc_ghg_emissions: None, + biogenic_carbon_withdrawal: None, + aircraft_ghg_emissions: None, + characterization_factors, + ipcc_characterization_factors_sources: characterization_factors_sources.into(), + cross_sectoral_standards_used: CrossSectoralStandardSet(vec![ + CrossSectoralStandard::ISO14083, + ]), + product_or_sector_specific_rules: None, // TODO: get clarity on whether GLEC should be specified + biogenic_accounting_methodology: None, + boundary_processes_description: "".to_string(), + reference_period_start: Utc::now(), + reference_period_end: (Utc::now() + chrono::Duration::days(364)), + geographic_scope: None, + secondary_emission_factor_sources: None, + exempted_emissions_percent: ExemptedEmissionsPercent(0.into()), + exempted_emissions_description: "".to_string(), + packaging_emissions_included: false, + packaging_ghg_emissions: None, + allocation_rules_description: None, + uncertainty_assessment_description: None, + primary_data_share: None, + dqi: None, + assurance: None, + }, + extensions: Some(vec![DataModelExtension { + spec_version: SpecVersionString::from("0.2.0".to_string()), + data_schema: format!("https://api.ileap.sine.dev/{data_schema_id}.json"), + documentation: Some("https://sine-fdn.github.io/ileap-extension/".to_string()), + data: serde_json::to_value(ileap_type) + .unwrap() + .as_object() + .unwrap() + .to_owned(), + }]), + } +} + +fn to_char_factors( + characterization_factors: Option>, +) -> ( + CharacterizationFactors, + Vec, +) { + let (characterization_factors, characterization_factors_sources) = + match characterization_factors { + None => ( + CharacterizationFactors::Ar5, + vec![IpccCharacterizationFactorsSource::from("AR5".to_string())], + ), + Some(cf) => { + if cf.is_empty() { + ( + CharacterizationFactors::Ar5, + vec![IpccCharacterizationFactorsSource::from("AR5".to_string())], + ) + } else { + let cf: Vec = cf + .iter() + .map(|cf| match cf { + CharacterizationFactors::Ar5 => { + IpccCharacterizationFactorsSource::from("AR5".to_string()) + } + CharacterizationFactors::Ar6 => { + IpccCharacterizationFactorsSource::from("AR6".to_string()) + } + }) + .collect(); + + let characterization_factors = if cf + .contains(&IpccCharacterizationFactorsSource::from("AR5".to_string())) + { + CharacterizationFactors::Ar5 + } else { + CharacterizationFactors::Ar6 + }; + + (characterization_factors, cf) + } + } + }; + (characterization_factors, characterization_factors_sources) +} + +#[test] +fn ship_foot_to_pfc() { + use crate::{GlecDistance, Tce}; + use rust_decimal_macros::dec; + + let ship_foot = ShipmentFootprint { + shipment_id: "shipment-test".to_string(), + tces: vec![ + Tce { + tce_id: "tce-1-toc-rail-1".to_string(), + prev_tce_ids: Some(vec![]), + toc_id: Some("toc-rail-1".to_string()), + hoc_id: None, + shipment_id: "shipment-test".to_string(), + mass: dec!(40000).into(), + distance: GlecDistance::Actual(dec!(423).into()), + transport_activity: dec!(16920).into(), + co2e_wtw: dec!(118.44).into(), + co2e_ttw: dec!(0).into(), + consignment_id: None, + packaging_or_tr_eq_type: None, + packaging_or_tr_eq_amount: None, + origin: None, + destination: None, + departure_at: None, + arrival_at: None, + flight_no: None, + voyage_no: None, + incoterms: None, + nox_ttw: None, + sox_ttw: None, + ch4_ttw: None, + pm_ttw: None, + }, + Tce { + tce_id: "tce-2-hoc-transshipment-1".to_string(), + prev_tce_ids: Some(vec!["tce-1-toc-rail-1".to_string()]), + toc_id: None, + hoc_id: Some("hoc-transshipment-1".to_string()), + shipment_id: "shipment-test".to_string(), + mass: dec!(40000).into(), + distance: GlecDistance::Actual(dec!(0).into()), + transport_activity: dec!(0).into(), + co2e_wtw: dec!(1320).into(), + co2e_ttw: dec!(400).into(), + consignment_id: None, + packaging_or_tr_eq_type: None, + packaging_or_tr_eq_amount: None, + origin: None, + destination: None, + departure_at: None, + arrival_at: None, + flight_no: None, + voyage_no: None, + incoterms: None, + nox_ttw: None, + sox_ttw: None, + ch4_ttw: None, + pm_ttw: None, + }, + Tce { + tce_id: "tce-3-toc-road-1".to_string(), + prev_tce_ids: Some(vec!["tce-2-hoc-transshipment-1".to_string()]), + toc_id: Some("toc-road-1".to_string()), + hoc_id: None, + shipment_id: "shipment-test".to_string(), + mass: dec!(40000).into(), + distance: GlecDistance::Actual(dec!(423).into()), + transport_activity: dec!(16920).into(), + co2e_wtw: dec!(1692.62).into(), + co2e_ttw: dec!(1505.88).into(), + consignment_id: None, + packaging_or_tr_eq_type: None, + packaging_or_tr_eq_amount: None, + origin: None, + destination: None, + departure_at: None, + arrival_at: None, + flight_no: None, + voyage_no: None, + incoterms: None, + nox_ttw: None, + sox_ttw: None, + ch4_ttw: None, + pm_ttw: None, + }, + ] + .into(), + mass: "40000".to_string(), + volume: None, + number_of_items: None, + type_of_items: None, + }; + + let pfc = to_pcf(&ship_foot, "test", "urn:test", None); + + assert_eq!( + pfc.product_name_company.0, + "ShipmentFootprint with id shipment-test" + ); + assert_eq!(pfc.pcf.declared_unit, DeclaredUnit::TonKilometer); + assert_eq!(pfc.pcf.unitary_product_amount.0, dec!(33840)); + assert_eq!(pfc.pcf.p_cf_excluding_biogenic.0, dec!(3131.06)); +} + +#[test] +fn toc_to_pcf() { + use crate::{ + EnergyCarrier, EnergyCarrierType, Feedstock, FeedstockType, TemperatureControl, Toc, + TocCo2eIntensityThroughput, TransportMode, + }; + use rust_decimal_macros::dec; + + let toc = Toc { + toc_id: "toc-test".to_string(), + mode: TransportMode::Rail, + load_factor: Some(dec!(0.6).to_string()), + empty_distance_factor: Some(dec!(0.33).to_string()), + temperature_control: Some(TemperatureControl::Ambient), + truck_loading_sequence: None, + energy_carriers: vec![EnergyCarrier { + energy_carrier: EnergyCarrierType::Electric, + feedstocks: Some(vec![Feedstock { + feedstock: FeedstockType::Grid, + feedstock_percentage: None, + region_provenance: Some("Europe".to_string()), + }]), + energy_consumption: None, + energy_consumption_unit: Some(crate::EnergyConsumptionUnit::MJ), + emission_factor_wtw: dec!(97).into(), + emission_factor_ttw: dec!(0).into(), + }] + .into(), + co2e_intensity_wtw: dec!(0.007).into(), + co2e_intensity_ttw: dec!(0).into(), + co2e_intensity_throughput: TocCo2eIntensityThroughput::Tkm, + is_verified: true, + is_accredited: true, + description: None, + air_shipping_option: None, + flight_length: None, + glec_data_quality_index: None, + }; + + let pfc = to_pcf(&toc, "test", "urn:test", None); + + assert_eq!(pfc.product_name_company.0, "TOC with ID toc-test"); + assert_eq!(pfc.pcf.declared_unit, DeclaredUnit::TonKilometer); + assert_eq!(pfc.pcf.unitary_product_amount.0, dec!(1)); + assert_eq!(pfc.pcf.p_cf_excluding_biogenic.0, dec!(0.007)); +} + +#[test] +fn hoc_to_pfc() { + use crate::{ + EnergyCarrier, EnergyCarrierType, Hoc, HubType, TemperatureControl, TransportMode, + }; + use rust_decimal_macros::dec; + + let hoc = Hoc { + hoc_id: "hoc-test".to_string(), + hub_type: HubType::Transshipment, + temperature_control: Some(TemperatureControl::Refrigerated), + inbound_transport_mode: Some(TransportMode::Road), + outbound_transport_mode: Some(TransportMode::Rail), + is_verified: true, + is_accredited: true, + hub_location: None, + packaging_or_tr_eq_type: None, + packaging_or_tr_eq_amount: None, + description: None, + energy_carriers: vec![ + EnergyCarrier { + energy_carrier: EnergyCarrierType::Diesel, + feedstocks: None, + energy_consumption: None, + energy_consumption_unit: Some(crate::EnergyConsumptionUnit::Kg), + emission_factor_wtw: dec!(4.13).into(), + emission_factor_ttw: dec!(3.17).into(), + }, + EnergyCarrier { + energy_carrier: EnergyCarrierType::Electric, + feedstocks: None, + energy_consumption: None, + energy_consumption_unit: Some(crate::EnergyConsumptionUnit::MJ), + emission_factor_wtw: dec!(97).into(), + emission_factor_ttw: dec!(0).into(), + }, + ] + .into(), + co2e_intensity_wtw: dec!(33).into(), + co2e_intensity_ttw: dec!(10).into(), + co2e_intensity_throughput: HocCo2eIntensityThroughput::Tonnes, + }; + + let pfc = to_pcf(&hoc, "test", "urn:test", None); + + assert_eq!(pfc.product_name_company.0, "HOC with ID hoc-test"); + assert_eq!(pfc.pcf.declared_unit, DeclaredUnit::Kilogram); + assert_eq!(pfc.pcf.unitary_product_amount.0, dec!(1000)); + assert_eq!(pfc.pcf.p_cf_excluding_biogenic.0, dec!(33)); +} diff --git a/gen/tests/tests.rs b/gen/tests/tests.rs index c2eadd6..1487a7b 100644 --- a/gen/tests/tests.rs +++ b/gen/tests/tests.rs @@ -127,3 +127,18 @@ fn test_ship_foot_deser() { let ship_foot: ShipmentFootprint = serde_json::from_str(json).unwrap(); assert_eq!(ship_foot, expected) } + +#[test] +fn test_energyconsumptionunit_deser() { + use EnergyConsumptionUnit::*; + let test_vectors = vec![ + ("\"l\"", L), + ("\"kg\"", Kg), + ("\"kWh\"", KWh), + ("\"MJ\"", MJ), + ]; + + for (expect, input) in &test_vectors { + assert_eq!(expect, &serde_json::to_string(input).unwrap()); + } +} diff --git a/specs/index.bs b/specs/index.bs index 30d12ef..14c59a0 100644 --- a/specs/index.bs +++ b/specs/index.bs @@ -827,7 +827,7 @@ Note: The properties `tocId` and `hocId` are mutually exclusive, but one of them
If the transport [=distance=] is `700` `kilometers` and the mass is `230` `kilograms`, - then the value of this property MUST be `161000` (`700` `kilometers` * `230` `kilograms`). + then the value of this property MUST be `161` (`(700 kilometers * 230 kilograms) / 1000`).
@@ -1467,7 +1467,8 @@ Properties of data type Location: iata iataCode O - IATA code of airport + IATA code of airport. Applies only to (i) <{TCE|TCEs}> referring to a <{TOC}> with <{TOC/mode}> `Air` + and (ii) <{TAD}> with <{TAD/mode}> `Air` locode locode @@ -1477,7 +1478,8 @@ Properties of data type Location: uic uic O - UIC Code of the location + UIC Code of the location. Applies only to (i) <{TCE|TCEs}> referring to a <{TOC}> with <{TOC/mode}> `Rail` + and (ii) <{TAD}> with <{TAD/mode}> `Rail` lat [=Decimal=] @@ -2412,6 +2414,10 @@ properties that cannot be derived from `HOC` CAN be populated in a best-effort m - provide better guidance on [[#pcf-mapping]] +## Version 0.2.1-20240903 (2024-09-03) ## {#version-20240903} + +- make application of <{Location/iata}> and <{Location/uic}> more explicit + ## Version 0.2.1-20240813 (2024-08-13) ## {#version-20240813} - add mentions to `HOC`s in [[#txn2]]