use crate::schema::Schema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenApi {
pub openapi: String,
pub info: Info,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub servers: Vec<Server>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub paths: HashMap<String, PathItem>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub components: Option<Components>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<Tag>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Info {
pub title: String,
pub version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub terms_of_service: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub contact: Option<Contact>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub license: Option<License>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Contact {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct License {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Server {
pub url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PathItem {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub get: Option<Operation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub post: Option<Operation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub put: Option<Operation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub delete: Option<Operation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub patch: Option<Operation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub options: Option<Operation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub head: Option<Operation>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Operation {
#[serde(
rename = "operationId",
default,
skip_serializing_if = "Option::is_none"
)]
pub operation_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub parameters: Vec<Parameter>,
#[serde(
rename = "requestBody",
default,
skip_serializing_if = "Option::is_none"
)]
pub request_body: Option<RequestBody>,
pub responses: HashMap<String, Response>,
#[serde(default, skip_serializing_if = "is_false")]
pub deprecated: bool,
}
fn is_false(b: &bool) -> bool {
!*b
}
fn default_responses() -> HashMap<String, Response> {
let mut responses = HashMap::new();
responses.insert(
"200".to_string(),
Response {
description: "Successful response".to_string(),
content: HashMap::new(),
},
);
responses
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Parameter {
pub name: String,
#[serde(rename = "in")]
pub location: ParameterLocation,
#[serde(default)]
pub required: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schema: Option<Schema>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "is_false")]
pub deprecated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub example: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub examples: HashMap<String, Example>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Example {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub value: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub external_value: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ParamMeta {
pub title: Option<String>,
pub description: Option<String>,
pub deprecated: bool,
pub include_in_schema: bool,
pub example: Option<serde_json::Value>,
pub examples: HashMap<String, Example>,
pub ge: Option<f64>,
pub le: Option<f64>,
pub gt: Option<f64>,
pub lt: Option<f64>,
pub min_length: Option<usize>,
pub max_length: Option<usize>,
pub pattern: Option<String>,
}
impl ParamMeta {
#[must_use]
pub fn new() -> Self {
Self {
include_in_schema: true,
..Default::default()
}
}
#[must_use]
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
#[must_use]
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn deprecated(mut self) -> Self {
self.deprecated = true;
self
}
#[must_use]
pub fn exclude_from_schema(mut self) -> Self {
self.include_in_schema = false;
self
}
#[must_use]
pub fn example(mut self, example: serde_json::Value) -> Self {
self.example = Some(example);
self
}
#[must_use]
pub fn ge(mut self, value: f64) -> Self {
self.ge = Some(value);
self
}
#[must_use]
pub fn le(mut self, value: f64) -> Self {
self.le = Some(value);
self
}
#[must_use]
pub fn gt(mut self, value: f64) -> Self {
self.gt = Some(value);
self
}
#[must_use]
pub fn lt(mut self, value: f64) -> Self {
self.lt = Some(value);
self
}
#[must_use]
pub fn min_length(mut self, len: usize) -> Self {
self.min_length = Some(len);
self
}
#[must_use]
pub fn max_length(mut self, len: usize) -> Self {
self.max_length = Some(len);
self
}
#[must_use]
pub fn pattern(mut self, pattern: impl Into<String>) -> Self {
self.pattern = Some(pattern.into());
self
}
#[must_use]
pub fn to_parameter(
&self,
name: impl Into<String>,
location: ParameterLocation,
required: bool,
schema: Option<Schema>,
) -> Parameter {
Parameter {
name: name.into(),
location,
required,
schema,
title: self.title.clone(),
description: self.description.clone(),
deprecated: self.deprecated,
example: self.example.clone(),
examples: self.examples.clone(),
}
}
}
pub trait HasParamMeta {
fn param_meta() -> ParamMeta {
ParamMeta::new()
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ParameterLocation {
Path,
Query,
Header,
Cookie,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RequestBody {
#[serde(default)]
pub required: bool,
pub content: HashMap<String, MediaType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MediaType {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schema: Option<Schema>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Response {
pub description: String,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub content: HashMap<String, MediaType>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Components {
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub schemas: HashMap<String, Schema>,
}
#[derive(Debug, Default)]
pub struct SchemaRegistry {
schemas: HashMap<String, Schema>,
}
impl SchemaRegistry {
#[must_use]
pub fn new() -> Self {
Self {
schemas: HashMap::new(),
}
}
pub fn register(&mut self, name: impl Into<String>, schema: Schema) -> Schema {
let name = name.into();
self.schemas.entry(name.clone()).or_insert(schema);
Schema::reference(&name)
}
#[must_use]
pub fn into_schemas(self) -> HashMap<String, Schema> {
self.schemas
}
}
pub struct SchemaRegistryMut<'a> {
schemas: &'a mut HashMap<String, Schema>,
}
impl SchemaRegistryMut<'_> {
pub fn register(&mut self, name: impl Into<String>, schema: Schema) -> Schema {
let name = name.into();
self.schemas.entry(name.clone()).or_insert(schema);
Schema::reference(&name)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tag {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[cfg(test)]
mod param_meta_tests {
use super::*;
#[test]
fn new_creates_default_with_include_in_schema_true() {
let meta = ParamMeta::new();
assert!(meta.include_in_schema);
assert!(meta.title.is_none());
assert!(meta.description.is_none());
assert!(!meta.deprecated);
}
#[test]
fn title_sets_title() {
let meta = ParamMeta::new().title("User ID");
assert_eq!(meta.title.as_deref(), Some("User ID"));
}
#[test]
fn description_sets_description() {
let meta = ParamMeta::new().description("The unique identifier");
assert_eq!(meta.description.as_deref(), Some("The unique identifier"));
}
#[test]
fn deprecated_marks_as_deprecated() {
let meta = ParamMeta::new().deprecated();
assert!(meta.deprecated);
}
#[test]
fn exclude_from_schema_sets_include_false() {
let meta = ParamMeta::new().exclude_from_schema();
assert!(!meta.include_in_schema);
}
#[test]
fn example_sets_example_value() {
let meta = ParamMeta::new().example(serde_json::json!(42));
assert_eq!(meta.example, Some(serde_json::json!(42)));
}
#[test]
fn ge_sets_minimum_constraint() {
let meta = ParamMeta::new().ge(1.0);
assert_eq!(meta.ge, Some(1.0));
}
#[test]
fn le_sets_maximum_constraint() {
let meta = ParamMeta::new().le(100.0);
assert_eq!(meta.le, Some(100.0));
}
#[test]
fn gt_sets_exclusive_minimum() {
let meta = ParamMeta::new().gt(0.0);
assert_eq!(meta.gt, Some(0.0));
}
#[test]
fn lt_sets_exclusive_maximum() {
let meta = ParamMeta::new().lt(1000.0);
assert_eq!(meta.lt, Some(1000.0));
}
#[test]
fn min_length_sets_minimum_string_length() {
let meta = ParamMeta::new().min_length(3);
assert_eq!(meta.min_length, Some(3));
}
#[test]
fn max_length_sets_maximum_string_length() {
let meta = ParamMeta::new().max_length(255);
assert_eq!(meta.max_length, Some(255));
}
#[test]
fn pattern_sets_regex_constraint() {
let meta = ParamMeta::new().pattern(r"^\d{4}-\d{2}-\d{2}$");
assert_eq!(meta.pattern.as_deref(), Some(r"^\d{4}-\d{2}-\d{2}$"));
}
#[test]
fn builder_methods_chain() {
let meta = ParamMeta::new()
.title("Page")
.description("Page number for pagination")
.ge(1.0)
.le(1000.0)
.example(serde_json::json!(1));
assert_eq!(meta.title.as_deref(), Some("Page"));
assert_eq!(
meta.description.as_deref(),
Some("Page number for pagination")
);
assert_eq!(meta.ge, Some(1.0));
assert_eq!(meta.le, Some(1000.0));
assert_eq!(meta.example, Some(serde_json::json!(1)));
}
#[test]
fn to_parameter_creates_parameter_with_metadata() {
let meta = ParamMeta::new()
.title("User ID")
.description("Unique user identifier")
.deprecated()
.example(serde_json::json!(42));
let param = meta.to_parameter("user_id", ParameterLocation::Path, true, None);
assert_eq!(param.name, "user_id");
assert!(matches!(param.location, ParameterLocation::Path));
assert!(param.required);
assert_eq!(param.title.as_deref(), Some("User ID"));
assert_eq!(param.description.as_deref(), Some("Unique user identifier"));
assert!(param.deprecated);
assert_eq!(param.example, Some(serde_json::json!(42)));
}
#[test]
fn to_parameter_with_query_location() {
let meta = ParamMeta::new().description("Search query");
let param = meta.to_parameter("q", ParameterLocation::Query, false, None);
assert_eq!(param.name, "q");
assert!(matches!(param.location, ParameterLocation::Query));
assert!(!param.required);
}
#[test]
fn to_parameter_with_header_location() {
let meta = ParamMeta::new().description("API key");
let param = meta.to_parameter("X-API-Key", ParameterLocation::Header, true, None);
assert_eq!(param.name, "X-API-Key");
assert!(matches!(param.location, ParameterLocation::Header));
}
#[test]
fn to_parameter_with_cookie_location() {
let meta = ParamMeta::new().description("Session cookie");
let param = meta.to_parameter("session", ParameterLocation::Cookie, false, None);
assert_eq!(param.name, "session");
assert!(matches!(param.location, ParameterLocation::Cookie));
}
#[test]
fn default_param_meta_is_empty() {
let meta = ParamMeta::default();
assert!(meta.title.is_none());
assert!(meta.description.is_none());
assert!(!meta.deprecated);
assert!(!meta.include_in_schema); assert!(meta.example.is_none());
assert!(meta.ge.is_none());
assert!(meta.le.is_none());
assert!(meta.gt.is_none());
assert!(meta.lt.is_none());
assert!(meta.min_length.is_none());
assert!(meta.max_length.is_none());
assert!(meta.pattern.is_none());
}
#[test]
fn string_constraints_together() {
let meta = ParamMeta::new()
.min_length(1)
.max_length(100)
.pattern(r"^[a-zA-Z]+$");
assert_eq!(meta.min_length, Some(1));
assert_eq!(meta.max_length, Some(100));
assert_eq!(meta.pattern.as_deref(), Some(r"^[a-zA-Z]+$"));
}
#[test]
fn numeric_constraints_together() {
let meta = ParamMeta::new().gt(0.0).lt(100.0).ge(1.0).le(99.0);
assert_eq!(meta.gt, Some(0.0));
assert_eq!(meta.lt, Some(100.0));
assert_eq!(meta.ge, Some(1.0));
assert_eq!(meta.le, Some(99.0));
}
}
#[cfg(test)]
mod serialization_tests {
use super::*;
#[test]
fn parameter_serializes_location_as_in() {
let param = Parameter {
name: "id".to_string(),
location: ParameterLocation::Path,
required: true,
schema: None,
title: None,
description: None,
deprecated: false,
example: None,
examples: HashMap::new(),
};
let json = serde_json::to_string(¶m).unwrap();
assert!(json.contains(r#""in":"path""#));
}
#[test]
fn parameter_location_serializes_lowercase() {
let path_json = serde_json::to_string(&ParameterLocation::Path).unwrap();
assert_eq!(path_json, r#""path""#);
let query_json = serde_json::to_string(&ParameterLocation::Query).unwrap();
assert_eq!(query_json, r#""query""#);
let header_json = serde_json::to_string(&ParameterLocation::Header).unwrap();
assert_eq!(header_json, r#""header""#);
let cookie_json = serde_json::to_string(&ParameterLocation::Cookie).unwrap();
assert_eq!(cookie_json, r#""cookie""#);
}
#[test]
fn parameter_skips_false_deprecated() {
let param = Parameter {
name: "id".to_string(),
location: ParameterLocation::Path,
required: true,
schema: None,
title: None,
description: None,
deprecated: false,
example: None,
examples: HashMap::new(),
};
let json = serde_json::to_string(¶m).unwrap();
assert!(!json.contains("deprecated"));
}
#[test]
fn parameter_includes_true_deprecated() {
let param = Parameter {
name: "old_id".to_string(),
location: ParameterLocation::Path,
required: true,
schema: None,
title: None,
description: Some("Deprecated, use new_id instead".to_string()),
deprecated: true,
example: None,
examples: HashMap::new(),
};
let json = serde_json::to_string(¶m).unwrap();
assert!(json.contains(r#""deprecated":true"#));
}
#[test]
fn openapi_builder_creates_valid_document() {
let doc = OpenApiBuilder::new("Test API", "1.0.0")
.description("A test API")
.server("https://api.example.com", Some("Production".to_string()))
.tag("users", Some("User operations".to_string()))
.build();
assert_eq!(doc.openapi, "3.1.0");
assert_eq!(doc.info.title, "Test API");
assert_eq!(doc.info.version, "1.0.0");
assert_eq!(doc.info.description.as_deref(), Some("A test API"));
assert_eq!(doc.servers.len(), 1);
assert_eq!(doc.servers[0].url, "https://api.example.com");
assert_eq!(doc.tags.len(), 1);
assert_eq!(doc.tags[0].name, "users");
}
#[test]
fn openapi_serializes_to_valid_json() {
let doc = OpenApiBuilder::new("Test API", "1.0.0").build();
let json = serde_json::to_string_pretty(&doc).unwrap();
assert!(json.contains(r#""openapi": "3.1.0""#));
assert!(json.contains(r#""title": "Test API""#));
assert!(json.contains(r#""version": "1.0.0""#));
}
#[test]
fn example_serializes_all_fields() {
let example = Example {
summary: Some("Example summary".to_string()),
description: Some("Example description".to_string()),
value: Some(serde_json::json!({"key": "value"})),
external_value: None,
};
let json = serde_json::to_string(&example).unwrap();
assert!(json.contains(r#""summary":"Example summary""#));
assert!(json.contains(r#""description":"Example description""#));
assert!(json.contains(r#""value""#));
}
}
pub struct OpenApiBuilder {
info: Info,
servers: Vec<Server>,
paths: HashMap<String, PathItem>,
components: Components,
tags: Vec<Tag>,
}
impl OpenApiBuilder {
#[must_use]
pub fn new(title: impl Into<String>, version: impl Into<String>) -> Self {
Self {
info: Info {
title: title.into(),
version: version.into(),
description: None,
terms_of_service: None,
contact: None,
license: None,
},
servers: Vec::new(),
paths: HashMap::new(),
components: Components::default(),
tags: Vec::new(),
}
}
#[must_use]
pub fn description(mut self, description: impl Into<String>) -> Self {
self.info.description = Some(description.into());
self
}
#[must_use]
pub fn server(mut self, url: impl Into<String>, description: Option<String>) -> Self {
self.servers.push(Server {
url: url.into(),
description,
});
self
}
#[must_use]
pub fn tag(mut self, name: impl Into<String>, description: Option<String>) -> Self {
self.tags.push(Tag {
name: name.into(),
description,
});
self
}
#[must_use]
pub fn schema(mut self, name: impl Into<String>, schema: Schema) -> Self {
self.components.schemas.insert(name.into(), schema);
self
}
pub fn registry(&mut self) -> SchemaRegistryMut<'_> {
SchemaRegistryMut {
schemas: &mut self.components.schemas,
}
}
#[allow(clippy::too_many_lines)]
pub fn add_route(&mut self, route: &fastapi_router::Route) {
use fastapi_router::Converter as RouteConverter;
fn param_schema(conv: RouteConverter) -> Schema {
match conv {
RouteConverter::Str | RouteConverter::Path => Schema::string(),
RouteConverter::Int => Schema::integer(Some("int64")),
RouteConverter::Float => Schema::number(Some("double")),
RouteConverter::Uuid => Schema::Primitive(crate::schema::PrimitiveSchema {
schema_type: crate::schema::SchemaType::String,
format: Some("uuid".to_string()),
nullable: false,
}),
}
}
let mut op = Operation {
operation_id: if route.operation_id.is_empty() {
None
} else {
Some(route.operation_id.clone())
},
summary: route.summary.clone(),
description: route.description.clone(),
tags: route.tags.clone(),
deprecated: route.deprecated,
..Default::default()
};
for p in &route.path_params {
let mut examples = HashMap::new();
for (name, value) in &p.examples {
examples.insert(
name.clone(),
Example {
summary: None,
description: None,
value: Some(value.clone()),
external_value: None,
},
);
}
op.parameters.push(Parameter {
name: p.name.clone(),
location: ParameterLocation::Path,
required: true,
schema: Some(param_schema(p.converter)),
title: p.title.clone(),
description: p.description.clone(),
deprecated: p.deprecated,
example: p.example.clone(),
examples,
});
}
if let Some(schema_name) = &route.request_body_schema {
let content_type = route
.request_body_content_type
.clone()
.unwrap_or_else(|| "application/json".to_string());
let mut content = HashMap::new();
content.insert(
content_type,
MediaType {
schema: Some(Schema::reference(schema_name)),
},
);
op.request_body = Some(RequestBody {
required: route.request_body_required,
content,
description: None,
});
}
let mut responses = HashMap::new();
if route.responses.is_empty() {
responses = default_responses();
} else {
for r in &route.responses {
let mut content = HashMap::new();
content.insert(
"application/json".to_string(),
MediaType {
schema: Some(Schema::reference(&r.schema_name)),
},
);
responses.insert(
r.status.to_string(),
Response {
description: r.description.clone(),
content,
},
);
}
}
op.responses = responses;
let path_item = self.paths.entry(route.path.clone()).or_default();
match route.method.as_str() {
"GET" => path_item.get = Some(op),
"POST" => path_item.post = Some(op),
"PUT" => path_item.put = Some(op),
"DELETE" => path_item.delete = Some(op),
"PATCH" => path_item.patch = Some(op),
"OPTIONS" => path_item.options = Some(op),
"HEAD" => path_item.head = Some(op),
_ => {}
}
}
pub fn add_routes<'a, I>(&mut self, routes: I)
where
I: IntoIterator<Item = &'a fastapi_router::Route>,
{
for r in routes {
self.add_route(r);
}
}
#[must_use]
pub fn operation(
mut self,
method: &str,
path: impl Into<String>,
operation: Operation,
) -> Self {
let path = path.into();
let path_item = self.paths.entry(path).or_default();
match method.to_uppercase().as_str() {
"GET" => path_item.get = Some(operation),
"POST" => path_item.post = Some(operation),
"PUT" => path_item.put = Some(operation),
"DELETE" => path_item.delete = Some(operation),
"PATCH" => path_item.patch = Some(operation),
"OPTIONS" => path_item.options = Some(operation),
"HEAD" => path_item.head = Some(operation),
_ => {} }
self
}
#[must_use]
pub fn get(self, path: impl Into<String>, operation_id: impl Into<String>) -> Self {
let operation_id = operation_id.into();
self.operation(
"GET",
path,
Operation {
operation_id: if operation_id.is_empty() {
None
} else {
Some(operation_id)
},
responses: default_responses(),
..Default::default()
},
)
}
#[must_use]
pub fn post(self, path: impl Into<String>, operation_id: impl Into<String>) -> Self {
let operation_id = operation_id.into();
self.operation(
"POST",
path,
Operation {
operation_id: if operation_id.is_empty() {
None
} else {
Some(operation_id)
},
responses: default_responses(),
..Default::default()
},
)
}
#[must_use]
pub fn put(self, path: impl Into<String>, operation_id: impl Into<String>) -> Self {
let operation_id = operation_id.into();
self.operation(
"PUT",
path,
Operation {
operation_id: if operation_id.is_empty() {
None
} else {
Some(operation_id)
},
responses: default_responses(),
..Default::default()
},
)
}
#[must_use]
pub fn delete(self, path: impl Into<String>, operation_id: impl Into<String>) -> Self {
let operation_id = operation_id.into();
self.operation(
"DELETE",
path,
Operation {
operation_id: if operation_id.is_empty() {
None
} else {
Some(operation_id)
},
responses: default_responses(),
..Default::default()
},
)
}
#[must_use]
pub fn patch(self, path: impl Into<String>, operation_id: impl Into<String>) -> Self {
let operation_id = operation_id.into();
self.operation(
"PATCH",
path,
Operation {
operation_id: if operation_id.is_empty() {
None
} else {
Some(operation_id)
},
responses: default_responses(),
..Default::default()
},
)
}
#[must_use]
pub fn build(self) -> OpenApi {
OpenApi {
openapi: "3.1.0".to_string(),
info: self.info,
servers: self.servers,
paths: self.paths,
components: if self.components.schemas.is_empty() {
None
} else {
Some(self.components)
},
tags: self.tags,
}
}
}