Skip to main content

auto_route/
lib.rs

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/// A controller router factory submitted by the `#[controller]` macro.
22#[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/// OpenAPI metadata submitted by the route macros.
39#[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/// OpenAPI schema metadata submitted by the route macros.
76#[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/// OpenAPI parameter metadata submitted by the route macros.
105#[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
153/// Resolves every registered controller from `container` and merges its routes.
154pub 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
162/// Builds all routes using auto-di's global container.
163pub 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        // Axum serving is still handled by `RouteDescriptor`; this hidden Poem
210        // adapter exists only so OpenAPI generation goes through poem-openapi.
211    }
212}
213
214/// Builds an OpenAPI document from every registered auto-route using
215/// poem-openapi's `OpenApiService`.
216pub fn openapi_json() -> Value {
217    serde_json::from_str(&openapi_spec()).expect("poem-openapi spec must be valid JSON")
218}
219
220/// Builds an OpenAPI JSON string using poem-openapi's native service.
221pub 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
303/// Adds `/openapi.json` and `/swagger-ui` style routes to an Axum router.
304pub 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
310/// Adds a Scalar API Reference route backed by the generated OpenAPI document.
311pub 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/// Re-exports used by generated code. Applications don't need these dependencies
334/// solely because the macro expands to them.
335#[doc(hidden)]
336pub mod __private {
337    pub use auto_di;
338    pub use axum;
339    pub use inventory;
340    pub use poem_openapi;
341}