gotham_restful/openapi/
router.rs

1use super::{
2	builder::OpenapiBuilder,
3	handler::{OpenapiDocHandler, OpenapiSpecHandler},
4	operation::OperationDescription
5};
6use crate::{routing::*, EndpointWithSchema, ResourceWithSchema, ResponseSchema};
7use gotham::{
8	hyper::{Method, StatusCode},
9	pipeline::PipelineHandleChain,
10	prelude::*,
11	router::builder::{RouterBuilder, ScopeBuilder}
12};
13use lazy_regex::regex_replace_all;
14use openapi_type::OpenapiType;
15use std::{collections::HashMap, panic::RefUnwindSafe};
16
17/// This trait adds the `openapi_spec` and `openapi_doc` method to an OpenAPI-aware router.
18pub trait GetOpenapi {
19	/// Register a GET route to `path` that returns the OpenAPI specification in JSON format.
20	fn openapi_spec(&mut self, path: &str);
21
22	/// Register a GET route to `path` that returns the OpenAPI documentation in HTML format.
23	fn openapi_doc(&mut self, path: &str);
24}
25
26#[derive(Debug)]
27pub struct OpenapiRouter<'a, D> {
28	pub(crate) router: &'a mut D,
29	pub(crate) scope: Option<&'a str>,
30	pub(crate) openapi_builder: &'a mut OpenapiBuilder
31}
32
33macro_rules! implOpenapiRouter {
34	($implType:ident) => {
35		impl<'a, 'b, C, P> OpenapiRouter<'a, $implType<'b, C, P>>
36		where
37			C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
38			P: RefUnwindSafe + Send + Sync + 'static
39		{
40			pub fn scope<F>(&mut self, path: &str, callback: F)
41			where
42				F: FnOnce(&mut OpenapiRouter<'_, ScopeBuilder<'_, C, P>>)
43			{
44				let mut openapi_builder = self.openapi_builder.clone();
45				let new_scope = self
46					.scope
47					.map(|scope| format!("{scope}/{path}").replace("//", "/"));
48				self.router.scope(path, |router| {
49					let mut router = OpenapiRouter {
50						router,
51						scope: Some(new_scope.as_ref().map(String::as_ref).unwrap_or(path)),
52						openapi_builder: &mut openapi_builder
53					};
54					callback(&mut router);
55				});
56			}
57		}
58
59		impl<'a, 'b, C, P> GetOpenapi for OpenapiRouter<'a, $implType<'b, C, P>>
60		where
61			C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
62			P: RefUnwindSafe + Send + Sync + 'static
63		{
64			fn openapi_spec(&mut self, path: &str) {
65				self.router
66					.get(path)
67					.to_new_handler(OpenapiSpecHandler::new(
68						self.openapi_builder.openapi.clone()
69					));
70			}
71
72			fn openapi_doc(&mut self, path: &str) {
73				self.router
74					.get(path)
75					.to_new_handler(OpenapiDocHandler::new(self.openapi_builder.openapi.clone()));
76			}
77		}
78
79		impl<'a, 'b, C, P> DrawResourcesWithSchema for OpenapiRouter<'a, $implType<'b, C, P>>
80		where
81			C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
82			P: RefUnwindSafe + Send + Sync + 'static
83		{
84			fn resource<R: ResourceWithSchema>(&mut self, mut path: &str) {
85				if path.starts_with('/') {
86					path = &path[1..];
87				}
88				R::setup((self, path));
89			}
90		}
91
92		impl<'a, 'b, C, P> DrawResourceRoutesWithSchema
93			for (&mut OpenapiRouter<'a, $implType<'b, C, P>>, &str)
94		where
95			C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
96			P: RefUnwindSafe + Send + Sync + 'static
97		{
98			fn endpoint<E: EndpointWithSchema + 'static>(&mut self) {
99				let mut responses: HashMap<StatusCode, _> = HashMap::new();
100				for code in E::Output::status_codes() {
101					responses.insert(
102						code,
103						(self.0).openapi_builder.add_schema(E::Output::schema(code))
104					);
105				}
106				let mut path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
107				let mut descr = OperationDescription::new::<E>(responses, &path);
108				if E::has_placeholders() {
109					descr.set_path_params(E::Placeholders::schema());
110				}
111				if E::needs_params() {
112					descr.set_query_params(E::Params::schema());
113				}
114				if E::needs_body() {
115					let body_schema = (self.0).openapi_builder.add_schema(E::Body::schema());
116					descr.set_body::<E::Body>(body_schema);
117				}
118
119				let uri: &str = &E::uri();
120				let uri =
121					regex_replace_all!(r#"(^|/):([^/]+)(/|$)"#, uri, |_, prefix, name, suffix| {
122						format!("{prefix}{{{name}}}{suffix}")
123					});
124				if !uri.is_empty() {
125					path = format!("{path}/{uri}");
126				}
127
128				let op = descr.into_operation();
129				let mut item = (self.0).openapi_builder.remove_path(&path);
130				match E::http_method() {
131					Method::GET => item.get = Some(op),
132					Method::PUT => item.put = Some(op),
133					Method::POST => item.post = Some(op),
134					Method::DELETE => item.delete = Some(op),
135					Method::OPTIONS => item.options = Some(op),
136					Method::HEAD => item.head = Some(op),
137					Method::PATCH => item.patch = Some(op),
138					Method::TRACE => item.trace = Some(op),
139					method => {
140						warn!("Ignoring unsupported method '{method}' in OpenAPI Specification")
141					}
142				};
143				(self.0).openapi_builder.add_path(path, item);
144
145				(&mut *(self.0).router, self.1).endpoint::<E>()
146			}
147		}
148	};
149}
150
151implOpenapiRouter!(RouterBuilder);
152implOpenapiRouter!(ScopeBuilder);