use anyhow::{Context, bail};
use http::Method;
use indexmap::IndexMap;
use okapi::openapi3::{
Contact, ExternalDocs, License, OpenApi, SecurityRequirement, SecurityScheme, Server, Tag,
};
use crate::{OperationGenerator, components::Components};
#[derive(Clone)]
pub struct OpenApiBuilder {
spec: OpenApi,
components: Components,
operations: IndexMap<(String, Method), OperationGenerator>,
}
impl Default for OpenApiBuilder {
fn default() -> Self {
let spec = OpenApi {
openapi: OpenApi::default_version(),
..Default::default()
};
Self {
spec,
components: Components::new(Default::default()),
operations: IndexMap::new(),
}
}
}
impl OpenApiBuilder {
pub fn new(title: &str, version: &str) -> Self {
let mut this = Self::default();
this.title(title);
this.version(version);
this
}
pub fn set_components(&mut self, new_components: Components) -> &mut Self {
self.components = new_components;
self
}
pub fn try_operation<T>(
&mut self,
path: T,
method: Method,
generator: OperationGenerator,
) -> Result<&mut Self, anyhow::Error>
where
T: Into<String>,
{
let path = path.into();
if self
.operations
.insert((path.clone(), method.clone()), generator)
.is_some()
{
bail!("{method} {path} is already present in specification");
};
Ok(self)
}
pub fn try_operations<I, S>(&mut self, operations: I) -> Result<&mut Self, anyhow::Error>
where
I: Iterator<Item = (S, Method, OperationGenerator)>,
S: Into<String>,
{
for (path, method, f) in operations {
self.try_operation(path, method, f)?;
}
Ok(self)
}
pub fn operation<T>(
&mut self,
path: T,
method: Method,
generator: OperationGenerator,
) -> &mut Self
where
T: Into<String>,
{
let _ = self.try_operation(path, method, generator);
self
}
pub fn operations<I, S>(&mut self, operations: I) -> &mut Self
where
I: Iterator<Item = (S, Method, OperationGenerator)>,
S: Into<String>,
{
for (path, method, f) in operations {
self.operation(path, method, f);
}
self
}
pub fn spec_mut(&mut self) -> &mut OpenApi {
&mut self.spec
}
pub fn apply_global_security<N, S>(&mut self, name: N, scopes: S) -> &mut Self
where
N: Into<String>,
S: IntoIterator<Item = String>,
{
let mut sec = SecurityRequirement::new();
sec.insert(name.into(), scopes.into_iter().collect());
self.spec.security.push(sec);
self
}
pub fn build(&mut self) -> Result<OpenApi, anyhow::Error> {
let mut spec = self.spec.clone();
self.operations.sort_by(|lkey, _, rkey, _| {
let lkey_str = (&lkey.0, lkey.1.as_str());
let rkey_str = (&rkey.0, rkey.1.as_str());
lkey_str.cmp(&rkey_str)
});
for ((path, method), generator) in &self.operations {
try_add_path(
&mut spec,
&mut self.components,
path,
method.clone(),
*generator,
)
.with_context(|| format!("Failed to add {method} {path}"))?;
}
spec.components = Some(self.components.okapi_components()?);
Ok(spec)
}
pub fn title(&mut self, title: impl Into<String>) -> &mut Self {
self.spec.info.title = title.into();
self
}
pub fn version(&mut self, version: impl Into<String>) -> &mut Self {
self.spec.info.version = version.into();
self
}
pub fn description(&mut self, description: impl Into<String>) -> &mut Self {
self.spec.info.description = Some(description.into());
self
}
pub fn contact(&mut self, contact: Contact) -> &mut Self {
self.spec.info.contact = Some(contact);
self
}
pub fn license(&mut self, license: License) -> &mut Self {
self.spec.info.license = Some(license);
self
}
pub fn terms_of_service(&mut self, terms_of_service: impl Into<String>) -> &mut Self {
self.spec.info.terms_of_service = Some(terms_of_service.into());
self
}
pub fn server(&mut self, server: Server) -> &mut Self {
self.spec.servers.push(server);
self
}
pub fn tag(&mut self, tag: Tag) -> &mut Self {
self.spec.tags.push(tag);
self
}
pub fn external_docs(&mut self, docs: ExternalDocs) -> &mut Self {
let _ = self.spec.external_docs.insert(docs);
self
}
pub fn security_scheme<N>(&mut self, name: N, sec: SecurityScheme) -> &mut Self
where
N: Into<String>,
{
self.components.add_security_scheme(name, sec);
self
}
}
fn try_add_path(
spec: &mut OpenApi,
components: &mut Components,
path: &str,
method: Method,
generator: OperationGenerator,
) -> Result<(), anyhow::Error> {
let operation_schema = generator(components)?;
let path_str = path;
let path = spec.paths.entry(path.into()).or_default();
if method == Method::DELETE {
path.delete = Some(operation_schema);
} else if method == Method::GET {
path.get = Some(operation_schema);
} else if method == Method::HEAD {
path.head = Some(operation_schema);
} else if method == Method::OPTIONS {
path.options = Some(operation_schema);
} else if method == Method::PATCH {
path.patch = Some(operation_schema);
} else if method == Method::POST {
path.post = Some(operation_schema);
} else if method == Method::PUT {
path.put = Some(operation_schema);
} else if method == Method::TRACE {
path.trace = Some(operation_schema);
} else {
return Err(anyhow::anyhow!(
"Unsupported method {method} (at {path_str})"
));
}
Ok(())
}
#[test]
fn ensure_builder_deterministic() {
use okapi::openapi3::Operation;
let mut built_specs = Vec::new();
for _ in 0..100 {
let mut builder = OpenApiBuilder::new("title", "version");
for i in 0..2 {
builder.operation(format!("/path/{}", i), Method::GET, |_| {
Ok(Operation::default())
});
}
let spec = builder
.build()
.map(|x| format!("{:?}", x))
.expect("Failed to build spec");
built_specs.push(spec);
}
for i in 1..built_specs.len() {
assert_eq!(built_specs[i - 1], built_specs[i]);
}
}