Skip to main content

mesh_types/
lib.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
4pub struct AgentCard {
5    pub metadata: AgentCardMetadata,
6    pub spec: AgentCardSpec,
7    #[serde(skip_serializing_if = "Option::is_none")]
8    pub status: Option<AgentStatus>,
9}
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct AgentCardMetadata {
13    pub id: String,
14    pub name: String,
15    pub version: String,
16    pub owner: String,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct AgentCardSpec {
21    pub domains: Vec<String>,
22    pub capabilities: Vec<CapabilitySpec>,
23    pub endpoints: AgentEndpoints,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct CapabilitySpec {
28    pub id: String,
29    pub description: Option<String>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct AgentEndpoints {
34    pub control_plane: ControlPlaneEndpoint,
35    pub data_plane: DataPlaneEndpoint,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ControlPlaneEndpoint {
40    pub nats_subject: String,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct DataPlaneEndpoint {
45    pub grpc: String,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct AgentStatus {
50    pub health: Option<String>,
51    pub last_heartbeat: Option<String>,
52}
53
54pub fn validate_agent_card(card: &AgentCard) -> Result<(), String> {
55    if card.metadata.id.is_empty() {
56        return Err("metadata.id is empty".into());
57    }
58    if card.spec.domains.is_empty() {
59        return Err("spec.domains is empty".into());
60    }
61    if card.spec.capabilities.is_empty() {
62        return Err("spec.capabilities is empty".into());
63    }
64    Ok(())
65}
66
67use serde_json::Value;
68
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
70pub struct PartiesData {
71    pub consumer: String,
72    pub provider: String,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
76pub struct SessionData {
77    pub session_id: String,
78    pub created_at: Option<String>,
79    pub expires_at: Option<String>,
80    pub session_token: Option<String>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
84pub struct AgreedTerms {
85    pub max_latency_ms: Option<u64>,
86    pub security_level: Option<String>,
87    pub cost_usd: Option<f64>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
91pub struct CapabilityMatchData {
92    pub request_id: String,
93    pub parties: PartiesData,
94    pub session: SessionData,
95    pub agreed_terms: Option<AgreedTerms>,
96    #[serde(flatten)]
97    pub extra: Option<Value>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
101pub struct CapabilityRejectData {
102    pub request_id: String,
103    pub reason: Option<String>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
107#[serde(tag = "kind", rename_all = "lowercase")]
108pub enum MatchOrReject {
109    Match { data: CapabilityMatchData },
110    Reject { data: CapabilityRejectData },
111}
112
113pub fn parse_match_or_reject(v: &Value) -> Result<MatchOrReject, String> {
114    // Try common shapes: either { "kind": "match", "data": { ... } } or CloudEvent-wrapped
115    if let Some(kind) = v.get("kind").and_then(|k| k.as_str()) {
116        match kind {
117            "match" => {
118                let data = v.get("data").ok_or("missing data")?;
119                let m: CapabilityMatchData = serde_json::from_value(data.clone()).map_err(|e| e.to_string())?;
120                return Ok(MatchOrReject::Match { data: m });
121            }
122            "reject" => {
123                let data = v.get("data").ok_or("missing data")?;
124                let r: CapabilityRejectData = serde_json::from_value(data.clone()).map_err(|e| e.to_string())?;
125                return Ok(MatchOrReject::Reject { data: r });
126            }
127            _ => return Err("unknown kind".into()),
128        }
129    }
130
131    // If it's a CloudEvent envelope, inspect type
132    if let Some(ty) = v.get("type").and_then(|t| t.as_str()) {
133        match ty {
134            "amp.capability.match" => {
135                let data = v.get("data").ok_or("missing data")?;
136                let m: CapabilityMatchData = serde_json::from_value(data.clone()).map_err(|e| e.to_string())?;
137                return Ok(MatchOrReject::Match { data: m });
138            }
139            "amp.capability.reject" => {
140                let data = v.get("data").ok_or("missing data")?;
141                let r: CapabilityRejectData = serde_json::from_value(data.clone()).map_err(|e| e.to_string())?;
142                return Ok(MatchOrReject::Reject { data: r });
143            }
144            _ => return Err("unknown cloud event type".into()),
145        }
146    }
147
148    Err("unrecognized message format".into())
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use serde_json::json;
155
156    #[test]
157    fn parse_match_cloud_event() {
158        let msg = json!({
159            "specversion": "1.0",
160            "type": "amp.capability.match",
161            "source": "did:mesh:agent:matcher",
162            "id": "evt-1",
163            "data": {
164                "request_id": "req-1",
165                "parties": { "consumer": "did:mesh:agent:cons", "provider": "did:mesh:agent:prov" },
166                "session": { "session_id": "s-1", "session_token": "tok" }
167            }
168        });
169
170        let parsed = parse_match_or_reject(&msg).expect("should parse");
171        match parsed {
172            MatchOrReject::Match { data } => {
173                assert_eq!(data.request_id, "req-1");
174                assert_eq!(data.parties.consumer, "did:mesh:agent:cons");
175            }
176            _ => panic!("expected match"),
177        }
178    }
179
180    #[test]
181    fn parse_reject_cloud_event() {
182        let msg = json!({
183            "specversion": "1.0",
184            "type": "amp.capability.reject",
185            "source": "did:mesh:agent:matcher",
186            "id": "evt-2",
187            "data": { "request_id": "req-2", "reason": "no-candidates" }
188        });
189
190        let parsed = parse_match_or_reject(&msg).expect("should parse");
191        match parsed {
192            MatchOrReject::Reject { data } => {
193                assert_eq!(data.request_id, "req-2");
194                assert_eq!(data.reason.unwrap(), "no-candidates");
195            }
196            _ => panic!("expected reject"),
197        }
198    }
199}