use std::collections::BTreeMap;
use ciborium::Value as CborValue;
use eccodes::{DynamicKeyType, FallibleIterator, KeysIteratorFlags, RefMessage};
const GRIB_NAMESPACES: &[&str] = &[
"ls",
"geography",
"time",
"vertical",
"parameter",
"statistics",
];
#[derive(Debug, Clone)]
pub(crate) struct GribKeySet {
pub(crate) keys: BTreeMap<String, CborValue>,
}
fn read_namespace_keys(
msg: &mut RefMessage,
namespace: &str,
) -> Result<BTreeMap<String, CborValue>, eccodes::errors::CodesError> {
let key_names: Vec<String> = {
let mut iter = msg.new_keys_iterator(
&[
KeysIteratorFlags::AllKeys,
KeysIteratorFlags::SkipDuplicates,
],
namespace,
)?;
let mut names = Vec::new();
while let Some(name) = iter.next()? {
names.push(name);
}
names
};
let mut keys = BTreeMap::new();
for key_name in &key_names {
if let Ok(val) = msg.read_key_dynamic(key_name)
&& let Some(cbor) = dynamic_to_cbor(val)
{
keys.insert(key_name.clone(), cbor);
}
}
Ok(keys)
}
const MAX_ARRAY_LEN: usize = 8192;
fn dynamic_to_cbor(val: DynamicKeyType) -> Option<CborValue> {
match val {
DynamicKeyType::Str(s) if s != "MISSING" && s != "not_found" => Some(CborValue::Text(s)),
DynamicKeyType::Int(i) if i != 2147483647 && i != -2147483647 => {
Some(CborValue::Integer(i.into()))
}
DynamicKeyType::Float(f) if f.is_finite() => Some(CborValue::Float(f)),
DynamicKeyType::IntArray(a) if a.len() <= MAX_ARRAY_LEN => Some(CborValue::Array(
a.into_iter()
.map(|i| CborValue::Integer(i.into()))
.collect(),
)),
DynamicKeyType::FloatArray(a)
if a.len() <= MAX_ARRAY_LEN && a.iter().all(|f| f.is_finite()) =>
{
Some(CborValue::Array(
a.into_iter().map(CborValue::Float).collect(),
))
}
DynamicKeyType::Bytes(b) => Some(CborValue::Bytes(b)),
_ => None, }
}
pub(crate) fn extract_mars_keys(
msg: &mut RefMessage,
) -> Result<GribKeySet, eccodes::errors::CodesError> {
let keys = read_namespace_keys(msg, "mars")?;
Ok(GribKeySet { keys })
}
pub(crate) fn extract_all_namespace_keys(
msg: &mut RefMessage,
) -> Result<BTreeMap<String, BTreeMap<String, CborValue>>, eccodes::errors::CodesError> {
let mut result = BTreeMap::new();
for &ns in GRIB_NAMESPACES {
let keys = read_namespace_keys(msg, ns)?;
if !keys.is_empty() {
result.insert(ns.to_string(), keys);
}
}
Ok(result)
}
const RECONSTRUCT_NAMESPACES: &[&str] = &["geography", "parameter", "time", "vertical"];
const RECONSTRUCT_SCALAR_KEYS: &[&str] = &[
"edition",
"discipline",
"gridType",
"packingType",
"bitsPerValue",
"decimalScaleFactor",
"tablesVersion",
"centre",
"subCentre",
"productionStatusOfProcessedData",
"typeOfProcessedData",
"typeOfGeneratingProcess",
"generatingProcessIdentifier",
"setLocalDefinition",
"grib2LocalSectionNumber",
"marsClass",
"marsType",
"marsStream",
"experimentVersionNumber",
];
pub(crate) fn extract_reconstruct_keys(
msg: &mut RefMessage,
) -> Result<BTreeMap<String, CborValue>, eccodes::errors::CodesError> {
let mut keys = BTreeMap::new();
for &ns in RECONSTRUCT_NAMESPACES {
for (k, v) in read_namespace_keys(msg, ns)? {
keys.insert(k, v);
}
}
for &k in RECONSTRUCT_SCALAR_KEYS {
if let Ok(val) = msg.read_key_dynamic(k)
&& let Some(cbor) = dynamic_to_cbor(val)
{
keys.insert(k.to_string(), cbor);
}
}
Ok(keys)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn str_non_sentinel_becomes_text() {
assert_eq!(
dynamic_to_cbor(DynamicKeyType::Str("2t".to_string())),
Some(CborValue::Text("2t".to_string())),
);
}
#[test]
fn str_missing_sentinel_returns_none() {
assert_eq!(
dynamic_to_cbor(DynamicKeyType::Str("MISSING".to_string())),
None,
);
}
#[test]
fn str_not_found_sentinel_returns_none() {
assert_eq!(
dynamic_to_cbor(DynamicKeyType::Str("not_found".to_string())),
None,
);
}
#[test]
fn int_non_sentinel_becomes_integer() {
assert_eq!(
dynamic_to_cbor(DynamicKeyType::Int(42)),
Some(CborValue::Integer(42_i64.into())),
);
}
#[test]
fn int_positive_sentinel_returns_none() {
assert_eq!(dynamic_to_cbor(DynamicKeyType::Int(2147483647)), None);
}
#[test]
fn int_negative_sentinel_returns_none() {
assert_eq!(dynamic_to_cbor(DynamicKeyType::Int(-2147483647)), None);
}
#[test]
fn float_finite_becomes_float() {
assert_eq!(
dynamic_to_cbor(DynamicKeyType::Float(1.5)),
Some(CborValue::Float(1.5)),
);
}
#[test]
fn float_nan_returns_none() {
assert_eq!(dynamic_to_cbor(DynamicKeyType::Float(f64::NAN)), None);
}
#[test]
fn float_positive_infinity_returns_none() {
assert_eq!(dynamic_to_cbor(DynamicKeyType::Float(f64::INFINITY)), None);
}
#[test]
fn float_negative_infinity_returns_none() {
assert_eq!(
dynamic_to_cbor(DynamicKeyType::Float(f64::NEG_INFINITY)),
None,
);
}
#[test]
fn small_float_array_is_kept() {
assert_eq!(
dynamic_to_cbor(DynamicKeyType::FloatArray(vec![1.0, 2.0, 3.0])),
Some(CborValue::Array(vec![
CborValue::Float(1.0),
CborValue::Float(2.0),
CborValue::Float(3.0),
])),
);
}
#[test]
fn small_int_array_is_kept() {
assert_eq!(
dynamic_to_cbor(DynamicKeyType::IntArray(vec![1, 2, 3])),
Some(CborValue::Array(vec![
CborValue::Integer(1_i64.into()),
CborValue::Integer(2_i64.into()),
CborValue::Integer(3_i64.into()),
])),
);
}
#[test]
fn oversized_array_returns_none() {
let big = vec![0_i64; MAX_ARRAY_LEN + 1];
assert_eq!(dynamic_to_cbor(DynamicKeyType::IntArray(big)), None);
}
#[test]
fn non_finite_float_array_returns_none() {
assert_eq!(
dynamic_to_cbor(DynamicKeyType::FloatArray(vec![1.0, f64::NAN])),
None,
);
}
#[test]
fn bytes_are_kept() {
assert_eq!(
dynamic_to_cbor(DynamicKeyType::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF])),
Some(CborValue::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF])),
);
}
}