use indexmap::IndexMap;
use serde_derive::{Deserialize, Serialize};
use serde_json::Value;
use super::extensions::Extensions;
use super::request_body::RequestBody;
use super::response::{Response, Responses};
use super::security::SecurityRequirement;
use super::{Deprecated, ExternalDocs, RefOr, Schema, Server};
#[non_exhaustive]
#[derive(Serialize, Deserialize, Default, Clone, PartialEq, bon::Builder)]
#[cfg_attr(feature = "debug", derive(Debug))]
#[builder(on(_, into))]
pub struct Paths {
#[serde(flatten)]
#[builder(field)]
pub paths: IndexMap<String, PathItem>,
#[serde(skip_serializing_if = "Option::is_none", flatten)]
pub extensions: Option<Extensions>,
}
impl Paths {
pub fn new() -> Self {
Default::default()
}
pub fn get_path_item<P: AsRef<str>>(&self, path: P) -> Option<&PathItem> {
self.paths.get(path.as_ref())
}
pub fn get_path_operation<P: AsRef<str>>(&self, path: P, http_method: HttpMethod) -> Option<&Operation> {
self.paths.get(path.as_ref()).and_then(|path| match http_method {
HttpMethod::Get => path.get.as_ref(),
HttpMethod::Put => path.put.as_ref(),
HttpMethod::Post => path.post.as_ref(),
HttpMethod::Delete => path.delete.as_ref(),
HttpMethod::Options => path.options.as_ref(),
HttpMethod::Head => path.head.as_ref(),
HttpMethod::Patch => path.patch.as_ref(),
HttpMethod::Trace => path.trace.as_ref(),
})
}
pub fn add_path_operation<P: AsRef<str>, O: Into<Operation>>(
&mut self,
path: P,
http_methods: Vec<HttpMethod>,
operation: O,
) {
let path = path.as_ref();
let operation = operation.into();
if let Some(existing_item) = self.paths.get_mut(path) {
for http_method in http_methods {
match http_method {
HttpMethod::Get => existing_item.get = Some(operation.clone()),
HttpMethod::Put => existing_item.put = Some(operation.clone()),
HttpMethod::Post => existing_item.post = Some(operation.clone()),
HttpMethod::Delete => existing_item.delete = Some(operation.clone()),
HttpMethod::Options => existing_item.options = Some(operation.clone()),
HttpMethod::Head => existing_item.head = Some(operation.clone()),
HttpMethod::Patch => existing_item.patch = Some(operation.clone()),
HttpMethod::Trace => existing_item.trace = Some(operation.clone()),
};
}
} else {
self.paths
.insert(String::from(path), PathItem::from_http_methods(http_methods, operation));
}
}
pub fn merge(&mut self, other_paths: Paths) {
for (path, that) in other_paths.paths {
if let Some(this) = self.paths.get_mut(&path) {
this.merge_operations(that);
} else {
self.paths.insert(path, that);
}
}
if let Some(other_paths_extensions) = other_paths.extensions {
let paths_extensions = self.extensions.get_or_insert(Extensions::default());
paths_extensions.merge(other_paths_extensions);
}
}
}
impl<S: paths_builder::State> PathsBuilder<S> {
pub fn path(mut self, path: impl Into<String>, item: impl Into<PathItem>) -> Self {
let path_string = path.into();
let item = item.into();
if let Some(existing_item) = self.paths.get_mut(&path_string) {
existing_item.merge_operations(item);
} else {
self.paths.insert(path_string, item);
}
self
}
pub fn paths<I: Into<String>, P: Into<PathItem>>(self, items: impl IntoIterator<Item = (I, P)>) -> Self {
items.into_iter().fold(self, |this, (i, p)| this.path(i, p))
}
}
impl<S: paths_builder::IsComplete> From<PathsBuilder<S>> for Paths {
fn from(builder: PathsBuilder<S>) -> Self {
builder.build()
}
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Default, Clone, PartialEq, bon::Builder)]
#[cfg_attr(feature = "debug", derive(Debug))]
#[serde(rename_all = "camelCase")]
#[builder(on(_, into))]
pub struct PathItem {
#[serde(skip_serializing_if = "Option::is_none", default)]
pub summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub servers: Option<Vec<Server>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub parameters: Option<Vec<Parameter>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub get: Option<Operation>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub put: Option<Operation>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub post: Option<Operation>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub delete: Option<Operation>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub options: Option<Operation>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub head: Option<Operation>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub patch: Option<Operation>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub trace: Option<Operation>,
#[serde(skip_serializing_if = "Option::is_none", flatten)]
pub extensions: Option<Extensions>,
}
impl<S: path_item_builder::IsComplete> From<PathItemBuilder<S>> for PathItem {
fn from(builder: PathItemBuilder<S>) -> Self {
builder.build()
}
}
impl PathItem {
pub fn new<O: Into<Operation>>(http_method: HttpMethod, operation: O) -> Self {
let mut path_item = Self::default();
match http_method {
HttpMethod::Get => path_item.get = Some(operation.into()),
HttpMethod::Put => path_item.put = Some(operation.into()),
HttpMethod::Post => path_item.post = Some(operation.into()),
HttpMethod::Delete => path_item.delete = Some(operation.into()),
HttpMethod::Options => path_item.options = Some(operation.into()),
HttpMethod::Head => path_item.head = Some(operation.into()),
HttpMethod::Patch => path_item.patch = Some(operation.into()),
HttpMethod::Trace => path_item.trace = Some(operation.into()),
};
path_item
}
pub fn from_http_methods<I: IntoIterator<Item = HttpMethod>, O: Into<Operation>>(http_methods: I, operation: O) -> Self {
let mut path_item = Self::default();
let operation = operation.into();
for method in http_methods {
match method {
HttpMethod::Get => path_item.get = Some(operation.clone()),
HttpMethod::Put => path_item.put = Some(operation.clone()),
HttpMethod::Post => path_item.post = Some(operation.clone()),
HttpMethod::Delete => path_item.delete = Some(operation.clone()),
HttpMethod::Options => path_item.options = Some(operation.clone()),
HttpMethod::Head => path_item.head = Some(operation.clone()),
HttpMethod::Patch => path_item.patch = Some(operation.clone()),
HttpMethod::Trace => path_item.trace = Some(operation.clone()),
};
}
path_item
}
pub fn merge_operations(&mut self, path_item: PathItem) {
if path_item.get.is_some() && self.get.is_none() {
self.get = path_item.get;
}
if path_item.put.is_some() && self.put.is_none() {
self.put = path_item.put;
}
if path_item.post.is_some() && self.post.is_none() {
self.post = path_item.post;
}
if path_item.delete.is_some() && self.delete.is_none() {
self.delete = path_item.delete;
}
if path_item.options.is_some() && self.options.is_none() {
self.options = path_item.options;
}
if path_item.head.is_some() && self.head.is_none() {
self.head = path_item.head;
}
if path_item.patch.is_some() && self.patch.is_none() {
self.patch = path_item.patch;
}
if path_item.trace.is_some() && self.trace.is_none() {
self.trace = path_item.trace;
}
}
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord, Clone)]
#[serde(rename_all = "lowercase")]
#[cfg_attr(feature = "debug", derive(Debug))]
pub enum HttpMethod {
Get,
Post,
Put,
Delete,
Options,
Head,
Patch,
Trace,
}
impl HttpMethod {
pub const fn as_str(&self) -> &str {
match self {
Self::Get => "get",
Self::Post => "post",
Self::Put => "put",
Self::Delete => "delete",
Self::Options => "options",
Self::Head => "head",
Self::Patch => "patch",
Self::Trace => "trace",
}
}
}
impl std::fmt::Display for HttpMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Default, Clone, PartialEq, bon::Builder)]
#[cfg_attr(feature = "debug", derive(Debug))]
#[serde(rename_all = "camelCase")]
#[builder(on(_, into))]
pub struct Operation {
#[serde(skip_serializing_if = "Option::is_none", default)]
#[builder(field)]
pub tags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
#[builder(field)]
pub parameters: Option<Vec<Parameter>>,
#[builder(field)]
pub responses: Responses,
#[serde(skip_serializing_if = "Option::is_none", default)]
#[builder(field)]
pub servers: Option<Vec<Server>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
#[builder(field)]
pub security: Option<Vec<SecurityRequirement>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub operation_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub external_docs: Option<ExternalDocs>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub request_body: Option<RequestBody>,
#[allow(missing_docs)]
#[serde(skip_serializing_if = "Option::is_none", default)]
pub callbacks: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub deprecated: Option<Deprecated>,
#[serde(skip_serializing_if = "Option::is_none", default, flatten)]
pub extensions: Option<Extensions>,
}
impl<S: operation_builder::IsComplete> From<OperationBuilder<S>> for Operation {
fn from(builder: OperationBuilder<S>) -> Self {
builder.build()
}
}
impl Operation {
pub fn new() -> Self {
Default::default()
}
}
impl<S: operation_builder::State> OperationBuilder<S> {
pub fn tag(mut self, tag: impl Into<String>) -> Self {
self.tags.get_or_insert_default().push(tag.into());
self
}
pub fn tags<T: Into<String>>(self, tags: impl IntoIterator<Item = T>) -> Self {
tags.into_iter().fold(self, |this, t| this.tag(t))
}
pub fn parameters<P: Into<Parameter>>(self, parameters: impl IntoIterator<Item = P>) -> Self {
parameters.into_iter().fold(self, |this, p| this.parameter(p))
}
pub fn parameter(mut self, parameter: impl Into<Parameter>) -> Self {
self.parameters.get_or_insert_default().push(parameter.into());
self
}
pub fn responses<R: Into<RefOr<Response>>, C: Into<String>>(self, responses: impl IntoIterator<Item = (C, R)>) -> Self {
responses.into_iter().fold(self, |this, (c, r)| this.response(c, r))
}
pub fn response(mut self, code: impl Into<String>, response: impl Into<RefOr<Response>>) -> Self {
self.responses.responses.insert(code.into(), response.into());
self
}
pub fn security(mut self, security: impl Into<SecurityRequirement>) -> Self {
self.security.get_or_insert_default().push(security.into());
self
}
pub fn securities<R: Into<SecurityRequirement>>(self, securities: impl IntoIterator<Item = R>) -> Self {
securities.into_iter().fold(self, |this, s| this.security(s))
}
pub fn servers<E: Into<Server>>(self, servers: impl IntoIterator<Item = E>) -> Self {
servers.into_iter().fold(self, |this, e| this.server(e))
}
pub fn server(mut self, server: impl Into<Server>) -> Self {
self.servers.get_or_insert_default().push(server.into());
self
}
}
impl Operation {
pub fn tag(&mut self, tag: impl Into<String>) -> &mut Self {
self.tags.get_or_insert_default().push(tag.into());
self
}
pub fn tags<T: Into<String>>(&mut self, tags: impl IntoIterator<Item = T>) -> &mut Self {
tags.into_iter().fold(self, |this, t| this.tag(t))
}
pub fn parameters<P: Into<Parameter>>(&mut self, parameters: impl IntoIterator<Item = P>) -> &mut Self {
parameters.into_iter().fold(self, |this, p| this.parameter(p))
}
pub fn parameter(&mut self, parameter: impl Into<Parameter>) -> &mut Self {
self.parameters.get_or_insert_default().push(parameter.into());
self
}
pub fn responses<R: Into<RefOr<Response>>, C: Into<String>>(
&mut self,
responses: impl IntoIterator<Item = (C, R)>,
) -> &mut Self {
responses.into_iter().fold(self, |this, (c, r)| this.response(c, r))
}
pub fn response(&mut self, code: impl Into<String>, response: impl Into<RefOr<Response>>) -> &mut Self {
self.responses.responses.insert(code.into(), response.into());
self
}
pub fn security(&mut self, security: impl Into<SecurityRequirement>) -> &mut Self {
self.security.get_or_insert_default().push(security.into());
self
}
pub fn securities<R: Into<SecurityRequirement>>(&mut self, securities: impl IntoIterator<Item = R>) -> &mut Self {
securities.into_iter().fold(self, |this, s| this.security(s))
}
pub fn servers<E: Into<Server>>(&mut self, servers: impl IntoIterator<Item = E>) -> &mut Self {
servers.into_iter().fold(self, |this, e| this.server(e))
}
pub fn server(&mut self, server: impl Into<Server>) -> &mut Self {
self.servers.get_or_insert_default().push(server.into());
self
}
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Default, Clone, PartialEq, bon::Builder)]
#[cfg_attr(feature = "debug", derive(Debug))]
#[serde(rename_all = "camelCase")]
#[builder(on(_, into))]
pub struct Parameter {
pub name: String,
#[serde(rename = "in")]
pub parameter_in: ParameterIn,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub description: Option<String>,
pub required: bool,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub deprecated: Option<Deprecated>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub schema: Option<Schema>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub style: Option<ParameterStyle>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub explode: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub allow_reserved: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none", default)]
example: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none", default, flatten)]
pub extensions: Option<Extensions>,
}
impl Parameter {
pub fn new<S: Into<String>>(name: S) -> Self {
Self {
name: name.into(),
required: true,
..Default::default()
}
}
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "lowercase")]
#[cfg_attr(feature = "debug", derive(Debug))]
#[derive(Default)]
pub enum ParameterIn {
Query,
#[default]
Path,
Header,
Cookie,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "debug", derive(Debug))]
#[serde(rename_all = "camelCase")]
pub enum ParameterStyle {
Matrix,
Label,
Form,
Simple,
SpaceDelimited,
PipeDelimited,
DeepObject,
}
#[cfg(test)]
#[cfg(feature = "debug")]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::{HttpMethod, Operation};
use crate::security::SecurityRequirement;
use crate::server::Server;
use crate::{PathItem, Paths};
#[test]
fn test_path_order() {
let paths_list = Paths::builder()
.path("/todo", PathItem::new(HttpMethod::Get, Operation::new()))
.path("/todo", PathItem::new(HttpMethod::Post, Operation::new()))
.path("/todo/{id}", PathItem::new(HttpMethod::Delete, Operation::new()))
.path("/todo/{id}", PathItem::new(HttpMethod::Get, Operation::new()))
.path("/todo/{id}", PathItem::new(HttpMethod::Put, Operation::new()))
.path("/todo/search", PathItem::new(HttpMethod::Get, Operation::new()))
.build();
let actual_value = paths_list
.paths
.iter()
.flat_map(|(path, path_item)| {
let mut path_methods = Vec::<(&str, &HttpMethod)>::with_capacity(paths_list.paths.len());
if path_item.get.is_some() {
path_methods.push((path, &HttpMethod::Get));
}
if path_item.put.is_some() {
path_methods.push((path, &HttpMethod::Put));
}
if path_item.post.is_some() {
path_methods.push((path, &HttpMethod::Post));
}
if path_item.delete.is_some() {
path_methods.push((path, &HttpMethod::Delete));
}
if path_item.options.is_some() {
path_methods.push((path, &HttpMethod::Options));
}
if path_item.head.is_some() {
path_methods.push((path, &HttpMethod::Head));
}
if path_item.patch.is_some() {
path_methods.push((path, &HttpMethod::Patch));
}
if path_item.trace.is_some() {
path_methods.push((path, &HttpMethod::Trace));
}
path_methods
})
.collect::<Vec<_>>();
let get = HttpMethod::Get;
let post = HttpMethod::Post;
let put = HttpMethod::Put;
let delete = HttpMethod::Delete;
let expected_value = vec![
("/todo", &get),
("/todo", &post),
("/todo/{id}", &get),
("/todo/{id}", &put),
("/todo/{id}", &delete),
("/todo/search", &get),
];
assert_eq!(actual_value, expected_value);
}
#[test]
fn operation_new() {
let operation = Operation::new();
assert!(operation.tags.is_none());
assert!(operation.summary.is_none());
assert!(operation.description.is_none());
assert!(operation.operation_id.is_none());
assert!(operation.external_docs.is_none());
assert!(operation.parameters.is_none());
assert!(operation.request_body.is_none());
assert!(operation.responses.responses.is_empty());
assert!(operation.callbacks.is_none());
assert!(operation.deprecated.is_none());
assert!(operation.security.is_none());
assert!(operation.servers.is_none());
}
#[test]
fn operation_builder_security() {
let security_requirement1 = SecurityRequirement::new("api_oauth2_flow", ["edit:items", "read:items"]);
let security_requirement2 = SecurityRequirement::new("api_oauth2_flow", ["remove:items"]);
let operation = Operation::builder()
.security(security_requirement1)
.security(security_requirement2)
.build();
assert!(operation.security.is_some());
}
#[test]
fn operation_builder_server() {
let server1 = Server::new("/api");
let server2 = Server::new("/admin");
let operation = Operation::builder().server(server1).server(server2).build();
assert!(operation.servers.is_some());
}
}