poem_openapi/
base.rs

1use std::{
2    collections::HashMap,
3    fmt::{self, Debug, Display},
4    future::Future,
5    ops::Deref,
6};
7
8use futures_util::FutureExt;
9use poem::{endpoint::BoxEndpoint, http::Method, Error, FromRequest, Request, RequestBody, Result};
10use serde::Serialize;
11
12use crate::{
13    payload::Payload,
14    registry::{
15        MetaApi, MetaMediaType, MetaOAuthScope, MetaParamIn, MetaRequest, MetaResponse,
16        MetaResponses, MetaSchemaRef, MetaWebhook, Registry,
17    },
18};
19
20/// The style of the passed parameter. See https://swagger.io/docs/specification/v3_0/serialization/ for details
21#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize)]
22#[serde(rename_all = "camelCase")]
23pub enum ParameterStyle {
24    /// Dot-prefixed values, also known as label expansion.
25    Label,
26    /// Semicolon-prefixed values, also known as path-style expansion.
27    Matrix,
28    /// Ampersand-separated values, also known as form-style query expansion.
29    Form,
30    /// Comma-separated values
31    Simple,
32    /// Space-separated array values. Has effect only for non-exploded arrays.
33    SpaceDelimited,
34    /// Pipeline-separated array values. Has effect only for non-exploded
35    /// arrays.
36    PipeDelimited,
37    /// Bracket-nested objects, e.g.
38    /// `paramName[prop1]=value1&paramName[prop2]=value2`
39    DeepObject,
40}
41
42/// API extractor types.
43#[derive(Debug, Copy, Clone, Eq, PartialEq)]
44pub enum ApiExtractorType {
45    /// A request object.
46    RequestObject,
47
48    /// A request parameter.
49    Parameter,
50
51    /// A security scheme.
52    SecurityScheme,
53
54    /// A poem extractor.
55    PoemExtractor,
56}
57
58#[doc(hidden)]
59#[derive(Clone)]
60pub struct UrlQuery(pub Vec<(String, String)>);
61
62impl Deref for UrlQuery {
63    type Target = Vec<(String, String)>;
64
65    fn deref(&self) -> &Self::Target {
66        &self.0
67    }
68}
69
70impl UrlQuery {
71    /// Returns all values with the specified name.
72    pub fn get_all<'a, 'b: 'a>(&'b self, name: &'a str) -> impl Iterator<Item = &'b String> + 'a {
73        self.0
74            .iter()
75            .filter(move |(n, _)| n == name)
76            .map(|(_, value)| value)
77    }
78
79    /// Returns the first value with the specified name.
80    pub fn get(&self, name: &str) -> Option<&String> {
81        self.get_all(name).next()
82    }
83}
84
85/// Options for the parameter extractor.
86#[derive(Clone)]
87pub struct ExtractParamOptions<T> {
88    /// The name of this parameter.
89    pub name: &'static str,
90
91    /// The default value of this parameter.
92    pub default_value: Option<fn() -> T>,
93
94    /// The example value of this parameter.
95    pub example_value: Option<fn() -> T>,
96
97    /// When this is `true`, parameter values of type array or object generate
98    /// separate parameters for each value of the array or key-value pair of the
99    /// map.
100    pub explode: bool,
101
102    /// The style of the parameter.
103    pub style: Option<ParameterStyle>,
104}
105
106impl<T> Default for ExtractParamOptions<T> {
107    fn default() -> Self {
108        Self {
109            name: "",
110            default_value: None,
111            example_value: None,
112            explode: true,
113            style: None,
114        }
115    }
116}
117
118/// Represents an OpenAPI extractor.
119///
120/// # Provided Implementations
121///
122/// - **Path&lt;T: Type>**
123///
124///    Extract the parameters in the request path into
125///    [`Path`](crate::param::Path).
126///
127/// - **Query&lt;T: Type>**
128///
129///    Extract the parameters in the query string into
130///    [`Query`](crate::param::Query).
131///
132/// - **Header&lt;T: Type>**
133///
134///    Extract the parameters in the request header into
135///    [`Header`](crate::param::Header).
136///
137/// - **Cookie&lt;T: Type>**
138///
139///    Extract the parameters in the cookie into
140///    [`Cookie`](crate::param::Cookie).
141///
142/// - **CookiePrivate&lt;T: Type>**
143///
144///    Extract the parameters in the private cookie into
145///    [`CookiePrivate`](crate::param::CookiePrivate).
146///
147/// - **CookieSigned&lt;T: Type>**
148///
149///    Extract the parameters in the signed cookie into
150///    [`CookieSigned`](crate::param::CookieSigned).
151///
152/// - **Binary&lt;T>**
153///
154///    Extract the request body as binary into
155///    [`Binary`](crate::payload::Binary).
156///
157/// - **Json&lt;T>**
158///
159///    Parse the request body in `JSON` format into
160///    [`Json`](crate::payload::Json).
161///
162/// - **PlainText&lt;T>**
163///
164///    Extract the request body as utf8 string into
165///    [`PlainText`](crate::payload::PlainText).
166///
167/// - **Any type derived from the [`ApiRequest`](crate::ApiRequest) macro**
168///
169///    Extract the complex request body derived from the `ApiRequest` macro.
170///
171/// - **Any type derived from the [`Multipart`](crate::Multipart) macro**
172///
173///    Extract the multipart object derived from the `Multipart` macro.
174///
175/// - **Any type derived from the [`SecurityScheme`](crate::SecurityScheme)
176///   macro**
177///
178///    Extract the authentication value derived from the `SecurityScheme`
179///    macro.
180///
181/// - **T: poem::FromRequest**
182///
183///    Use Poem's extractor.
184#[allow(unused_variables)]
185pub trait ApiExtractor<'a>: Sized {
186    /// The type of API extractor.
187    const TYPES: &'static [ApiExtractorType];
188
189    /// If it is `true`, it means that this parameter is required.
190    const PARAM_IS_REQUIRED: bool = false;
191
192    /// The parameter type.
193    type ParamType;
194
195    /// The raw parameter type for validators.
196    type ParamRawType;
197
198    /// Register related types to registry.
199    fn register(registry: &mut Registry) {}
200
201    /// Returns names of security scheme if this extractor is security scheme.
202    fn security_schemes() -> Vec<&'static str> {
203        vec![]
204    }
205
206    /// Returns `true` if the extractor is a security scheme with a fallback.
207    fn has_security_fallback() -> bool {
208        false
209    }
210
211    /// Returns the location of the parameter if this extractor is parameter.
212    fn param_in() -> Option<MetaParamIn> {
213        None
214    }
215
216    /// Returns the schema of the parameter if this extractor is parameter.
217    fn param_schema_ref() -> Option<MetaSchemaRef> {
218        None
219    }
220
221    /// Returns `MetaRequest` if this extractor is request object.
222    fn request_meta() -> Option<MetaRequest> {
223        None
224    }
225
226    /// Returns a reference to the raw type of this parameter.
227    fn param_raw_type(&self) -> Option<&Self::ParamRawType> {
228        None
229    }
230
231    /// Parse from the HTTP request.
232    fn from_request(
233        request: &'a Request,
234        body: &mut RequestBody,
235        param_opts: ExtractParamOptions<Self::ParamType>,
236    ) -> impl Future<Output = Result<Self>> + Send;
237}
238
239impl<'a, T: FromRequest<'a>> ApiExtractor<'a> for T {
240    const TYPES: &'static [ApiExtractorType] = &[ApiExtractorType::PoemExtractor];
241
242    type ParamType = ();
243    type ParamRawType = ();
244
245    async fn from_request(
246        request: &'a Request,
247        body: &mut RequestBody,
248        _param_opts: ExtractParamOptions<Self::ParamType>,
249    ) -> Result<Self> {
250        // FIXME: remove the unnecessary boxed
251        // https://github.com/rust-lang/rust/issues/100013
252        T::from_request(request, body).boxed().await
253    }
254}
255
256/// Represents an OpenAPI response content object.
257pub trait ResponseContent {
258    /// Returns the media types in this content.
259    fn media_types() -> Vec<MetaMediaType>;
260
261    /// Register the schema contained in this content to the registry.
262    #[allow(unused_variables)]
263    fn register(registry: &mut Registry) {}
264}
265
266impl<T: Payload> ResponseContent for T {
267    fn media_types() -> Vec<MetaMediaType> {
268        vec![MetaMediaType {
269            content_type: T::CONTENT_TYPE,
270            schema: T::schema_ref(),
271        }]
272    }
273
274    fn register(registry: &mut Registry) {
275        T::register(registry);
276    }
277}
278
279/// Represents an OpenAPI responses object.
280///
281/// # Provided Implementations
282///
283/// - **Binary&lt;T: Type>**
284///
285///    A binary response with content type `application/octet-stream`.
286///
287/// - **Json&lt;T: Type>**
288///
289///    A JSON response with content type `application/json`.
290///
291/// - **PlainText&lt;T: Type>**
292///
293///    A utf8 string response with content type `text/plain`.
294///
295/// - **Attachment&lt;T: Type>**
296///
297///    A file download response, the content type is
298///    `application/octet-stream`.
299///
300/// - **Response&lt;T: Type>**
301///
302///    A response type use it to modify the status code and HTTP headers.
303///
304/// - **()**
305///
306///     It means that this API does not have any response body.
307///
308/// - **poem::Result&lt;T: ApiResponse>**
309///
310///     It means that an error may occur in this API.
311///
312/// - **Any type derived from the [`ApiResponse`](crate::ApiResponse) macro**
313///
314///     A complex response  derived from the `ApiResponse` macro.
315pub trait ApiResponse: Sized {
316    /// If true, it means that the response object has a custom bad request
317    /// handler.
318    const BAD_REQUEST_HANDLER: bool = false;
319
320    /// Gets metadata of this response.
321    fn meta() -> MetaResponses;
322
323    /// Register the schema contained in this response object to the registry.
324    fn register(registry: &mut Registry);
325
326    /// Convert [`poem::Error`] to this response object.
327    #[allow(unused_variables)]
328    fn from_parse_request_error(err: Error) -> Self {
329        unreachable!()
330    }
331}
332
333impl ApiResponse for () {
334    fn meta() -> MetaResponses {
335        MetaResponses {
336            responses: vec![MetaResponse {
337                description: "",
338                status: Some(200),
339                status_range: None,
340                content: vec![],
341                headers: vec![],
342            }],
343        }
344    }
345
346    fn register(_registry: &mut Registry) {}
347}
348
349impl ApiResponse for Error {
350    fn meta() -> MetaResponses {
351        MetaResponses {
352            responses: Vec::new(),
353        }
354    }
355
356    fn register(_registry: &mut Registry) {}
357}
358
359impl<T, E> ApiResponse for Result<T, E>
360where
361    T: ApiResponse,
362    E: ApiResponse + Into<Error> + Send + Sync + 'static,
363{
364    const BAD_REQUEST_HANDLER: bool = T::BAD_REQUEST_HANDLER;
365
366    fn meta() -> MetaResponses {
367        let mut meta = T::meta();
368        meta.responses.extend(E::meta().responses);
369        meta
370    }
371
372    fn register(registry: &mut Registry) {
373        T::register(registry);
374        E::register(registry);
375    }
376
377    fn from_parse_request_error(err: Error) -> Self {
378        Ok(T::from_parse_request_error(err))
379    }
380}
381
382#[cfg(feature = "websocket")]
383impl<F, Fut> ApiResponse for poem::web::websocket::WebSocketUpgraded<F>
384where
385    F: FnOnce(poem::web::websocket::WebSocketStream) -> Fut + Send + Sync + 'static,
386    Fut: std::future::Future + Send + 'static,
387{
388    fn meta() -> MetaResponses {
389        MetaResponses {
390            responses: vec![MetaResponse {
391                description: "A websocket response",
392                status: Some(101),
393                status_range: None,
394                content: vec![],
395                headers: vec![],
396            }],
397        }
398    }
399
400    fn register(_registry: &mut Registry) {}
401}
402
403/// Represents an OpenAPI tags.
404pub trait Tags {
405    /// Register this tag type to registry.
406    fn register(&self, registry: &mut Registry);
407
408    /// Gets the tag name.
409    fn name(&self) -> &'static str;
410}
411
412/// Represents a OAuth scopes.
413pub trait OAuthScopes {
414    /// Gets metadata of this object.
415    fn meta() -> Vec<MetaOAuthScope>;
416
417    /// Get the scope name.
418    fn name(&self) -> &'static str;
419}
420
421/// A operation id that can be obtained from the response
422#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
423pub struct OperationId(pub &'static str);
424
425impl Display for OperationId {
426    #[inline]
427    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
428        f.write_str(self.0)
429    }
430}
431
432/// Represents an OpenAPI object.
433pub trait OpenApi: Sized {
434    /// Gets metadata of this API object.
435    fn meta() -> Vec<MetaApi>;
436
437    /// Register some types to the registry.
438    fn register(registry: &mut Registry);
439
440    /// Adds all API endpoints to the routing object.
441    fn add_routes(self, route_table: &mut HashMap<String, HashMap<Method, BoxEndpoint<'static>>>);
442}
443
444macro_rules! impl_openapi_for_tuple {
445    (($head:ident, $hn:tt), $(($tail:ident, $tn:tt)),*) => {
446        impl<$head: OpenApi, $($tail: OpenApi),*> OpenApi for ($head, $($tail),*) {
447            fn meta() -> Vec<MetaApi> {
448                let mut metadata = $head::meta();
449                $(
450                metadata.extend($tail::meta());
451                )*
452                metadata
453            }
454
455            fn register(registry: &mut Registry) {
456                $head::register(registry);
457                $(
458                $tail::register(registry);
459                )*
460            }
461
462            fn add_routes(self, route_table: &mut HashMap<String, HashMap<Method, BoxEndpoint<'static>>>) {
463                self.$hn.add_routes(route_table);
464                $(
465                self.$tn.add_routes(route_table);
466                )*
467            }
468        }
469    };
470
471    () => {};
472}
473
474#[rustfmt::skip]
475impl_openapi_for_tuple!((T1, 0), (T2, 1), (T3, 2), (T4, 3), (T5, 4), (T6, 5), (T7, 6), (T8, 7), (T9, 8), (T10, 9), (T11, 10), (T12, 11), (T13, 12), (T14, 13), (T15, 14), (T16, 15));
476#[rustfmt::skip]
477impl_openapi_for_tuple!((T1, 0), (T2, 1), (T3, 2), (T4, 3), (T5, 4), (T6, 5), (T7, 6), (T8, 7), (T9, 8), (T10, 9), (T11, 10), (T12, 11), (T13, 12), (T14, 13), (T15, 14));
478#[rustfmt::skip]
479impl_openapi_for_tuple!((T1, 0), (T2, 1), (T3, 2), (T4, 3), (T5, 4), (T6, 5), (T7, 6), (T8, 7), (T9, 8), (T10, 9), (T11, 10), (T12, 11), (T13, 12), (T14, 13));
480#[rustfmt::skip]
481impl_openapi_for_tuple!((T1, 0), (T2, 1), (T3, 2), (T4, 3), (T5, 4), (T6, 5), (T7, 6), (T8, 7), (T9, 8), (T10, 9), (T11, 10), (T12, 11), (T13, 12));
482#[rustfmt::skip]
483impl_openapi_for_tuple!((T1, 0), (T2, 1), (T3, 2), (T4, 3), (T5, 4), (T6, 5), (T7, 6), (T8, 7), (T9, 8), (T10, 9), (T11, 10), (T12, 11));
484#[rustfmt::skip]
485impl_openapi_for_tuple!((T1, 0), (T2, 1), (T3, 2), (T4, 3), (T5, 4), (T6, 5), (T7, 6), (T8, 7), (T9, 8), (T10, 9), (T11, 10));
486#[rustfmt::skip]
487impl_openapi_for_tuple!((T1, 0), (T2, 1), (T3, 2), (T4, 3), (T5, 4), (T6, 5), (T7, 6), (T8, 7), (T9, 8), (T10, 9));
488#[rustfmt::skip]
489impl_openapi_for_tuple!((T1, 0), (T2, 1), (T3, 2), (T4, 3), (T5, 4), (T6, 5), (T7, 6), (T8, 7), (T9, 8));
490#[rustfmt::skip]
491impl_openapi_for_tuple!((T1, 0), (T2, 1), (T3, 2), (T4, 3), (T5, 4), (T6, 5), (T7, 6), (T8, 7));
492#[rustfmt::skip]
493impl_openapi_for_tuple!((T1, 0), (T2, 1), (T3, 2), (T4, 3), (T5, 4), (T6, 5), (T7, 6));
494#[rustfmt::skip]
495impl_openapi_for_tuple!((T1, 0), (T2, 1), (T3, 2), (T4, 3), (T5, 4), (T6, 5));
496#[rustfmt::skip]
497impl_openapi_for_tuple!((T1, 0), (T2, 1), (T3, 2), (T4, 3), (T5, 4));
498#[rustfmt::skip]
499impl_openapi_for_tuple!((T1, 0), (T2, 1), (T3, 2), (T4, 3));
500#[rustfmt::skip]
501impl_openapi_for_tuple!((T1, 0), (T2, 1), (T3, 2));
502#[rustfmt::skip]
503impl_openapi_for_tuple!((T1, 0), (T2, 1));
504
505impl OpenApi for () {
506    fn meta() -> Vec<MetaApi> {
507        vec![]
508    }
509
510    fn register(_registry: &mut Registry) {}
511
512    fn add_routes(self, _route_table: &mut HashMap<String, HashMap<Method, BoxEndpoint<'static>>>) {
513    }
514}
515
516/// Represents a webhook object.
517pub trait Webhook: Sized {
518    /// Gets metadata of this webhooks object.
519    fn meta() -> Vec<MetaWebhook>;
520
521    /// Register some types to the registry.
522    fn register(registry: &mut Registry);
523}
524
525impl Webhook for () {
526    fn meta() -> Vec<MetaWebhook> {
527        vec![]
528    }
529
530    fn register(_: &mut Registry) {}
531}