1#![doc = include_str!("../README.md")]
2
3use std::collections::{BTreeMap, HashMap};
4
5use auto_di::{BoxFuture, Container, DiError};
6use axum::{Json, Router, response::Html, routing::get};
7use poem_openapi::{
8 __private::poem::{endpoint::BoxEndpoint, http::Method},
9 OpenApi, OpenApiService, ParameterStyle,
10 registry::{
11 MetaApi, MetaMediaType, MetaOperation, MetaOperationParam, MetaParamIn, MetaPath,
12 MetaRequest, MetaResponse, MetaResponses, MetaSchemaRef, MetaTag, Registry,
13 },
14 types::Type,
15};
16use scalar_api_reference::axum as scalar;
17use serde_json::Value;
18
19pub use auto_route_macros::{controller, delete, get, head, options, patch, post, put};
20
21#[doc(hidden)]
23pub struct RouteDescriptor {
24 factory: for<'a> fn(&'a Container) -> BoxFuture<'a, Result<Router<()>, DiError>>,
25}
26
27impl RouteDescriptor {
28 #[doc(hidden)]
29 pub const fn new(
30 factory: for<'a> fn(&'a Container) -> BoxFuture<'a, Result<Router<()>, DiError>>,
31 ) -> Self {
32 Self { factory }
33 }
34}
35
36inventory::collect!(RouteDescriptor);
37
38#[doc(hidden)]
40pub struct OpenApiRouteDescriptor {
41 method: &'static str,
42 path: &'static str,
43 operation_id: &'static str,
44 tag: &'static str,
45 params: &'static [OpenApiParamDescriptor],
46 request: Option<OpenApiSchemaDescriptor>,
47 response: Option<OpenApiSchemaDescriptor>,
48}
49
50impl OpenApiRouteDescriptor {
51 #[doc(hidden)]
52 pub const fn new(
53 method: &'static str,
54 path: &'static str,
55 operation_id: &'static str,
56 tag: &'static str,
57 params: &'static [OpenApiParamDescriptor],
58 request: Option<OpenApiSchemaDescriptor>,
59 response: Option<OpenApiSchemaDescriptor>,
60 ) -> Self {
61 Self {
62 method,
63 path,
64 operation_id,
65 tag,
66 params,
67 request,
68 response,
69 }
70 }
71}
72
73inventory::collect!(OpenApiRouteDescriptor);
74
75#[doc(hidden)]
77#[derive(Clone, Copy)]
78pub struct OpenApiSchemaDescriptor {
79 schema_ref: fn() -> MetaSchemaRef,
80 register: fn(&mut Registry),
81 content_type: &'static str,
82}
83
84impl OpenApiSchemaDescriptor {
85 #[doc(hidden)]
86 pub const fn json<T: Type>() -> Self {
87 Self {
88 schema_ref: schema_ref::<T>,
89 register: register_type::<T>,
90 content_type: "application/json",
91 }
92 }
93
94 #[doc(hidden)]
95 pub const fn form<T: Type>() -> Self {
96 Self {
97 schema_ref: schema_ref::<T>,
98 register: register_type::<T>,
99 content_type: "application/x-www-form-urlencoded",
100 }
101 }
102}
103
104#[doc(hidden)]
106#[derive(Clone, Copy)]
107pub struct OpenApiParamDescriptor {
108 name: &'static str,
109 in_type: MetaParamIn,
110 schema: OpenApiSchemaDescriptor,
111 required: bool,
112 deprecated: bool,
113 explode: bool,
114 style: Option<ParameterStyle>,
115}
116
117impl OpenApiParamDescriptor {
118 #[doc(hidden)]
119 pub const fn path<T: Type>(name: &'static str) -> Self {
120 Self {
121 name,
122 in_type: MetaParamIn::Path,
123 schema: OpenApiSchemaDescriptor::json::<T>(),
124 required: true,
125 deprecated: false,
126 explode: false,
127 style: None,
128 }
129 }
130
131 #[doc(hidden)]
132 pub const fn query<T: Type>(name: &'static str) -> Self {
133 Self {
134 name,
135 in_type: MetaParamIn::Query,
136 schema: OpenApiSchemaDescriptor::json::<T>(),
137 required: true,
138 deprecated: false,
139 explode: true,
140 style: Some(ParameterStyle::Form),
141 }
142 }
143}
144
145fn schema_ref<T: Type>() -> MetaSchemaRef {
146 T::schema_ref()
147}
148
149fn register_type<T: Type>(registry: &mut Registry) {
150 T::register(registry);
151}
152
153pub async fn build_routes(container: &Container) -> Result<Router<()>, DiError> {
155 let mut router = Router::new();
156 for descriptor in inventory::iter::<RouteDescriptor> {
157 router = router.merge((descriptor.factory)(container).await?);
158 }
159 Ok(router)
160}
161
162pub async fn routes() -> Result<Router<()>, DiError> {
164 build_routes(auto_di::global_container()?).await
165}
166
167#[doc(hidden)]
168pub struct AutoRouteOpenApi;
169
170impl OpenApi for AutoRouteOpenApi {
171 fn meta() -> Vec<MetaApi> {
172 let mut paths = BTreeMap::<String, Vec<MetaOperation>>::new();
173
174 for descriptor in inventory::iter::<OpenApiRouteDescriptor> {
175 paths
176 .entry(descriptor.path.to_owned())
177 .or_default()
178 .push(openapi_operation(descriptor));
179 }
180
181 vec![MetaApi {
182 paths: paths
183 .into_iter()
184 .map(|(path, operations)| MetaPath { path, operations })
185 .collect(),
186 }]
187 }
188
189 fn register(registry: &mut Registry) {
190 for descriptor in inventory::iter::<OpenApiRouteDescriptor> {
191 registry.tags.insert(MetaTag {
192 name: descriptor.tag,
193 description: None,
194 external_docs: None,
195 });
196 if let Some(schema) = descriptor.request {
197 (schema.register)(registry);
198 }
199 if let Some(schema) = descriptor.response {
200 (schema.register)(registry);
201 }
202 for param in descriptor.params {
203 (param.schema.register)(registry);
204 }
205 }
206 }
207
208 fn add_routes(self, _route_table: &mut HashMap<String, HashMap<Method, BoxEndpoint<'static>>>) {
209 }
212}
213
214pub fn openapi_json() -> Value {
217 serde_json::from_str(&openapi_spec()).expect("poem-openapi spec must be valid JSON")
218}
219
220pub fn openapi_spec() -> String {
222 OpenApiService::new(
223 AutoRouteOpenApi,
224 env!("CARGO_PKG_NAME"),
225 env!("CARGO_PKG_VERSION"),
226 )
227 .spec()
228}
229
230fn openapi_operation(descriptor: &OpenApiRouteDescriptor) -> MetaOperation {
231 MetaOperation {
232 method: method_from_str(descriptor.method),
233 tags: vec![descriptor.tag],
234 summary: None,
235 description: None,
236 external_docs: None,
237 params: descriptor
238 .params
239 .iter()
240 .map(|param| param.to_meta())
241 .collect(),
242 request: descriptor.request.map(|schema| MetaRequest {
243 description: None,
244 content: vec![schema.media_type()],
245 required: true,
246 }),
247 responses: MetaResponses {
248 responses: vec![MetaResponse {
249 description: "Successful response",
250 status: Some(200),
251 status_range: None,
252 content: descriptor
253 .response
254 .map(|schema| vec![schema.media_type()])
255 .unwrap_or_default(),
256 headers: Vec::new(),
257 }],
258 },
259 deprecated: false,
260 security: Vec::new(),
261 operation_id: Some(descriptor.operation_id),
262 code_samples: Vec::new(),
263 }
264}
265
266impl OpenApiSchemaDescriptor {
267 fn media_type(self) -> MetaMediaType {
268 MetaMediaType {
269 content_type: self.content_type,
270 schema: (self.schema_ref)(),
271 }
272 }
273}
274
275impl OpenApiParamDescriptor {
276 fn to_meta(self) -> MetaOperationParam {
277 MetaOperationParam {
278 name: self.name.trim_start_matches('*').to_owned(),
279 schema: (self.schema.schema_ref)(),
280 in_type: self.in_type,
281 description: None,
282 required: self.required,
283 deprecated: self.deprecated,
284 explode: self.explode,
285 style: self.style,
286 }
287 }
288}
289
290fn method_from_str(method: &str) -> Method {
291 match method {
292 "GET" => Method::GET,
293 "POST" => Method::POST,
294 "PUT" => Method::PUT,
295 "DELETE" => Method::DELETE,
296 "PATCH" => Method::PATCH,
297 "OPTIONS" => Method::OPTIONS,
298 "HEAD" => Method::HEAD,
299 _ => Method::GET,
300 }
301}
302
303pub fn openapi_routes(json_path: &'static str, ui_path: &'static str) -> Router<()> {
305 Router::new()
306 .route(json_path, get(|| async { Json(openapi_json()) }))
307 .route(ui_path, get(|| async { Html(swagger_ui_html()) }))
308}
309
310pub fn scalar_routes(scalar_path: &'static str, openapi_json_path: &'static str) -> Router<()> {
312 let configuration = serde_json::json!({
313 "url": openapi_json_path,
314 "theme": "kepler",
315 "layout": "modern",
316 "showSidebar": true,
317 "hideDownloadButton": false,
318 "agent": { "disabled": true },
319 });
320
321 scalar::router(scalar_path, &configuration)
322}
323
324fn swagger_ui_html() -> String {
325 OpenApiService::new(
326 AutoRouteOpenApi,
327 env!("CARGO_PKG_NAME"),
328 env!("CARGO_PKG_VERSION"),
329 )
330 .swagger_ui_html()
331}
332
333#[doc(hidden)]
336pub mod __private {
337 pub use auto_di;
338 pub use axum;
339 pub use inventory;
340 pub use poem_openapi;
341}