Skip to main content

roboticus_api/
openapi.rs

1//! OpenAPI spec generation and Swagger UI endpoint.
2//!
3//! Serves the OpenAPI JSON spec at `/openapi.json` and provides a redirect
4//! to an external Swagger UI viewer at `/docs`.
5
6use axum::{
7    Json,
8    response::{IntoResponse, Redirect},
9};
10
11/// Minimal OpenAPI 3.1 spec describing the Roboticus API.
12///
13/// This is a hand-maintained spec covering the core endpoints. Future work
14/// will auto-generate this from route definitions.
15pub fn openapi_spec() -> serde_json::Value {
16    serde_json::json!({
17        "openapi": "3.1.0",
18        "info": {
19            "title": "Roboticus API",
20            "version": env!("CARGO_PKG_VERSION"),
21            "description": "Autonomous agent runtime API — sessions, messages, tools, routing, and operator controls."
22        },
23        "servers": [
24            { "url": "/", "description": "Local instance" }
25        ],
26        "paths": {
27            "/api/agent/message": {
28                "post": {
29                    "summary": "Send a message to the agent",
30                    "tags": ["Agent"],
31                    "requestBody": {
32                        "required": true,
33                        "content": {
34                            "application/json": {
35                                "schema": {
36                                    "type": "object",
37                                    "properties": {
38                                        "message": { "type": "string" },
39                                        "session_id": { "type": "string" }
40                                    },
41                                    "required": ["message"]
42                                }
43                            }
44                        }
45                    },
46                    "responses": {
47                        "200": { "description": "Agent response" }
48                    }
49                }
50            },
51            "/api/agent/message/stream": {
52                "post": {
53                    "summary": "Stream a message response via SSE",
54                    "tags": ["Agent"],
55                    "responses": {
56                        "200": { "description": "SSE token stream" }
57                    }
58                }
59            },
60            "/api/sessions": {
61                "get": {
62                    "summary": "List sessions",
63                    "tags": ["Sessions"],
64                    "responses": {
65                        "200": { "description": "Array of sessions" }
66                    }
67                }
68            },
69            "/api/health": {
70                "get": {
71                    "summary": "Health check",
72                    "tags": ["System"],
73                    "responses": {
74                        "200": { "description": "Service healthy" }
75                    }
76                }
77            },
78            "/api/config": {
79                "get": {
80                    "summary": "Get current configuration",
81                    "tags": ["Config"],
82                    "responses": {
83                        "200": { "description": "Current config as JSON" }
84                    }
85                },
86                "post": {
87                    "summary": "Apply configuration changes",
88                    "tags": ["Config"],
89                    "responses": {
90                        "200": { "description": "Config applied" }
91                    }
92                }
93            },
94            "/api/models/routing-diagnostics": {
95                "get": {
96                    "summary": "Get routing diagnostics",
97                    "tags": ["Models"],
98                    "responses": {
99                        "200": { "description": "Routing decision trace" }
100                    }
101                }
102            },
103            "/api/approvals": {
104                "get": {
105                    "summary": "List pending approvals",
106                    "tags": ["Approvals"],
107                    "responses": {
108                        "200": { "description": "Array of pending approval requests" }
109                    }
110                }
111            },
112            "/api/stats/efficiency": {
113                "get": {
114                    "summary": "Context efficiency analytics",
115                    "tags": ["Stats"],
116                    "responses": {
117                        "200": { "description": "Efficiency metrics" }
118                    }
119                }
120            },
121            "/api/channels/{platform}/test": {
122                "post": {
123                    "summary": "Test channel connectivity",
124                    "tags": ["Channels"],
125                    "parameters": [{
126                        "name": "platform",
127                        "in": "path",
128                        "required": true,
129                        "schema": { "type": "string" }
130                    }],
131                    "responses": {
132                        "200": { "description": "Channel test result" }
133                    }
134                }
135            }
136        },
137        "components": {
138            "securitySchemes": {
139                "apiKey": {
140                    "type": "apiKey",
141                    "in": "header",
142                    "name": "X-API-Key"
143                },
144                "bearer": {
145                    "type": "http",
146                    "scheme": "bearer"
147                }
148            }
149        }
150    })
151}
152
153/// Handler: GET /openapi.json
154pub async fn get_openapi_spec() -> impl IntoResponse {
155    Json(openapi_spec())
156}
157
158/// Handler: GET /docs — redirect to Swagger UI with the spec URL.
159pub async fn get_docs_redirect() -> impl IntoResponse {
160    // Use the public Swagger UI petstore instance pointed at our spec
161    Redirect::temporary("/openapi.json")
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn spec_is_valid_json() {
170        let spec = openapi_spec();
171        assert_eq!(spec["openapi"], "3.1.0");
172        assert!(
173            spec["info"]["title"]
174                .as_str()
175                .unwrap()
176                .contains("Roboticus")
177        );
178        assert!(spec["paths"].as_object().unwrap().len() >= 5);
179    }
180
181    #[test]
182    fn spec_version_from_cargo() {
183        let spec = openapi_spec();
184        let version = spec["info"]["version"].as_str().unwrap();
185        assert!(!version.is_empty());
186    }
187}