greentic_pack/
messaging.rs1use std::collections::BTreeSet;
2
3use anyhow::{Result, bail};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Default)]
8pub struct MessagingSection {
9 #[serde(default, skip_serializing_if = "Option::is_none")]
10 pub adapters: Option<Vec<MessagingAdapter>>,
11}
12
13impl MessagingSection {
14 pub fn validate(&self) -> Result<()> {
15 let mut seen = BTreeSet::new();
16 if let Some(adapters) = &self.adapters {
17 for adapter in adapters {
18 adapter.validate()?;
19 if !seen.insert(adapter.name.clone()) {
20 bail!("duplicate messaging adapter name: {}", adapter.name);
21 }
22 }
23 }
24 Ok(())
25 }
26}
27
28#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
29pub struct MessagingAdapter {
30 pub name: String,
31 pub kind: MessagingAdapterKind,
32 pub component: String,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub default_flow: Option<String>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub custom_flow: Option<String>,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub capabilities: Option<MessagingAdapterCapabilities>,
39}
40
41impl MessagingAdapter {
42 fn validate(&self) -> Result<()> {
43 if self.name.trim().is_empty() {
44 bail!("messaging.adapters[].name is required");
45 }
46 if self.component.trim().is_empty() {
47 bail!(
48 "messaging.adapters[{}].component must not be empty",
49 self.name
50 );
51 }
52 if let Some(cap) = &self.capabilities {
53 cap.validate(&self.name)?;
54 }
55 Ok(())
56 }
57}
58
59#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
60#[serde(rename_all = "kebab-case")]
61pub enum MessagingAdapterKind {
62 Ingress,
63 Egress,
64 IngressEgress,
65}
66
67#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Default)]
68#[serde(deny_unknown_fields)]
69pub struct MessagingAdapterCapabilities {
70 #[serde(default, skip_serializing_if = "Vec::is_empty")]
71 pub direction: Vec<String>,
72 #[serde(default, skip_serializing_if = "Vec::is_empty")]
73 pub features: Vec<String>,
74}
75
76impl MessagingAdapterCapabilities {
77 fn validate(&self, name: &str) -> Result<()> {
78 for entry in &self.direction {
79 if entry.trim().is_empty() {
80 bail!(
81 "messaging.adapters[{name}].capabilities.direction must not contain empty values"
82 );
83 }
84 }
85 for entry in &self.features {
86 if entry.trim().is_empty() {
87 bail!(
88 "messaging.adapters[{name}].capabilities.features must not contain empty values"
89 );
90 }
91 }
92 Ok(())
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 fn valid_adapter() -> MessagingAdapter {
101 MessagingAdapter {
102 name: "inbox".to_string(),
103 kind: MessagingAdapterKind::Ingress,
104 component: "component.messaging".to_string(),
105 default_flow: Some("flow.main".to_string()),
106 custom_flow: None,
107 capabilities: Some(MessagingAdapterCapabilities {
108 direction: vec!["ingress".to_string()],
109 features: vec!["retry".to_string()],
110 }),
111 }
112 }
113
114 #[test]
115 fn validate_accepts_unique_adapter() {
116 MessagingSection {
117 adapters: Some(vec![valid_adapter()]),
118 }
119 .validate()
120 .expect("valid adapter should pass");
121 }
122
123 #[test]
124 fn validate_rejects_duplicate_adapter_names() {
125 let adapter = valid_adapter();
126 let err = MessagingSection {
127 adapters: Some(vec![adapter.clone(), adapter]),
128 }
129 .validate()
130 .expect_err("duplicate names should fail");
131
132 assert!(err.to_string().contains("duplicate messaging adapter name"));
133 }
134
135 #[test]
136 fn validate_rejects_blank_feature_entry() {
137 let mut adapter = valid_adapter();
138 adapter
139 .capabilities
140 .as_mut()
141 .expect("caps")
142 .features
143 .push(" ".to_string());
144
145 let err = MessagingSection {
146 adapters: Some(vec![adapter]),
147 }
148 .validate()
149 .expect_err("blank feature should fail");
150
151 assert!(err.to_string().contains("capabilities.features"));
152 }
153}