Skip to main content

oxihuman_export/
asyncapi_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! AsyncAPI spec export stub.
6
7use std::collections::BTreeMap;
8
9/// AsyncAPI channel binding protocol.
10#[derive(Debug, Clone, PartialEq)]
11pub enum AsyncProtocol {
12    Amqp,
13    Mqtt,
14    WebSocket,
15    Kafka,
16    Http,
17}
18
19impl AsyncProtocol {
20    /// Protocol name string.
21    pub fn name(&self) -> &'static str {
22        match self {
23            Self::Amqp => "amqp",
24            Self::Mqtt => "mqtt",
25            Self::WebSocket => "ws",
26            Self::Kafka => "kafka",
27            Self::Http => "http",
28        }
29    }
30}
31
32/// An AsyncAPI message.
33#[derive(Debug, Clone)]
34pub struct AsyncMessage {
35    pub name: String,
36    pub summary: String,
37    pub payload_schema: String,
38}
39
40/// An AsyncAPI channel (publish/subscribe topic).
41#[derive(Debug, Clone)]
42pub struct AsyncChannel {
43    pub name: String,
44    pub description: Option<String>,
45    pub subscribe: Option<AsyncMessage>,
46    pub publish: Option<AsyncMessage>,
47}
48
49impl AsyncChannel {
50    /// Create a new channel.
51    pub fn new(name: impl Into<String>) -> Self {
52        Self {
53            name: name.into(),
54            description: None,
55            subscribe: None,
56            publish: None,
57        }
58    }
59}
60
61/// AsyncAPI document.
62#[derive(Debug, Clone, Default)]
63pub struct AsyncApiSpec {
64    pub title: String,
65    pub version: String,
66    pub protocol: Option<AsyncProtocol>,
67    pub channels: BTreeMap<String, AsyncChannel>,
68    pub servers: Vec<String>,
69}
70
71impl AsyncApiSpec {
72    /// Add a channel.
73    pub fn add_channel(&mut self, channel: AsyncChannel) {
74        self.channels.insert(channel.name.clone(), channel);
75    }
76
77    /// Number of channels.
78    pub fn channel_count(&self) -> usize {
79        self.channels.len()
80    }
81
82    /// Find channel by name.
83    pub fn find_channel(&self, name: &str) -> Option<&AsyncChannel> {
84        self.channels.get(name)
85    }
86}
87
88/// Render AsyncAPI spec as minimal JSON.
89pub fn render_asyncapi_json(spec: &AsyncApiSpec) -> String {
90    let protocol_str = spec.protocol.as_ref().map(|p| p.name()).unwrap_or("ws");
91    let channels: Vec<String> = spec
92        .channels
93        .iter()
94        .map(|(name, ch)| {
95            format!(
96                r#""{name}":{{"description":"{}"}}"#,
97                ch.description.as_deref().unwrap_or("")
98            )
99        })
100        .collect();
101    format!(
102        r#"{{"asyncapi":"2.6.0","info":{{"title":"{}","version":"{}"}},"defaultContentType":"application/json","channels":{{{}}},"protocol":"{}"}}"#,
103        spec.title,
104        spec.version,
105        channels.join(","),
106        protocol_str
107    )
108}
109
110/// Validate spec (title, version, at least one channel).
111pub fn validate_spec(spec: &AsyncApiSpec) -> bool {
112    !spec.title.is_empty() && !spec.version.is_empty() && !spec.channels.is_empty()
113}
114
115/// Count channels that have a subscribe operation.
116pub fn subscribe_channel_count(spec: &AsyncApiSpec) -> usize {
117    spec.channels
118        .values()
119        .filter(|c| c.subscribe.is_some())
120        .count()
121}
122
123/// Count channels that have a publish operation.
124pub fn publish_channel_count(spec: &AsyncApiSpec) -> usize {
125    spec.channels
126        .values()
127        .filter(|c| c.publish.is_some())
128        .count()
129}
130
131/// Add a server URL to the spec.
132pub fn add_server(spec: &mut AsyncApiSpec, url: impl Into<String>) {
133    spec.servers.push(url.into());
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    fn sample_spec() -> AsyncApiSpec {
141        let mut spec = AsyncApiSpec {
142            title: "Test Async".into(),
143            version: "1.0.0".into(),
144            protocol: Some(AsyncProtocol::WebSocket),
145            ..Default::default()
146        };
147        let mut ch = AsyncChannel::new("user/events");
148        ch.description = Some("User event stream".into());
149        ch.subscribe = Some(AsyncMessage {
150            name: "UserCreated".into(),
151            summary: "A user was created".into(),
152            payload_schema: "{}".into(),
153        });
154        spec.add_channel(ch);
155        spec
156    }
157
158    #[test]
159    fn channel_count() {
160        assert_eq!(sample_spec().channel_count(), 1);
161    }
162
163    #[test]
164    fn find_channel_found() {
165        assert!(sample_spec().find_channel("user/events").is_some());
166    }
167
168    #[test]
169    fn find_channel_missing() {
170        assert!(sample_spec().find_channel("nope").is_none());
171    }
172
173    #[test]
174    fn render_contains_asyncapi_version() {
175        assert!(render_asyncapi_json(&sample_spec()).contains("2.6.0"));
176    }
177
178    #[test]
179    fn render_contains_title() {
180        assert!(render_asyncapi_json(&sample_spec()).contains("Test Async"));
181    }
182
183    #[test]
184    fn validate_ok() {
185        assert!(validate_spec(&sample_spec()));
186    }
187
188    #[test]
189    fn validate_no_channels() {
190        let spec = AsyncApiSpec {
191            title: "T".into(),
192            version: "1.0".into(),
193            ..Default::default()
194        };
195        assert!(!validate_spec(&spec));
196    }
197
198    #[test]
199    fn subscribe_count() {
200        /* one channel has subscribe */
201        assert_eq!(subscribe_channel_count(&sample_spec()), 1);
202    }
203
204    #[test]
205    fn publish_count_zero() {
206        /* no publish operations */
207        assert_eq!(publish_channel_count(&sample_spec()), 0);
208    }
209
210    #[test]
211    fn add_server_increments() {
212        let mut spec = sample_spec();
213        add_server(&mut spec, "ws://localhost:4000");
214        assert_eq!(spec.servers.len(), 1);
215    }
216}