Skip to main content

hypercall_api/
asyncapi.rs

1//! AsyncAPI specification for WebSocket API.
2//!
3//! This module generates an AsyncAPI 3.0 specification for the WebSocket API,
4//! documenting all channels, messages, and their schemas.
5//!
6//! The message types are defined in [`super::websocket`] and reused here
7//! to avoid duplication.
8
9use asyncapi_rust::AsyncApi;
10
11// Re-export the shared websocket message types.
12pub use hypercall_types::ws_protocol::{
13    WsCandleUpdate, WsCompetitionFinalStanding, WsCompetitionFinalStats, WsCompetitionGapUpdate,
14    WsCompetitionRankChange, WsCompetitionUpdate, WsFillUpdate, WsIndexPriceUpdate,
15    WsIndicativeMarketData, WsLiquidationStateChange, WsMarketUpdate, WsMessage, WsOrderbookUpdate,
16    WsPositionExpired, WsRfqQuoteEntry, WsRfqQuotes, WsRfqStatusUpdate, WsTradeUpdate,
17};
18
19/// WebSocket API specification
20///
21/// This struct generates the full AsyncAPI 3.0 specification for the
22/// Hypercall WebSocket API.
23#[derive(AsyncApi)]
24#[asyncapi(
25    title = "Hypercall WebSocket API",
26    version = "0.0.1",
27    description = "Real-time data streaming for options trading.\n\n\
28        ## Connection\n\n\
29        Connect to `wss://HOST/ws` with optional query parameters:\n\
30        - `wallet`: Your wallet address (required for authenticated channels)\n\n\
31        ## Available Channels\n\n\
32        | Channel | Auth Required | Description |\n\
33        |---------|---------------|-------------|\n\
34        | `orderbook` | No | L2 orderbook updates for all symbols |\n\
35        | `options_chain` | No | Incremental options chain updates |\n\
36        | `trades` | No | Public trade feed |\n\
37        | `market_updates` | No | Market listing changes (created/deleted/expired) |\n\
38        | `candles:<UNDERLYING>:<RESOLUTION>` | No | Realtime underlying candle updates |\n\
39        | `index_prices` | No | Batch index/spot price updates for all underlyings |\n\
40        | `order_updates` | Yes | Your order status changes |\n\
41        | `fills` | Yes | Your trade fills |\n\
42        | `portfolio` | Yes | Your position and balance updates |\n\
43        | `liquidation` | Yes | Your liquidation state changes |\n\
44        | `competition` | Yes | Your competition rank and final stats |\n\
45        | `competition_engagement` | Yes | Your rank jumps, gap-to-next, and final standing updates |\n\
46        | `indicative_market_data` | No | Aggregated indicative quotes from quote providers |\n\
47        | `rfq` | Yes | RFQ quotes and status updates |\n\n\
48        Orderbook channel sizes (`bids`/`asks`) are human-readable contract quantities.\n\n\
49        ## Authentication\n\n\
50        Authenticated channels require the `wallet` query parameter and filter messages to only \
51        show data for that wallet. No signature is required for WebSocket connections.\n\n\
52        ## Message Format\n\n\
53        All messages are JSON with a `type` field indicating the message type."
54)]
55#[asyncapi_server(
56    name = "local",
57    host = "localhost:3000",
58    protocol = "ws",
59    pathname = "/ws",
60    description = "Local development server"
61)]
62#[asyncapi_server(
63    name = "testnet",
64    host = "testnet.hypercall.xyz",
65    protocol = "wss",
66    description = "Testnet environment"
67)]
68#[asyncapi_channel(
69    name = "websocket",
70    address = "/ws",
71    description = "Main WebSocket endpoint for subscribing to real-time data channels.",
72    parameter(
73        name = "wallet",
74        description = "Wallet address for authenticated channels (optional)",
75        schema_type = "string"
76    )
77)]
78#[asyncapi_messages(WsMessage)]
79pub struct WebSocketApi;
80
81/// Get the AsyncAPI specification as JSON string
82pub fn asyncapi_spec_json() -> String {
83    let spec = WebSocketApi::asyncapi_spec();
84    sonic_rs::to_string_pretty(&spec).expect("Failed to serialize AsyncAPI spec")
85}
86
87/// Get the AsyncAPI specification with a custom server URL
88pub fn asyncapi_spec_with_server(base_url: &str, description: &str) -> String {
89    let mut spec = WebSocketApi::asyncapi_spec();
90
91    // Determine protocol from URL
92    let (protocol, host) = if base_url.starts_with("https://") {
93        ("wss", base_url.trim_start_matches("https://"))
94    } else if base_url.starts_with("http://") {
95        ("ws", base_url.trim_start_matches("http://"))
96    } else {
97        ("wss", base_url)
98    };
99
100    // Replace servers with the custom one
101    let server = asyncapi_rust::Server {
102        host: host.to_string(),
103        protocol: protocol.to_string(),
104        pathname: Some("/ws".to_string()),
105        description: Some(description.to_string()),
106        variables: None,
107    };
108
109    let mut servers = std::collections::HashMap::new();
110    servers.insert("production".to_string(), server);
111    spec.servers = Some(servers);
112
113    sonic_rs::to_string_pretty(&spec).expect("Failed to serialize AsyncAPI spec")
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_asyncapi_spec_generation() {
122        let json = asyncapi_spec_json();
123        assert!(json.contains("Hypercall WebSocket API"));
124        assert!(json.contains("orderbook"));
125        assert!(json.contains("competition"));
126        assert!(json.contains("competition_engagement"));
127        assert!(json.contains("CandleUpdate"));
128        assert!(json.contains("candles:<UNDERLYING>:<RESOLUTION>"));
129    }
130}