use std::fmt;
use std::str::FromStr;
use serde::Serialize;
use thiserror::Error;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub struct Contact {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
}
impl Contact {
#[must_use]
pub const fn new() -> Self {
Self {
name: None,
url: None,
email: None,
}
}
#[must_use]
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
self
}
#[must_use]
pub fn email(mut self, email: impl Into<String>) -> Self {
self.email = Some(email.into());
self
}
#[cfg(feature = "asyncapi")]
#[must_use]
pub(crate) const fn is_empty(&self) -> bool {
self.name.is_none() && self.url.is_none() && self.email.is_none()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub struct License {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
impl License {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
url: None,
}
}
#[must_use]
pub fn url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub struct ExternalDocs {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl ExternalDocs {
#[must_use]
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
description: None,
}
}
#[must_use]
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub struct Tag {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "externalDocs", skip_serializing_if = "Option::is_none")]
pub external_docs: Option<ExternalDocs>,
}
impl Tag {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
description: None,
external_docs: None,
}
}
#[must_use]
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn external_docs(mut self, docs: ExternalDocs) -> Self {
self.external_docs = Some(docs);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[serde(transparent)]
pub struct AppId(String);
impl AppId {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for AppId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for AppId {
type Err = AppIdError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let (scheme, rest) = value.split_once(':').ok_or(AppIdError::NoScheme)?;
let mut characters = scheme.chars();
let first_is_letter = characters.next().is_some_and(|c| c.is_ascii_alphabetic());
let tail_is_legal =
characters.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'));
if !first_is_letter || !tail_is_legal {
return Err(AppIdError::MalformedScheme);
}
let _ = rest;
Ok(Self(value.to_owned()))
}
}
impl TryFrom<&str> for AppId {
type Error = AppIdError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
impl TryFrom<String> for AppId {
type Error = AppIdError;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.as_str().parse()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum AppIdError {
#[error("an application id is a URI and needs a scheme, as in `urn:example:orders`")]
NoScheme,
#[error(
"a URI scheme starts with a letter and continues with letters, digits, `+`, `-` or `.`"
)]
MalformedScheme,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_uri_scheme_is_what_separates_an_id_from_a_title() {
assert!("urn:example:orders".parse::<AppId>().is_ok());
assert!("https://example.com/orders".parse::<AppId>().is_ok());
assert!("a+b-c.d:rest".parse::<AppId>().is_ok());
assert_eq!("orders".parse::<AppId>(), Err(AppIdError::NoScheme));
assert_eq!("1http:x".parse::<AppId>(), Err(AppIdError::MalformedScheme));
assert_eq!(":x".parse::<AppId>(), Err(AppIdError::MalformedScheme));
assert_eq!("ht tp:x".parse::<AppId>(), Err(AppIdError::MalformedScheme));
}
#[cfg(feature = "asyncapi")]
#[test]
fn an_empty_contact_is_left_out_of_the_document() {
assert!(Contact::new().is_empty());
assert!(!Contact::new().name("Ops").is_empty());
}
}