use std::{any::type_name, marker::PhantomData};
use crate::{
generate::GenContext,
openapi::{
Components, Contact, Info, License, OpenApi, Operation, Parameter,
ParameterSchemaOrContent, PathItem, ReferenceOr, Response, SecurityScheme, Server,
StatusCode, Tag,
},
OperationInput,
};
use indexmap::IndexMap;
use serde::Serialize;
use serde_json::Value;
use crate::{
error::Error, generate::in_context, operation::OperationOutput, util::iter_operations_mut,
};
#[must_use]
pub struct TransformOpenApi<'t> {
pub(crate) api: &'t mut OpenApi,
}
impl<'t> TransformOpenApi<'t> {
pub fn new(api: &'t mut OpenApi) -> Self {
Self { api }
}
#[tracing::instrument(skip_all)]
pub fn title(self, title: &str) -> Self {
self.api.info.title = title.into();
self
}
#[tracing::instrument(skip_all)]
pub fn summary(self, summary: &str) -> Self {
self.api.info.summary = Some(summary.into());
self
}
#[tracing::instrument(skip_all)]
pub fn tos(self, tos: &str) -> Self {
self.api.info.terms_of_service = Some(tos.into());
self
}
#[tracing::instrument(skip_all)]
pub fn description(self, description: &str) -> Self {
self.api.info.description = Some(description.into());
self
}
#[tracing::instrument(skip_all)]
pub fn version(self, version: &str) -> Self {
self.api.info.version = version.into();
self
}
#[tracing::instrument(skip_all)]
pub fn contact(self, contact: Contact) -> Self {
self.api.info.contact = Some(contact);
self
}
#[tracing::instrument(skip_all)]
pub fn license(self, license: License) -> Self {
self.api.info.license = Some(license);
self
}
#[tracing::instrument(skip_all)]
pub fn info(self, info: Info) -> Self {
self.api.info = info;
self
}
#[tracing::instrument(skip_all)]
pub fn tag(self, tag: Tag) -> Self {
self.api.tags.push(tag);
self
}
#[tracing::instrument(skip_all)]
pub fn server(self, server: Server) -> Self {
self.api.servers.push(server);
self
}
#[tracing::instrument(skip_all)]
pub fn default_response<R>(self) -> Self
where
R: OperationOutput,
{
if let Some(p) = &mut self.api.paths {
for (_, p) in &mut p.paths {
let p = match p {
ReferenceOr::Reference { .. } => continue,
ReferenceOr::Item(p) => p,
};
let _ = TransformPathItem::new(p).default_response::<R>();
}
}
self
}
#[tracing::instrument(skip_all)]
pub fn default_response_with<R, F>(self, transform: F) -> Self
where
R: OperationOutput,
F: Fn(TransformResponse<'_, R::Inner>) -> TransformResponse<'_, R::Inner> + Clone,
{
if let Some(p) = &mut self.api.paths {
for (_, p) in &mut p.paths {
let p = match p {
ReferenceOr::Reference { .. } => continue,
ReferenceOr::Item(p) => p,
};
for (_, op) in iter_operations_mut(p) {
let _ = TransformOperation::new(op)
.default_response_with::<R, F>(transform.clone());
}
}
}
self
}
#[tracing::instrument(skip_all)]
pub fn strip_null_from_query_params(self) -> Self {
strip_null_from_query_params_impl(self.api);
self
}
#[allow(clippy::missing_panics_doc)]
pub fn security_scheme(mut self, name: &str, scheme: SecurityScheme) -> Self {
let components = match &mut self.inner_mut().components {
Some(c) => c,
None => {
self.inner_mut().components = Some(Components::default());
self.inner_mut().components.as_mut().unwrap()
}
};
components
.security_schemes
.insert(name.into(), ReferenceOr::Item(scheme));
self
}
#[tracing::instrument(skip_all)]
pub fn security_requirement(self, security_scheme: &str) -> Self {
self.security_requirement_multi([security_scheme])
}
#[tracing::instrument(skip_all)]
pub fn security_requirement_multi<'a, I>(mut self, security_schemes: I) -> Self
where
I: IntoIterator<Item = &'a str> + Clone,
{
if self.inner_mut().security.iter().any(|s| {
s.len() == security_schemes.clone().into_iter().count()
&& security_schemes
.clone()
.into_iter()
.all(|security_scheme| s.contains_key(security_scheme))
}) {
return self;
}
self.inner_mut().security.push(
security_schemes
.into_iter()
.map(|security_scheme| (security_scheme.to_string(), Vec::new()))
.collect(),
);
self
}
#[tracing::instrument(skip_all)]
#[allow(clippy::missing_panics_doc)]
pub fn security_requirement_scopes<I, S>(self, security_scheme: &str, scopes: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.security_requirement_multi_scopes([security_scheme], scopes)
}
#[tracing::instrument(skip_all)]
#[allow(clippy::missing_panics_doc)]
pub fn security_requirement_multi_scopes<'a, I, IS, S>(
mut self,
security_schemes: I,
scopes: IS,
) -> Self
where
I: IntoIterator<Item = &'a str> + Clone,
IS: IntoIterator<Item = S>,
S: Into<String>,
{
match self.inner_mut().security.iter_mut().find(|s| {
s.len() == security_schemes.clone().into_iter().count()
&& security_schemes
.clone()
.into_iter()
.all(|security_scheme| s.contains_key(security_scheme))
}) {
Some(s) => {
let scopes: Vec<String> = scopes.into_iter().map(Into::into).collect();
s.iter_mut().for_each(|(_, s)| s.extend(scopes.clone()));
}
None => {
let scopes: Vec<String> = scopes.into_iter().map(Into::into).collect();
self.inner_mut().security.push(
security_schemes
.into_iter()
.map(|security_scheme| (security_scheme.to_string(), scopes.clone()))
.collect(),
);
}
}
self
}
pub fn with(self, transform: impl FnOnce(Self) -> Self) -> Self {
transform(self)
}
#[inline]
pub fn inner_mut(&mut self) -> &mut OpenApi {
self.api
}
}
#[must_use]
pub struct TransformPathItem<'t> {
pub(crate) hidden: bool,
pub(crate) path: &'t mut PathItem,
}
impl<'t> TransformPathItem<'t> {
pub fn new(path: &'t mut PathItem) -> Self {
Self {
hidden: false,
path,
}
}
#[tracing::instrument(skip_all)]
pub fn hidden(mut self, hidden: bool) -> Self {
self.hidden = hidden;
self
}
#[tracing::instrument(skip_all)]
pub fn summary(self, desc: &str) -> Self {
self.path.summary = Some(desc.into());
self
}
#[tracing::instrument(skip_all)]
pub fn description(self, desc: &str) -> Self {
self.path.description = Some(desc.into());
self
}
#[tracing::instrument(skip_all)]
pub fn tag(self, tag: &str) -> Self {
for (_, op) in iter_operations_mut(self.path) {
if !op.tags.iter().any(|t| t == tag) {
op.tags.push(tag.into());
}
}
self
}
#[tracing::instrument(skip_all)]
pub fn default_response<R>(self) -> Self
where
R: OperationOutput,
{
in_context(|ctx| ctx.show_error = filter_no_duplicate_response);
for (_, op) in iter_operations_mut(self.path) {
let _ = TransformOperation::new(op).default_response::<R>();
}
in_context(GenContext::reset_error_filter);
self
}
#[tracing::instrument(skip_all)]
pub fn default_response_with<R, F>(self, transform: F) -> Self
where
R: OperationOutput,
F: Fn(TransformResponse<'_, R::Inner>) -> TransformResponse<'_, R::Inner> + Clone,
{
in_context(|ctx| ctx.show_error = filter_no_duplicate_response);
for (_, op) in iter_operations_mut(self.path) {
let _ = TransformOperation::new(op).default_response_with::<R, F>(transform.clone());
}
in_context(GenContext::reset_error_filter);
self
}
#[tracing::instrument(skip_all)]
pub fn security_requirement(self, security_scheme: &str) -> Self {
self.security_requirement_multi([security_scheme])
}
#[tracing::instrument(skip_all)]
pub fn security_requirement_multi<'a, I>(self, security_schemes: I) -> Self
where
I: IntoIterator<Item = &'a str> + Clone,
{
for (_, op) in iter_operations_mut(self.path) {
let _ =
TransformOperation::new(op).security_requirement_multi(security_schemes.clone());
}
self
}
#[tracing::instrument(skip_all)]
#[allow(clippy::missing_panics_doc)]
pub fn security_requirement_scopes<I, S>(self, security_scheme: &str, scopes: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.security_requirement_multi_scopes([security_scheme], scopes)
}
#[tracing::instrument(skip_all)]
#[allow(clippy::missing_panics_doc)]
pub fn security_requirement_multi_scopes<'a, I, IS, S>(
self,
security_schemes: I,
scopes: IS,
) -> Self
where
I: IntoIterator<Item = &'a str> + Clone,
IS: IntoIterator<Item = S>,
S: Into<String>,
{
let scopes: Vec<String> = scopes.into_iter().map(Into::into).collect();
for (_, op) in iter_operations_mut(self.path) {
let _ = TransformOperation::new(op)
.security_requirement_multi_scopes(security_schemes.clone(), scopes.clone());
}
self
}
pub fn with(self, transform: impl FnOnce(Self) -> Self) -> Self {
transform(self)
}
#[inline]
pub fn inner_mut(&mut self) -> &mut PathItem {
self.path
}
}
#[must_use]
pub struct TransformOperation<'t> {
pub(crate) hidden: bool,
pub(crate) operation: &'t mut Operation,
}
impl<'t> TransformOperation<'t> {
pub fn new(operation: &'t mut Operation) -> Self {
Self {
hidden: false,
operation,
}
}
#[tracing::instrument(skip_all, fields(operation_id = self.operation.operation_id))]
pub fn id(self, name: &str) -> Self {
self.operation.operation_id = Some(name.into());
self
}
#[tracing::instrument(skip_all, fields(operation_id = self.operation.operation_id))]
pub fn summary(self, desc: &str) -> Self {
self.operation.summary = Some(desc.into());
self
}
#[tracing::instrument(skip_all, fields(operation_id = self.operation.operation_id))]
pub fn description(self, desc: &str) -> Self {
self.operation.description = Some(desc.into());
self
}
#[tracing::instrument(skip_all)]
pub fn tag(self, tag: &str) -> Self {
if !self.operation.tags.iter().any(|t| t == tag) {
self.operation.tags.push(tag.into());
}
self
}
#[tracing::instrument(skip_all, fields(operation_id = self.operation.operation_id))]
pub fn hidden(mut self, hidden: bool) -> Self {
self.hidden = hidden;
self
}
#[tracing::instrument(skip_all, fields(operation_id = self.operation.operation_id))]
pub fn input<T: OperationInput>(self) -> Self {
in_context(|ctx| {
T::operation_input(ctx, self.operation);
});
self
}
#[tracing::instrument(skip_all, fields(operation_id = self.operation.operation_id))]
pub fn parameter<T, F>(self, name: &str, transform: F) -> Self
where
T: Serialize,
F: FnOnce(TransformParameter<'_, T>) -> TransformParameter<'_, T>,
{
let (idx, param) = match self
.operation
.parameters
.iter_mut()
.enumerate()
.find(|(_, p)| match p {
ReferenceOr::Item(p) => p.parameter_data_ref().name == name,
ReferenceOr::Reference { .. } => false,
}) {
Some((idx, p)) => match p {
ReferenceOr::Item(p) => (idx, p),
ReferenceOr::Reference { .. } => {
in_context(|ctx| {
ctx.error(Error::UnexpectedReference);
});
return self;
}
},
None => {
in_context(|ctx| {
ctx.error(Error::ParameterNotExists(name.to_string()));
});
return self;
}
};
let t = transform(TransformParameter::new(param));
if t.hidden {
self.operation.parameters.remove(idx);
}
self
}
#[tracing::instrument(skip_all, fields(operation_id = self.operation.operation_id))]
pub fn parameter_untyped<F>(self, name: &str, transform: F) -> Self
where
F: FnOnce(TransformParameter<'_, ()>) -> TransformParameter<'_, ()>,
{
self.parameter(name, transform)
}
#[tracing::instrument(skip_all, fields(operation_id = self.operation.operation_id))]
#[allow(clippy::missing_panics_doc)]
pub fn default_response<R>(self) -> Self
where
R: OperationOutput,
{
if self.operation.responses.is_none() {
self.operation.responses = Some(Default::default());
}
in_context(|ctx| {
if let Some(res) = R::operation_response(ctx, self.operation) {
let responses = self.operation.responses.as_mut().unwrap();
if responses.default.is_none() {
responses.default = Some(ReferenceOr::Item(res));
} else {
ctx.error(Error::DefaultResponseExists);
}
} else {
tracing::debug!(type_name = type_name::<R>(), "no response info of type");
}
});
self
}
#[tracing::instrument(skip_all, fields(operation_id = self.operation.operation_id))]
#[allow(clippy::missing_panics_doc)]
pub fn default_response_with<R, F>(self, transform: F) -> Self
where
R: OperationOutput,
F: FnOnce(TransformResponse<'_, R::Inner>) -> TransformResponse<'_, R::Inner>,
{
in_context(|ctx| {
if let Some(mut res) = R::operation_response(ctx, self.operation) {
let responses = self
.operation
.responses
.get_or_insert_with(Default::default);
if responses.default.is_none() {
let t = transform(TransformResponse::new(&mut res));
if !t.hidden {
responses.default = Some(ReferenceOr::Item(res));
}
} else {
ctx.error(Error::DefaultResponseExists);
}
} else {
tracing::debug!(type_name = type_name::<R>(), "no response info of type");
}
});
self
}
#[tracing::instrument(skip_all, fields(operation_id = self.operation.operation_id))]
#[allow(clippy::missing_panics_doc)]
pub fn response<const N: u16, R>(self) -> Self
where
R: OperationOutput,
{
if self.operation.responses.is_none() {
self.operation.responses = Some(Default::default());
}
in_context(|ctx| {
if let Some(res) = R::operation_response(ctx, self.operation) {
let responses = self.operation.responses.as_mut().unwrap();
if responses
.responses
.insert(StatusCode::Code(N), ReferenceOr::Item(res))
.is_some()
{
ctx.error(Error::ResponseExists(StatusCode::Code(N)));
}
} else {
tracing::debug!(type_name = type_name::<R>(), "no response info of type");
}
});
self
}
#[tracing::instrument(skip_all, fields(operation_id = self.operation.operation_id))]
#[allow(clippy::missing_panics_doc)]
pub fn response_with<const N: u16, R, F>(self, transform: F) -> Self
where
R: OperationOutput,
F: FnOnce(TransformResponse<'_, R::Inner>) -> TransformResponse<'_, R::Inner>,
{
if self.operation.responses.is_none() {
self.operation.responses = Some(Default::default());
}
in_context(|ctx| {
if let Some(mut res) = R::operation_response(ctx, self.operation) {
let t = transform(TransformResponse::new(&mut res));
let responses = self.operation.responses.as_mut().unwrap();
if !t.hidden {
let existing = responses
.responses
.insert(StatusCode::Code(N), ReferenceOr::Item(res))
.is_some();
if existing {
ctx.error(Error::ResponseExists(StatusCode::Code(N)));
}
}
} else {
tracing::debug!(type_name = type_name::<R>(), "no response info of type");
}
});
self
}
#[tracing::instrument(skip_all, fields(operation_id = self.operation.operation_id))]
#[allow(clippy::missing_panics_doc)]
pub fn response_range<const N: u16, R>(self) -> Self
where
R: OperationOutput,
{
if self.operation.responses.is_none() {
self.operation.responses = Some(Default::default());
}
in_context(|ctx| {
if let Some(res) = R::operation_response(ctx, self.operation) {
let responses = self.operation.responses.as_mut().unwrap();
if responses
.responses
.insert(StatusCode::Range(N), ReferenceOr::Item(res))
.is_some()
{
ctx.error(Error::ResponseExists(StatusCode::Range(N)));
}
} else {
tracing::debug!(type_name = type_name::<R>(), "no response info of type");
}
});
self
}
#[tracing::instrument(skip_all, fields(operation_id = self.operation.operation_id))]
#[allow(clippy::missing_panics_doc)]
pub fn response_range_with<const N: u16, R, F>(self, transform: F) -> Self
where
R: OperationOutput,
F: FnOnce(TransformResponse<'_, R::Inner>) -> TransformResponse<'_, R::Inner>,
{
if self.operation.responses.is_none() {
self.operation.responses = Some(Default::default());
}
in_context(|ctx| {
if let Some(mut res) = R::operation_response(ctx, self.operation) {
let t = transform(TransformResponse::new(&mut res));
let responses = self.operation.responses.as_mut().unwrap();
if !t.hidden {
let existing = responses
.responses
.insert(StatusCode::Range(N), ReferenceOr::Item(res))
.is_some();
if existing {
ctx.error(Error::ResponseExists(StatusCode::Range(N)));
}
}
} else {
tracing::debug!(type_name = type_name::<R>(), "no response info of type");
}
});
self
}
#[tracing::instrument(skip_all, fields(operation_id = self.operation.operation_id))]
#[allow(clippy::missing_panics_doc)]
pub fn callback(
self,
callback_name: &str,
callback_url: &str,
callback_transform: impl FnOnce(TransformCallback<'_>) -> TransformCallback<'_>,
) -> Self {
let callbacks = self
.operation
.callbacks
.entry(callback_name.to_string())
.or_insert_with(|| ReferenceOr::Item(IndexMap::default()));
let callbacks = match callbacks {
ReferenceOr::Reference { .. } => {
in_context(|ctx| ctx.error(Error::UnexpectedReference));
return self;
}
ReferenceOr::Item(cbs) => cbs,
};
let p = callbacks
.entry(callback_url.to_string())
.or_insert_with(|| ReferenceOr::Item(PathItem::default()));
let p = match p {
ReferenceOr::Reference { .. } => {
in_context(|ctx| ctx.error(Error::UnexpectedReference));
return self;
}
ReferenceOr::Item(p) => p,
};
let t = callback_transform(TransformCallback::new(p));
if t.hidden {
callbacks.swap_remove(callback_url);
if self
.operation
.callbacks
.get(callback_name)
.unwrap()
.as_item()
.unwrap()
.is_empty()
{
self.operation.callbacks.swap_remove(callback_name);
}
}
self
}
#[tracing::instrument(skip_all, fields(operation_id = self.operation.operation_id))]
pub fn security_requirement(self, security_scheme: &str) -> Self {
self.security_requirement_multi([security_scheme])
}
#[tracing::instrument(skip_all, fields(operation_id = self.operation.operation_id))]
pub fn security_requirement_multi<'a, I>(self, security_schemes: I) -> Self
where
I: IntoIterator<Item = &'a str> + Clone,
{
if self.operation.security.iter().any(|s| {
s.len() == security_schemes.clone().into_iter().count()
&& security_schemes
.clone()
.into_iter()
.all(|security_scheme| s.contains_key(security_scheme))
}) {
return self;
}
self.operation.security.push(
security_schemes
.into_iter()
.map(|security_scheme| (security_scheme.to_string(), Vec::new()))
.collect(),
);
self
}
#[tracing::instrument(skip_all, fields(operation_id = self.operation.operation_id))]
#[allow(clippy::missing_panics_doc)]
pub fn security_requirement_scopes<I, S>(self, security_scheme: &str, scopes: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.security_requirement_multi_scopes([security_scheme], scopes)
}
#[tracing::instrument(skip_all, fields(operation_id = self.operation.operation_id))]
#[allow(clippy::missing_panics_doc)]
pub fn security_requirement_multi_scopes<'a, I, IS, S>(
self,
security_schemes: I,
scopes: IS,
) -> Self
where
I: IntoIterator<Item = &'a str> + Clone,
IS: IntoIterator<Item = S>,
S: Into<String>,
{
match self.operation.security.iter_mut().find(|s| {
s.len() == security_schemes.clone().into_iter().count()
&& security_schemes
.clone()
.into_iter()
.all(|security_scheme| s.contains_key(security_scheme))
}) {
Some(s) => {
let scopes: Vec<String> = scopes.into_iter().map(Into::into).collect();
s.iter_mut().for_each(|(_, s)| s.extend(scopes.clone()));
}
None => {
let scopes: Vec<String> = scopes.into_iter().map(Into::into).collect();
self.operation.security.push(
security_schemes
.into_iter()
.map(|security_scheme| (security_scheme.to_string(), scopes.clone()))
.collect(),
);
}
}
self
}
pub fn with(self, transform: impl FnOnce(Self) -> Self) -> Self {
transform(self)
}
#[inline]
pub fn inner_mut(&mut self) -> &mut Operation {
self.operation
}
}
#[must_use]
pub struct TransformParameter<'t, T> {
pub(crate) hidden: bool,
pub(crate) param: &'t mut Parameter,
_t: PhantomData<T>,
}
impl<'t, T> TransformParameter<'t, T> {
pub fn new(param: &'t mut Parameter) -> Self {
Self {
hidden: false,
param,
_t: PhantomData,
}
}
#[tracing::instrument(skip_all)]
pub fn hidden(mut self, hidden: bool) -> Self {
self.hidden = hidden;
self
}
#[tracing::instrument(skip_all)]
pub fn description(mut self, desc: &str) -> Self {
let data = match &mut self.param {
Parameter::Query { parameter_data, .. }
| Parameter::Header { parameter_data, .. }
| Parameter::Path { parameter_data, .. }
| Parameter::Cookie { parameter_data, .. } => parameter_data,
};
data.description = Some(desc.into());
self
}
pub fn with(self, transform: impl FnOnce(Self) -> Self) -> Self {
transform(self)
}
#[inline]
pub fn inner_mut(&mut self) -> &mut Parameter {
self.param
}
}
#[must_use]
pub struct TransformResponse<'t, T> {
pub(crate) hidden: bool,
pub(crate) response: &'t mut Response,
_t: PhantomData<T>,
}
impl<'t, T> TransformResponse<'t, T> {
pub fn new(response: &'t mut Response) -> Self {
Self {
hidden: false,
response,
_t: PhantomData,
}
}
#[tracing::instrument(skip_all)]
pub fn hidden(mut self, hidden: bool) -> Self {
self.hidden = hidden;
self
}
#[tracing::instrument(skip_all)]
pub fn description(self, desc: &str) -> Self {
self.response.description = desc.into();
self
}
#[tracing::instrument(skip_all)]
#[allow(clippy::missing_panics_doc)]
pub fn example(self, example: impl Into<T>) -> Self
where
T: Serialize,
{
let example = example.into();
for (_, c) in &mut self.response.content {
c.example = Some(serde_json::to_value(&example).unwrap());
}
self
}
pub fn with(self, transform: impl FnOnce(Self) -> Self) -> Self {
transform(self)
}
pub fn inner(&mut self) -> &mut Response {
self.response
}
}
#[must_use]
pub struct TransformCallback<'t> {
hidden: bool,
path: &'t mut PathItem,
}
impl<'t> TransformCallback<'t> {
pub fn new(path: &'t mut PathItem) -> Self {
Self {
hidden: false,
path,
}
}
pub fn hidden(mut self, hidden: bool) -> Self {
self.hidden = hidden;
self
}
#[allow(clippy::missing_panics_doc)]
pub fn delete(
self,
operation: impl FnOnce(TransformOperation<'_>) -> TransformOperation<'_>,
) -> Self {
let op = match &mut self.path.delete {
Some(op) => op,
None => {
self.path.delete = Some(Operation::default());
self.path.delete.as_mut().unwrap()
}
};
let t = operation(TransformOperation::new(op));
if t.hidden {
self.path.delete = None;
}
self
}
#[allow(clippy::missing_panics_doc)]
pub fn get(
self,
operation: impl FnOnce(TransformOperation<'_>) -> TransformOperation<'_>,
) -> Self {
let op = match &mut self.path.get {
Some(op) => op,
None => {
self.path.get = Some(Operation::default());
self.path.get.as_mut().unwrap()
}
};
let t = operation(TransformOperation::new(op));
if t.hidden {
self.path.get = None;
}
self
}
#[allow(clippy::missing_panics_doc)]
pub fn head(
self,
operation: impl FnOnce(TransformOperation<'_>) -> TransformOperation<'_>,
) -> Self {
let op = match &mut self.path.head {
Some(op) => op,
None => {
self.path.head = Some(Operation::default());
self.path.head.as_mut().unwrap()
}
};
let t = operation(TransformOperation::new(op));
if t.hidden {
self.path.head = None;
}
self
}
#[allow(clippy::missing_panics_doc)]
pub fn options(
self,
operation: impl FnOnce(TransformOperation<'_>) -> TransformOperation<'_>,
) -> Self {
let op = match &mut self.path.options {
Some(op) => op,
None => {
self.path.options = Some(Operation::default());
self.path.options.as_mut().unwrap()
}
};
let t = operation(TransformOperation::new(op));
if t.hidden {
self.path.options = None;
}
self
}
#[allow(clippy::missing_panics_doc)]
pub fn patch(
self,
operation: impl FnOnce(TransformOperation<'_>) -> TransformOperation<'_>,
) -> Self {
let op = match &mut self.path.patch {
Some(op) => op,
None => {
self.path.patch = Some(Operation::default());
self.path.patch.as_mut().unwrap()
}
};
let t = operation(TransformOperation::new(op));
if t.hidden {
self.path.patch = None;
}
self
}
#[allow(clippy::missing_panics_doc)]
pub fn post(
self,
operation: impl FnOnce(TransformOperation<'_>) -> TransformOperation<'_>,
) -> Self {
let op = match &mut self.path.post {
Some(op) => op,
None => {
self.path.post = Some(Operation::default());
self.path.post.as_mut().unwrap()
}
};
let t = operation(TransformOperation::new(op));
if t.hidden {
self.path.post = None;
}
self
}
#[allow(clippy::missing_panics_doc)]
pub fn put(
self,
operation: impl FnOnce(TransformOperation<'_>) -> TransformOperation<'_>,
) -> Self {
let op = match &mut self.path.put {
Some(op) => op,
None => {
self.path.put = Some(Operation::default());
self.path.put.as_mut().unwrap()
}
};
let t = operation(TransformOperation::new(op));
if t.hidden {
self.path.put = None;
}
self
}
#[allow(clippy::missing_panics_doc)]
pub fn trace(
self,
operation: impl FnOnce(TransformOperation<'_>) -> TransformOperation<'_>,
) -> Self {
let op = match &mut self.path.trace {
Some(op) => op,
None => {
self.path.trace = Some(Operation::default());
self.path.trace.as_mut().unwrap()
}
};
let t = operation(TransformOperation::new(op));
if t.hidden {
self.path.trace = None;
}
self
}
pub fn path(
mut self,
transform: impl FnOnce(TransformPathItem<'_>) -> TransformPathItem<'_>,
) -> Self {
let t = transform(TransformPathItem::new(self.path));
if t.hidden {
self.hidden = true;
}
self
}
pub fn with(self, transform: impl FnOnce(Self) -> Self) -> Self {
transform(self)
}
}
fn filter_no_duplicate_response(err: &Error) -> bool {
!matches!(err, Error::DefaultResponseExists | Error::ResponseExists(_))
}
pub(crate) fn strip_null_from_query_params_impl(api: &mut OpenApi) {
let Some(paths) = &mut api.paths else { return };
for (_, path_item) in &mut paths.paths {
let ReferenceOr::Item(path_item) = path_item else {
continue;
};
for (_, op) in iter_operations_mut(path_item) {
for param in &mut op.parameters {
let ReferenceOr::Item(Parameter::Query { parameter_data, .. }) = param else {
continue;
};
let ParameterSchemaOrContent::Schema(schema_obj) = &mut parameter_data.format
else {
continue;
};
strip_null_from_type(&mut schema_obj.json_schema);
}
}
}
}
fn strip_null_from_type(schema: &mut schemars::Schema) {
if let Some(Value::Array(types)) = schema.get_mut("type") {
let null_count = types.iter().filter(|t| *t == "null").count();
if null_count == 0 || null_count == types.len() {
return; }
types.retain(|t| t != "null");
if types.len() == 1 {
*schema.get_mut("type").unwrap() = types.remove(0);
}
return;
}
let Some(Value::Array(items)) = schema.get_mut("anyOf") else {
return;
};
let is_null = |v: &Value| matches!(v.get("type"), Some(Value::String(s)) if s == "null");
let null_count = items.iter().filter(|item| is_null(item)).count();
if null_count == 0 || null_count == items.len() {
return; }
items.retain(|item| !is_null(item));
if items.len() == 1 {
if let Some(Value::Object(obj)) = items.pop() {
if let Some(schema_obj) = schema.as_object_mut() {
schema_obj.remove("anyOf");
for (key, value) in obj {
schema_obj.insert(key, value);
}
}
}
}
}
#[cfg(test)]
mod tests {
use crate::openapi::{
MediaType, OpenApi, Operation, Parameter, ParameterData, ParameterSchemaOrContent, Paths,
ReferenceOr, RequestBody, SchemaObject,
};
use indexmap::IndexMap;
use schemars::JsonSchema;
use serde_json::json;
use super::TransformOpenApi;
fn inline_schema_for<T: JsonSchema>() -> schemars::Schema {
let settings = schemars::generate::SchemaSettings::draft07().with(|s| {
s.inline_subschemas = true;
});
let mut gen = settings.into_generator();
gen.subschema_for::<T>()
}
fn property_schema(struct_schema: &schemars::Schema, name: &str) -> schemars::Schema {
struct_schema
.get("properties")
.and_then(|p| p.get(name))
.unwrap_or_else(|| panic!("property {name:?} not found"))
.clone()
.try_into()
.unwrap()
}
fn build_api(params: Vec<Parameter>, body_schema: Option<schemars::Schema>) -> OpenApi {
let parameters = params.into_iter().map(ReferenceOr::Item).collect();
let request_body = body_schema.map(|schema| {
ReferenceOr::Item(RequestBody {
content: IndexMap::from_iter([(
"application/json".into(),
MediaType {
schema: Some(SchemaObject {
json_schema: schema,
external_docs: None,
example: None,
}),
..Default::default()
},
)]),
..Default::default()
})
});
let op = Operation {
parameters,
request_body,
..Default::default()
};
let path_item = crate::openapi::PathItem {
get: Some(op),
..Default::default()
};
OpenApi {
paths: Some(Paths {
paths: IndexMap::from([("/test".to_string(), ReferenceOr::Item(path_item))]),
extensions: IndexMap::new(),
}),
..OpenApi::default()
}
}
fn query_param(name: &str, schema: schemars::Schema) -> Parameter {
Parameter::Query {
parameter_data: ParameterData {
name: name.to_string(),
description: None,
required: false,
deprecated: None,
format: ParameterSchemaOrContent::Schema(SchemaObject {
json_schema: schema,
external_docs: None,
example: None,
}),
example: None,
examples: IndexMap::new(),
explode: None,
extensions: IndexMap::new(),
},
allow_reserved: false,
style: Default::default(),
allow_empty_value: None,
}
}
fn get_param_schema(api: &OpenApi, param_index: usize) -> &schemars::Schema {
let paths = api.paths.as_ref().unwrap();
let ReferenceOr::Item(path_item) = &paths.paths["/test"] else {
panic!("expected item");
};
let op = path_item.get.as_ref().unwrap();
let ReferenceOr::Item(param) = &op.parameters[param_index] else {
panic!("expected parameter item");
};
let ParameterSchemaOrContent::Schema(schema_obj) = ¶m.parameter_data_ref().format
else {
panic!("expected schema");
};
&schema_obj.json_schema
}
fn get_body_schema(api: &OpenApi) -> &schemars::Schema {
let paths = api.paths.as_ref().unwrap();
let ReferenceOr::Item(path_item) = &paths.paths["/test"] else {
panic!("expected item");
};
let op = path_item.get.as_ref().unwrap();
let Some(ReferenceOr::Item(body)) = &op.request_body else {
panic!("expected request body");
};
&body.content["application/json"]
.schema
.as_ref()
.unwrap()
.json_schema
}
#[test]
fn strip_null_from_query_params() {
#[derive(JsonSchema)]
#[allow(dead_code)]
struct QueryParams {
optional: Option<String>,
}
#[derive(JsonSchema)]
#[allow(dead_code)]
struct Body {
optional: Option<String>,
}
let query_field = property_schema(&inline_schema_for::<QueryParams>(), "optional");
let body_field = property_schema(&inline_schema_for::<Body>(), "optional");
assert_eq!(query_field.get("type"), Some(&json!(["string", "null"])));
assert_eq!(body_field.get("type"), Some(&json!(["string", "null"])));
let mut api = build_api(vec![query_param("optional", query_field)], Some(body_field));
let _ = TransformOpenApi::new(&mut api).strip_null_from_query_params();
assert_eq!(
get_param_schema(&api, 0).get("type"),
Some(&json!("string"))
);
assert_eq!(
get_body_schema(&api).get("type"),
Some(&json!(["string", "null"])),
"request body schema should retain its nullable type"
);
}
}