greentic-pack-lib 0.4.124

Greentic pack builder and reader
Documentation
use std::collections::BTreeSet;

use anyhow::{Result, bail};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Default)]
pub struct MessagingSection {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub adapters: Option<Vec<MessagingAdapter>>,
}

impl MessagingSection {
    pub fn validate(&self) -> Result<()> {
        let mut seen = BTreeSet::new();
        if let Some(adapters) = &self.adapters {
            for adapter in adapters {
                adapter.validate()?;
                if !seen.insert(adapter.name.clone()) {
                    bail!("duplicate messaging adapter name: {}", adapter.name);
                }
            }
        }
        Ok(())
    }
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct MessagingAdapter {
    pub name: String,
    pub kind: MessagingAdapterKind,
    pub component: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub default_flow: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub custom_flow: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub capabilities: Option<MessagingAdapterCapabilities>,
}

impl MessagingAdapter {
    fn validate(&self) -> Result<()> {
        if self.name.trim().is_empty() {
            bail!("messaging.adapters[].name is required");
        }
        if self.component.trim().is_empty() {
            bail!(
                "messaging.adapters[{}].component must not be empty",
                self.name
            );
        }
        if let Some(cap) = &self.capabilities {
            cap.validate(&self.name)?;
        }
        Ok(())
    }
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum MessagingAdapterKind {
    Ingress,
    Egress,
    IngressEgress,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Default)]
#[serde(deny_unknown_fields)]
pub struct MessagingAdapterCapabilities {
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub direction: Vec<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub features: Vec<String>,
}

impl MessagingAdapterCapabilities {
    fn validate(&self, name: &str) -> Result<()> {
        for entry in &self.direction {
            if entry.trim().is_empty() {
                bail!(
                    "messaging.adapters[{name}].capabilities.direction must not contain empty values"
                );
            }
        }
        for entry in &self.features {
            if entry.trim().is_empty() {
                bail!(
                    "messaging.adapters[{name}].capabilities.features must not contain empty values"
                );
            }
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn valid_adapter() -> MessagingAdapter {
        MessagingAdapter {
            name: "inbox".to_string(),
            kind: MessagingAdapterKind::Ingress,
            component: "component.messaging".to_string(),
            default_flow: Some("flow.main".to_string()),
            custom_flow: None,
            capabilities: Some(MessagingAdapterCapabilities {
                direction: vec!["ingress".to_string()],
                features: vec!["retry".to_string()],
            }),
        }
    }

    #[test]
    fn validate_accepts_unique_adapter() {
        MessagingSection {
            adapters: Some(vec![valid_adapter()]),
        }
        .validate()
        .expect("valid adapter should pass");
    }

    #[test]
    fn validate_rejects_duplicate_adapter_names() {
        let adapter = valid_adapter();
        let err = MessagingSection {
            adapters: Some(vec![adapter.clone(), adapter]),
        }
        .validate()
        .expect_err("duplicate names should fail");

        assert!(err.to_string().contains("duplicate messaging adapter name"));
    }

    #[test]
    fn validate_rejects_blank_feature_entry() {
        let mut adapter = valid_adapter();
        adapter
            .capabilities
            .as_mut()
            .expect("caps")
            .features
            .push(" ".to_string());

        let err = MessagingSection {
            adapters: Some(vec![adapter]),
        }
        .validate()
        .expect_err("blank feature should fail");

        assert!(err.to_string().contains("capabilities.features"));
    }
}