use std::collections::BTreeMap;
use std::sync::Arc;
use indexmap::IndexMap;
use openapiv3::Components;
use openapiv3::Contact;
use openapiv3::Info;
use openapiv3::License;
use openapiv3::OpenAPI;
use openapiv3::Operation;
use openapiv3::PathItem;
use openapiv3::Paths;
use openapiv3::ReferenceOr;
use schemars::schema::Schema;
use crate::internals::convert_schema;
use crate::internals::HttpMethod;
use crate::internals::SchemaGenerator;
use crate::internals::SwaggapiHandler;
use crate::page::SwaggapiPageBuilder;
pub trait AccessSwaggapiPageBuilder {
fn get_builder(&self) -> &'static SwaggapiPageBuilder;
}
impl AccessSwaggapiPageBuilder for &'static SwaggapiPageBuilder {
fn get_builder(&self) -> &'static SwaggapiPageBuilder {
*self
}
}
#[derive(Default)]
pub struct SwaggapiPageBuilderImpl {
paths: Paths,
schemas: BTreeMap<String, Schema>,
last_build: Option<Arc<OpenAPI>>,
}
impl SwaggapiPageBuilderImpl {
pub fn add_handler(
builder: &SwaggapiPageBuilder,
handler_path: String,
handler: SwaggapiHandler,
context_tags: &[&'static str],
) {
let mut guard = builder.state.lock().unwrap();
let state = guard.get_or_insert_with(Default::default);
state.last_build = None;
let (parameters, mut request_body, responses) =
SchemaGenerator::employ(&mut state.schemas, |gen| {
let mut parameters = Vec::new();
let mut request_body = Vec::new();
for arg in handler.handler_arguments {
if let Some(arg) = arg.as_ref() {
parameters.extend(
(arg.parameters)(&mut *gen)
.into_iter()
.map(ReferenceOr::Item),
);
request_body.extend((arg.request_body)(&mut *gen));
}
}
let responses = (handler.responses)(&mut *gen);
(parameters, request_body, responses)
});
let summary = handler.doc.get(0).map(|line| line.trim().to_string());
let description = summary.clone().map(|summary| {
handler
.doc
.get(1..)
.unwrap_or(&[])
.iter()
.fold(summary, |text, line| format!("{text}\n{}", line.trim()))
});
let operation = Operation {
summary,
description,
operation_id: Some(handler.ident.to_string()),
parameters,
request_body: request_body.pop().map(ReferenceOr::Item),
responses,
deprecated: handler.deprecated,
security: None, tags: handler
.tags
.iter()
.chain(context_tags.iter())
.copied()
.map(String::from)
.collect(),
external_docs: Default::default(),
servers: Default::default(),
extensions: Default::default(),
callbacks: Default::default(),
};
let ReferenceOr::Item(path) = state
.paths
.paths
.entry(handler_path)
.or_insert_with(|| ReferenceOr::Item(PathItem::default()))
else {
unreachable!("We only ever insert ReferenceOr::Item. See above")
};
let operation_mut = match handler.method {
HttpMethod::Get => &mut path.get,
HttpMethod::Post => &mut path.post,
HttpMethod::Put => &mut path.put,
HttpMethod::Delete => &mut path.delete,
HttpMethod::Head => &mut path.head,
HttpMethod::Options => &mut path.options,
HttpMethod::Patch => &mut path.patch,
HttpMethod::Trace => &mut path.trace,
};
*operation_mut = Some(operation);
}
pub fn build(builder: &SwaggapiPageBuilder) -> Arc<OpenAPI> {
let SwaggapiPageBuilder {
title,
description,
terms_of_service,
contact_name,
contact_url,
contact_email,
license_name,
license_url,
version,
filename: _,
state,
} = builder;
let mut guard = state.lock().unwrap();
let state = guard.get_or_insert_with(Default::default);
if let Some(open_api) = state.last_build.clone() {
return open_api;
}
let open_api = Arc::new(OpenAPI {
openapi: "3.0.0".to_string(),
info: Info {
title: title.unwrap_or("Unnamed API").to_string(),
description: description.map(str::to_string),
terms_of_service: terms_of_service.map(str::to_string),
contact: (contact_name.is_some()
|| contact_url.is_some()
|| contact_email.is_some())
.then(|| Contact {
name: contact_name.map(str::to_string),
url: contact_url.map(str::to_string),
email: contact_email.map(str::to_string),
extensions: Default::default(),
}),
license: (license_name.is_some() || license_url.is_some()).then(|| License {
name: builder
.license_name
.unwrap_or("Unnamed License")
.to_string(),
url: license_url.map(str::to_string),
extensions: Default::default(),
}),
version: version.unwrap_or("v0.0.0").to_string(),
extensions: IndexMap::new(),
},
servers: vec![],
paths: state.paths.clone(),
components: Some(Components {
schemas: state
.schemas
.iter()
.map(|(key, schema)| (key.clone(), convert_schema(schema.clone())))
.collect(),
..Default::default()
}),
security: None,
tags: vec![],
external_docs: None,
extensions: IndexMap::new(),
});
state.last_build = Some(open_api.clone());
open_api
}
}