use std::collections::HashMap;
use std::env::var;
use std::future::Future;
use std::path::Path;
use std::sync::Arc;
use http::Method;
use once_cell::sync::Lazy;
use openapiv3::{OpenAPI, Operation, ReferenceOr, Parameter, ParameterKind};
use oasgen_core::{OaSchema};
#[cfg_attr(docsrs, doc(cfg(feature = "actix")))]
#[cfg(feature = "actix")]
mod actix;
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
#[cfg(feature = "axum")]
mod axum;
mod none;
static OPERATION_LOOKUP: Lazy<HashMap<&'static str, &'static (dyn Fn() -> Operation + Send + Sync)>> = Lazy::new(|| {
let mut map = HashMap::new();
for flag in inventory::iter::<oasgen_core::OperationRegister> {
let z: &'static (dyn Fn() -> Operation + Send + Sync) = flag.constructor;
map.insert(flag.name, z);
}
map
});
pub struct Server<Router, Mutability = OpenAPI> {
router: Router,
pub openapi: Mutability,
pub prefix: Option<String>,
pub json_route: Option<String>,
pub yaml_route: Option<String>,
#[cfg(feature = "swagger-ui")]
#[cfg_attr(docsrs, doc(cfg(feature = "swagger-ui")))]
pub swagger_ui_route: Option<String>,
#[cfg_attr(docsrs, doc(cfg(feature = "swagger-ui")))]
#[cfg(feature = "swagger-ui")]
pub swagger_ui: Option<swagger_ui::SwaggerUi>,
}
impl<Router: Clone> Clone for Server<Router, Arc<OpenAPI>> {
fn clone(&self) -> Self {
Server {
router: self.router.clone(),
openapi: self.openapi.clone(),
json_route: self.json_route.clone(),
yaml_route: self.yaml_route.clone(),
prefix: self.prefix.clone(),
#[cfg(feature = "swagger-ui")]
swagger_ui_route: self.swagger_ui_route.clone(),
#[cfg(feature = "swagger-ui")]
swagger_ui: self.swagger_ui.clone(),
}
}
}
impl<Router: Default> Server<Router, OpenAPI> {
pub fn new() -> Self {
let mut openapi = OpenAPI::default();
for flag in inventory::iter::<oasgen_core::SchemaRegister> {
let schema = (flag.constructor)();
openapi.schemas.insert(flag.name.to_string(), ReferenceOr::Item(schema));
}
openapi.schemas.sort_keys();
Self {
openapi,
router: Router::default(),
json_route: None,
yaml_route: None,
prefix: None,
#[cfg(feature = "swagger-ui")]
swagger_ui_route: None,
#[cfg(feature = "swagger-ui")]
swagger_ui: None,
}
}
fn add_handler_to_spec<F>(&mut self, path: &str, method: Method, _handler: &F) {
use http::Method;
let item = self.openapi.paths.paths.entry(path.to_string()).or_default();
let item = item.as_mut().expect("Currently don't support references for PathItem");
let type_name = std::any::type_name::<F>();
let mut operation = OPERATION_LOOKUP.get(type_name)
.expect(&format!("Operation {} not found in OpenAPI spec.", type_name))();
modify_parameter_names(&mut operation, &path);
match method {
Method::GET => item.get = Some(operation),
Method::POST => item.post = Some(operation),
Method::PUT => item.put = Some(operation),
Method::DELETE => item.delete = Some(operation),
Method::OPTIONS => item.options = Some(operation),
Method::HEAD => item.head = Some(operation),
Method::PATCH => item.patch = Some(operation),
Method::TRACE => item.trace = Some(operation),
_ => panic!("Unsupported method: {}", method),
}
}
pub fn route_json_spec(mut self, path: &str) -> Self {
self.json_route = Some(path.to_string());
self
}
pub fn route_yaml_spec(mut self, path: &str) -> Self {
self.yaml_route = Some(path.to_string());
self
}
pub fn prefix(mut self, prefix: &str) -> Self {
self.prefix = Some(prefix.to_string());
self
}
#[cfg(feature = "swagger-ui")]
#[cfg_attr(docsrs, doc(cfg(feature = "swagger-ui")))]
pub fn swagger_ui(mut self, swagger_ui_route: &str) -> Self {
if !swagger_ui_route.ends_with('/') {
panic!("Swagger UI route must end with a slash. Without it, static resources will not be found.");
}
let route_without_trailing = &swagger_ui_route[..swagger_ui_route.len() - 1];
let swagger = swagger_ui::SwaggerUi::default()
.prefix(route_without_trailing)
.url(self.json_route.as_ref()
.or(self.yaml_route.as_ref())
.expect("Tried to create Swagger UI route, but no JSON or YAML route was set. \
On `oasgen::Server` instance, call `route_yaml_spec` or `route_json_spec`. \
If you manually create the route, set the field, call this method, then set the field to None.")
);
self.swagger_ui_route = Some(swagger_ui_route.to_string());
self.swagger_ui = Some(swagger);
self
}
pub fn inspect(self, closure: impl Fn(&OpenAPI)) -> Self {
closure(&self.openapi);
self
}
pub fn write_and_exit_if_env_var_set<P: AsRef<Path>>(self, path: P) -> Self {
let path = path.as_ref();
if var("OASGEN_WRITE_SPEC").map(|s| s == "1").unwrap_or(false) {
let spec = if path.extension().map(|e| e == "json").unwrap_or(false) {
serde_json::to_string(&self.openapi).expect("Serializing OpenAPI spec to JSON failed.")
} else {
serde_yaml::to_string(&self.openapi).expect("Serializing OpenAPI spec failed.")
};
std::fs::write(path, spec).expect("Writing OpenAPI spec to file failed.");
eprintln!("{}: Wrote OpenAPI spec.", path.display());
std::process::exit(0);
}
self
}
pub fn freeze(self) -> Server<Router, Arc<OpenAPI>> {
Server {
router: self.router,
openapi: Arc::new(self.openapi),
json_route: self.json_route,
yaml_route: self.yaml_route,
prefix: self.prefix,
#[cfg(feature = "swagger-ui")]
swagger_ui_route: self.swagger_ui_route,
#[cfg(feature = "swagger-ui")]
swagger_ui: self.swagger_ui,
}
}
}
fn modify_parameter_names(operation: &mut Operation, path: &str) {
if !path.contains("{") {
return;
}
let path_parts = path.split("/")
.filter(|part| part.starts_with("{"))
.map(|part| &part[1..part.len() - 1]);
let path_params = operation.parameters.iter_mut()
.filter_map(|mut p| p.as_mut())
.filter(|p| matches!(p.kind, ParameterKind::Path { .. }));
for (part, param) in path_parts.zip(path_params) {
param.name = part.to_string();
}
}
#[cfg(test)]
mod tests {
use super::*;
use openapiv3 as oa;
#[test]
fn test_modify_parameter_names() {
let path = "/api/v1/pet/{id}/";
let mut operation = Operation::default();
operation.parameters.push(Parameter::path("path", oa::Schema::new_number()).into());
operation.parameters.push(Parameter::query("query", oa::Schema::new_number()).into());
modify_parameter_names(&mut operation, path);
assert_eq!(operation.parameters[0].as_item().unwrap().name, "id", "path param name is updated");
assert_eq!(operation.parameters[1].as_item().unwrap().name, "query", "leave query param alone");
}
}