use std::fmt;
use serde::{Deserialize, Serialize};
use crate::{
Map,
model::{
body::RequestBody,
callback::Callback,
example::Example,
extensions::Extensions,
parameter::{Parameter, header::Header},
paths::item::PathItem,
reference::RefOr,
response::Response,
schema::Schema,
security::SecurityScheme,
},
};
#[cfg(feature = "openapi32")]
use crate::model::body::media_type::MediaType;
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("`{0}` is not a valid component name: expected only `A-Z a-z 0-9 . - _`")]
pub struct InvalidComponentName(pub String);
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct ComponentName(String);
impl ComponentName {
pub fn new(name: impl Into<String>) -> Result<Self, InvalidComponentName> {
let name = name.into();
if name.is_empty() || !name.chars().all(Self::is_valid_char) {
return Err(InvalidComponentName(name));
}
Ok(Self(name))
}
#[must_use]
pub fn is_valid_char(c: char) -> bool {
c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_')
}
#[must_use]
pub fn is_valid(name: &str) -> bool {
!name.is_empty() && name.chars().all(Self::is_valid_char)
}
pub fn sanitized(name: &str) -> Result<Self, InvalidComponentName> {
let mut out = String::with_capacity(name.len());
let mut previous_was_underscore = false;
for c in name.chars() {
if Self::is_valid_char(c) {
out.push(c);
previous_was_underscore = false;
} else if !previous_was_underscore {
out.push('_');
previous_was_underscore = true;
}
}
let trimmed = out.trim_matches('_');
Self::new(if trimmed.is_empty() { &out } else { trimmed })
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for ComponentName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl TryFrom<String> for ComponentName {
type Error = InvalidComponentName;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl From<ComponentName> for String {
fn from(name: ComponentName) -> Self {
name.0
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Components {
#[serde(default, skip_serializing_if = "Map::is_empty")]
pub schemas: Map<Schema>,
#[serde(default, skip_serializing_if = "Map::is_empty")]
pub responses: Map<RefOr<Response>>,
#[serde(default, skip_serializing_if = "Map::is_empty")]
pub parameters: Map<RefOr<Parameter>>,
#[serde(default, skip_serializing_if = "Map::is_empty")]
pub examples: Map<RefOr<Example>>,
#[serde(
rename = "requestBodies",
default,
skip_serializing_if = "Map::is_empty"
)]
pub request_bodies: Map<RefOr<RequestBody>>,
#[serde(default, skip_serializing_if = "Map::is_empty")]
pub headers: Map<RefOr<Header>>,
#[serde(
rename = "securitySchemes",
default,
skip_serializing_if = "Map::is_empty"
)]
pub security_schemes: Map<RefOr<SecurityScheme>>,
#[serde(default, skip_serializing_if = "Map::is_empty")]
pub links: Map<RefOr<crate::model::link::Link>>,
#[serde(default, skip_serializing_if = "Map::is_empty")]
pub callbacks: Map<RefOr<Callback>>,
#[serde(rename = "pathItems", default, skip_serializing_if = "Map::is_empty")]
pub path_items: Map<PathItem>,
#[cfg(feature = "openapi32")]
#[serde(rename = "mediaTypes", default, skip_serializing_if = "Map::is_empty")]
pub media_types: Map<RefOr<MediaType>>,
#[serde(flatten)]
pub extensions: Extensions,
}
impl Components {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert_schema(&mut self, name: &ComponentName, schema: Schema) -> Schema {
self.schemas.insert(name.as_str().to_owned(), schema);
Schema::component(name.as_str())
}
pub fn insert_security_scheme(&mut self, name: &ComponentName, scheme: SecurityScheme) {
self.security_schemes
.insert(name.as_str().to_owned(), RefOr::Item(scheme));
}
#[must_use]
pub fn is_empty(&self) -> bool {
let empty = self.schemas.is_empty()
&& self.responses.is_empty()
&& self.parameters.is_empty()
&& self.examples.is_empty()
&& self.request_bodies.is_empty()
&& self.headers.is_empty()
&& self.security_schemes.is_empty()
&& self.links.is_empty()
&& self.callbacks.is_empty()
&& self.path_items.is_empty()
&& self.extensions.is_empty();
#[cfg(feature = "openapi32")]
let empty = empty && self.media_types.is_empty();
empty
}
}
#[cfg(test)]
mod tests;