use std::{collections::HashSet, marker::PhantomData};
use poem::{
endpoint::{make_sync, BoxEndpoint},
middleware::CookieJarManager,
web::cookie::CookieKey,
Endpoint, EndpointExt, IntoEndpoint, Request, Response, Result, Route,
};
use crate::{
base::UrlQuery,
registry::{Document, MetaExternalDocument, MetaInfo, MetaLicense, MetaServer, Registry},
OpenApi, Webhook,
};
#[derive(Debug, Clone)]
pub struct ServerObject {
url: String,
description: Option<String>,
}
impl<T: Into<String>> From<T> for ServerObject {
fn from(url: T) -> Self {
Self::new(url)
}
}
impl ServerObject {
pub fn new(url: impl Into<String>) -> ServerObject {
Self {
url: url.into(),
description: None,
}
}
#[must_use]
pub fn description(self, description: impl Into<String>) -> Self {
Self {
description: Some(description.into()),
..self
}
}
}
pub struct LicenseObject {
name: String,
identifier: Option<String>,
url: Option<String>,
}
impl<T: Into<String>> From<T> for LicenseObject {
fn from(url: T) -> Self {
Self::new(url)
}
}
impl LicenseObject {
pub fn new(name: impl Into<String>) -> LicenseObject {
Self {
name: name.into(),
identifier: None,
url: None,
}
}
#[must_use]
pub fn identifier(self, identifier: impl Into<String>) -> Self {
Self {
identifier: Some(identifier.into()),
..self
}
}
#[must_use]
pub fn url(self, url: impl Into<String>) -> Self {
Self {
url: Some(url.into()),
..self
}
}
}
#[derive(Debug, Clone)]
pub struct ExternalDocumentObject {
url: String,
description: Option<String>,
}
impl<T: Into<String>> From<T> for ExternalDocumentObject {
fn from(url: T) -> Self {
Self::new(url)
}
}
impl ExternalDocumentObject {
pub fn new(url: impl Into<String>) -> ExternalDocumentObject {
Self {
url: url.into(),
description: None,
}
}
#[must_use]
pub fn description(self, description: impl Into<String>) -> Self {
Self {
description: Some(description.into()),
..self
}
}
}
pub struct OpenApiService<T, W: ?Sized> {
api: T,
_webhook: PhantomData<W>,
info: MetaInfo,
external_document: Option<MetaExternalDocument>,
servers: Vec<MetaServer>,
cookie_key: Option<CookieKey>,
}
impl<T> OpenApiService<T, ()> {
#[must_use]
pub fn new(api: T, title: impl Into<String>, version: impl Into<String>) -> Self {
Self {
api,
_webhook: PhantomData,
info: MetaInfo {
title: title.into(),
summary: None,
description: None,
version: version.into(),
terms_of_service: None,
license: None,
},
external_document: None,
servers: Vec::new(),
cookie_key: None,
}
}
}
impl<T, W: ?Sized> OpenApiService<T, W> {
pub fn webhooks<W2: ?Sized>(self) -> OpenApiService<T, W2> {
OpenApiService {
api: self.api,
_webhook: PhantomData,
info: self.info,
external_document: self.external_document,
servers: self.servers,
cookie_key: self.cookie_key,
}
}
#[must_use]
pub fn summary(mut self, summary: impl Into<String>) -> Self {
self.info.summary = Some(summary.into());
self
}
#[must_use]
pub fn description(mut self, description: impl Into<String>) -> Self {
self.info.description = Some(description.into());
self
}
#[must_use]
pub fn terms_of_service(mut self, url: impl Into<String>) -> Self {
self.info.terms_of_service = Some(url.into());
self
}
#[must_use]
pub fn server(mut self, server: impl Into<ServerObject>) -> Self {
let server = server.into();
self.servers.push(MetaServer {
url: server.url,
description: server.description,
});
self
}
#[must_use]
pub fn license(mut self, license: impl Into<LicenseObject>) -> Self {
let license = license.into();
self.info.license = Some(MetaLicense {
name: license.name,
identifier: license.identifier,
url: license.url,
});
self
}
#[must_use]
pub fn external_document(
mut self,
external_document: impl Into<ExternalDocumentObject>,
) -> Self {
let external_document = external_document.into();
self.external_document = Some(MetaExternalDocument {
url: external_document.url,
description: external_document.description,
});
self
}
#[must_use]
pub fn cookie_key(self, key: CookieKey) -> Self {
Self {
cookie_key: Some(key),
..self
}
}
#[must_use]
#[cfg(feature = "swagger-ui")]
pub fn swagger_ui(&self) -> impl Endpoint
where
T: OpenApi,
W: Webhook,
{
crate::ui::swagger_ui::create_endpoint(&self.spec())
}
#[must_use]
#[cfg(feature = "rapidoc")]
pub fn rapidoc(&self) -> impl Endpoint
where
T: OpenApi,
W: Webhook,
{
crate::ui::rapidoc::create_endpoint(&self.spec())
}
#[must_use]
#[cfg(feature = "redoc")]
pub fn redoc(&self) -> impl Endpoint
where
T: OpenApi,
W: Webhook,
{
crate::ui::redoc::create_endpoint(&self.spec())
}
pub fn spec_endpoint(&self) -> impl Endpoint
where
T: OpenApi,
W: Webhook,
{
let spec = self.spec();
make_sync(move |_| {
Response::builder()
.content_type("application/json")
.body(spec.clone())
})
}
pub fn spec(&self) -> String
where
T: OpenApi,
W: Webhook,
{
let mut registry = Registry::new();
let metadata = T::meta();
T::register(&mut registry);
W::register(&mut registry);
let webhooks = W::meta();
let doc = Document {
info: &self.info,
servers: &self.servers,
apis: &metadata,
webhooks: &webhooks,
registry: ®istry,
external_document: self.external_document.as_ref(),
};
serde_json::to_string_pretty(&doc).unwrap()
}
}
impl<T: OpenApi, W: Webhook> IntoEndpoint for OpenApiService<T, W> {
type Endpoint = BoxEndpoint<'static, Response>;
fn into_endpoint(self) -> Self::Endpoint {
async fn extract_query(mut req: Request) -> Result<Request> {
let url_query: Vec<(String, String)> = req.params().unwrap_or_default();
req.extensions_mut().insert(UrlQuery(url_query));
Ok(req)
}
let cookie_jar_manager = match self.cookie_key {
Some(key) => CookieJarManager::with_key(key),
None => CookieJarManager::new(),
};
let mut operation_ids = HashSet::new();
for operation in T::meta()
.into_iter()
.flat_map(|api| api.paths.into_iter())
.flat_map(|path| path.operations.into_iter())
{
if let Some(operation_id) = operation.operation_id {
if !operation_ids.insert(operation_id) {
panic!("duplicate operation id: {}", operation_id);
}
}
}
self.api
.add_routes(Route::new())
.with(cookie_jar_manager)
.before(extract_query)
.map_to_response()
.boxed()
}
}