use std::collections::BTreeMap;
use std::str::FromStr;
#[derive(Debug, thiserror::Error)]
pub enum AuxiliaryError {
#[error("auxiliary schema id must not be empty")]
EmptySchemaId,
#[error("malformed auxiliary schema id: {value:?}")]
MalformedSchemaId {
value: String,
},
#[error("auxiliary artifact key must not be empty")]
EmptyArtifactKey,
#[error("auxiliary artifact path must not be empty for key {key:?}")]
EmptyArtifactPath {
key: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BlessedAuxSchema {
D8RasterV1,
SnapV1,
}
impl std::fmt::Display for BlessedAuxSchema {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BlessedAuxSchema::D8RasterV1 => write!(f, "hfx.aux.d8_raster.v1"),
BlessedAuxSchema::SnapV1 => write!(f, "hfx.aux.snap.v1"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum AuxiliarySchemaId {
Blessed(BlessedAuxSchema),
Provisional(String),
ThirdParty(String),
}
impl AuxiliarySchemaId {
pub fn parse(raw: &str) -> Result<Self, AuxiliaryError> {
if raw.is_empty() {
return Err(AuxiliaryError::EmptySchemaId);
}
match raw {
"hfx.aux.d8_raster.v1" => Ok(Self::Blessed(BlessedAuxSchema::D8RasterV1)),
"hfx.aux.snap.v1" => Ok(Self::Blessed(BlessedAuxSchema::SnapV1)),
value if value.starts_with("hfx.aux.") => Err(AuxiliaryError::MalformedSchemaId {
value: value.to_owned(),
}),
value if value.starts_with("hfx.x.") => Ok(Self::Provisional(value.to_owned())),
value => Ok(Self::ThirdParty(value.to_owned())),
}
}
}
impl std::fmt::Display for AuxiliarySchemaId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AuxiliarySchemaId::Blessed(schema) => write!(f, "{schema}"),
AuxiliarySchemaId::Provisional(value) | AuxiliarySchemaId::ThirdParty(value) => {
write!(f, "{value}")
}
}
}
}
impl FromStr for AuxiliarySchemaId {
type Err = AuxiliaryError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuxiliaryDecl {
schema: AuxiliarySchemaId,
artifacts: BTreeMap<String, String>,
}
impl AuxiliaryDecl {
pub fn new(
schema: AuxiliarySchemaId,
artifacts: BTreeMap<String, String>,
) -> Result<Self, AuxiliaryError> {
for (key, path) in &artifacts {
if key.is_empty() {
return Err(AuxiliaryError::EmptyArtifactKey);
}
if path.is_empty() {
return Err(AuxiliaryError::EmptyArtifactPath { key: key.clone() });
}
}
Ok(Self { schema, artifacts })
}
pub fn schema(&self) -> &AuxiliarySchemaId {
&self.schema
}
pub fn artifacts(&self) -> &BTreeMap<String, String> {
&self.artifacts
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_blessed_d8_raster() {
assert_eq!(
AuxiliarySchemaId::parse("hfx.aux.d8_raster.v1").unwrap(),
AuxiliarySchemaId::Blessed(BlessedAuxSchema::D8RasterV1)
);
}
#[test]
fn parse_blessed_snap_v1() {
assert_eq!(
AuxiliarySchemaId::parse("hfx.aux.snap.v1").unwrap(),
AuxiliarySchemaId::Blessed(BlessedAuxSchema::SnapV1)
);
}
#[test]
fn display_blessed_snap_v1() {
assert_eq!(BlessedAuxSchema::SnapV1.to_string(), "hfx.aux.snap.v1");
}
#[test]
fn parse_provisional_schema() {
assert_eq!(
AuxiliarySchemaId::parse("hfx.x.experimental.v1").unwrap(),
AuxiliarySchemaId::Provisional("hfx.x.experimental.v1".to_string())
);
}
#[test]
fn parse_third_party_schema() {
assert_eq!(
AuxiliarySchemaId::parse("org.example.custom.v1").unwrap(),
AuxiliarySchemaId::ThirdParty("org.example.custom.v1".to_string())
);
}
#[test]
fn parse_empty_schema_fails() {
assert!(matches!(
AuxiliarySchemaId::parse(""),
Err(AuxiliaryError::EmptySchemaId)
));
}
#[test]
fn parse_unknown_blessed_schema_fails() {
assert!(matches!(
AuxiliarySchemaId::parse("hfx.aux.other.v1"),
Err(AuxiliaryError::MalformedSchemaId { .. })
));
}
#[test]
fn auxiliary_decl_rejects_empty_path() {
let mut artifacts = BTreeMap::new();
artifacts.insert("flow_dir".to_string(), String::new());
assert!(matches!(
AuxiliaryDecl::new(
AuxiliarySchemaId::Blessed(BlessedAuxSchema::D8RasterV1),
artifacts
),
Err(AuxiliaryError::EmptyArtifactPath { key }) if key == "flow_dir"
));
}
}