1use std::collections::BTreeMap;
2
3use axum::{Json, Router, response::Html, routing::get};
4use nidus_http::error::RoutePathError;
5use nidus_http::router::{OpenApiSchemaRegistrar, RouteMetadata};
6use serde_json::{Value, json};
7use utoipa::{PartialSchema, ToSchema};
8
9use crate::html::docs_html;
10use crate::route::OpenApiRoute;
11
12#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
14pub enum OpenApiDocumentError {
15 #[error("duplicate OpenAPI operation `{method}` `{path}`")]
17 DuplicateOperation {
18 method: String,
20 path: String,
22 },
23 #[error(transparent)]
25 RoutePath(#[from] RoutePathError),
26 #[error("OpenAPI schema registration failed: {message}")]
28 SchemaRegistration {
29 message: String,
31 },
32}
33
34#[derive(Clone, Debug)]
36pub struct OpenApiDocument {
37 title: String,
38 version: String,
39 routes: Vec<OpenApiRoute>,
40 schemas: BTreeMap<String, Value>,
41}
42
43impl OpenApiDocument {
44 pub fn new(title: impl Into<String>, version: impl Into<String>) -> Self {
46 Self {
47 title: title.into(),
48 version: version.into(),
49 routes: Vec::new(),
50 schemas: BTreeMap::new(),
51 }
52 }
53
54 pub fn route(mut self, route: OpenApiRoute) -> Self {
56 self = self
57 .try_route(route)
58 .unwrap_or_else(|error| panic!("{error}"));
59 self
60 }
61
62 pub fn try_route(mut self, route: OpenApiRoute) -> Result<Self, OpenApiDocumentError> {
64 if self
65 .routes
66 .iter()
67 .any(|existing| existing.path() == route.path() && existing.method() == route.method())
68 {
69 return Err(OpenApiDocumentError::DuplicateOperation {
70 method: route.method().to_owned(),
71 path: route.path().to_owned(),
72 });
73 }
74 self.routes.push(route);
75 Ok(self)
76 }
77
78 pub fn controller_routes(self, controller_prefix: &str, routes: &[RouteMetadata]) -> Self {
80 self.try_controller_routes(controller_prefix, routes)
81 .unwrap_or_else(|error| panic!("{error}"))
82 }
83
84 pub fn try_controller_routes(
86 mut self,
87 controller_prefix: &str,
88 routes: &[RouteMetadata],
89 ) -> Result<Self, OpenApiDocumentError> {
90 for route in routes {
91 self = self.try_route(OpenApiRoute::try_from_route_metadata_at_path(
92 route,
93 route.try_full_path(controller_prefix)?,
94 )?)?;
95 }
96 Ok(self)
97 }
98
99 pub fn schemas_from_route_metadata(mut self, routes: &[RouteMetadata]) -> Self {
101 self = self
102 .try_schemas_from_route_metadata(routes)
103 .unwrap_or_else(|error| panic!("{error}"));
104 self
105 }
106
107 pub fn try_schemas_from_route_metadata(
109 mut self,
110 routes: &[RouteMetadata],
111 ) -> Result<Self, OpenApiDocumentError> {
112 for route in routes {
113 self = self
114 .try_with_schema_registrar(route.request_schema_registrar())?
115 .try_with_schema_registrar(route.response_schema_registrar())?;
116 }
117 Ok(self)
118 }
119
120 pub fn schema<T>(mut self) -> Self
122 where
123 T: ToSchema,
124 {
125 self = self
126 .try_schema::<T>()
127 .unwrap_or_else(|error| panic!("{error}"));
128 self
129 }
130
131 pub fn try_schema<T>(mut self) -> Result<Self, OpenApiDocumentError>
133 where
134 T: ToSchema,
135 {
136 self.schemas = self.register_schemas(Self::try_collect_openapi_schemas::<T>()?);
137 Ok(self)
138 }
139
140 fn try_collect_openapi_schemas<T: ToSchema>()
141 -> Result<Vec<(String, Value)>, OpenApiDocumentError> {
142 let mut openapi_schemas: Vec<(
143 String,
144 utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
145 )> = vec![(T::name().to_string(), <T as PartialSchema>::schema())];
146 <T as ToSchema>::schemas(&mut openapi_schemas);
147 let mut schemas = Vec::new();
148 for (name, schema) in openapi_schemas {
149 let value = serde_json::to_value(schema).map_err(|error| {
150 OpenApiDocumentError::SchemaRegistration {
151 message: format!("schema `{name}`: {error}"),
152 }
153 })?;
154 schemas.push((name, value));
155 }
156 Ok(schemas)
157 }
158
159 pub fn from_route_metadata(
161 title: impl Into<String>,
162 version: impl Into<String>,
163 routes: &[RouteMetadata],
164 ) -> Self {
165 Self::try_from_route_metadata(title, version, routes)
166 .unwrap_or_else(|error| panic!("{error}"))
167 }
168
169 pub fn try_from_route_metadata(
171 title: impl Into<String>,
172 version: impl Into<String>,
173 routes: &[RouteMetadata],
174 ) -> Result<Self, OpenApiDocumentError> {
175 let mut document = Self::new(title, version);
176 for route in routes {
177 document = document.try_route(OpenApiRoute::try_from_route_metadata(route)?)?;
178 }
179 Ok(document)
180 }
181
182 pub fn from_controller_routes(
184 title: impl Into<String>,
185 version: impl Into<String>,
186 controller_prefix: &str,
187 routes: &[RouteMetadata],
188 ) -> Self {
189 Self::try_from_controller_routes(title, version, controller_prefix, routes)
190 .unwrap_or_else(|error| panic!("{error}"))
191 }
192
193 pub fn try_from_controller_routes(
195 title: impl Into<String>,
196 version: impl Into<String>,
197 controller_prefix: &str,
198 routes: &[RouteMetadata],
199 ) -> Result<Self, OpenApiDocumentError> {
200 Self::new(title, version).try_controller_routes(controller_prefix, routes)
201 }
202
203 pub fn to_json_value(&self) -> Value {
205 let mut paths = serde_json::Map::new();
206 for route in &self.routes {
207 let entry = paths
208 .entry(route.path().to_owned())
209 .or_insert_with(|| Value::Object(serde_json::Map::new()));
210 if let Value::Object(methods) = entry {
211 methods.insert(route.method().to_owned(), route.to_json_value());
212 }
213 }
214
215 let mut document = json!({
216 "openapi": "3.1.0",
217 "info": {
218 "title": self.title,
219 "version": self.version,
220 },
221 "paths": paths,
222 });
223
224 if !self.schemas.is_empty() {
225 document["components"] = json!({
226 "schemas": &self.schemas,
227 });
228 }
229
230 document
231 }
232
233 pub fn into_router(self) -> Router {
235 let json = self.to_json_value();
236 let docs = docs_html(&self.title, "/openapi.json");
237
238 Router::new()
239 .route(
240 "/openapi.json",
241 get(move || {
242 let json = json.clone();
243 async move { Json(json) }
244 }),
245 )
246 .route(
247 "/docs",
248 get(move || {
249 let docs = docs.clone();
250 async move { Html(docs) }
251 }),
252 )
253 }
254
255 fn try_with_schema_registrar(
256 mut self,
257 registrar: Option<OpenApiSchemaRegistrar>,
258 ) -> Result<Self, OpenApiDocumentError> {
259 let Some(registrar) = registrar else {
260 return Ok(self);
261 };
262 let mut schemas = Vec::new();
263 registrar(&mut schemas).map_err(|error| OpenApiDocumentError::SchemaRegistration {
264 message: error.to_string(),
265 })?;
266 self.schemas = self.register_schemas(schemas);
267 Ok(self)
268 }
269
270 fn register_schemas(&self, schemas: Vec<(String, Value)>) -> BTreeMap<String, Value> {
271 let mut registered = self.schemas.clone();
272 for (name, schema) in schemas {
273 registered.entry(name).or_insert(schema);
274 }
275 registered
276 }
277}