use std::borrow::Cow;
use std::collections::{btree_map::Entry, BTreeMap};
use std::fmt::Write;
use either::*;
use serde::ser::{SerializeMap, SerializeSeq, SerializeStruct};
use serde::{Serialize, Serializer};
#[derive(Debug, Clone, Default, Serialize)]
pub struct Opg {
pub openapi: OpenApiVersion,
pub info: Info,
#[serde(
skip_serializing_if = "BTreeMap::is_empty",
serialize_with = "serialize_tags"
)]
pub tags: BTreeMap<String, Tag>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub servers: Vec<Server>,
#[serde(
serialize_with = "serialize_ordered_entries",
skip_serializing_if = "Vec::is_empty"
)]
pub paths: Vec<(Path, PathValue)>,
pub components: Components,
}
#[derive(Debug, Clone, Serialize)]
pub struct OpenApiVersion(String);
impl Default for OpenApiVersion {
fn default() -> Self {
Self(crate::OPENAPI_VERSION.to_owned())
}
}
fn serialize_ordered_entries<S, T1, T2>(
entries: &[(T1, T2)],
serializer: S,
) -> Result<S::Ok, S::Error>
where
T1: Serialize,
T2: Serialize,
S: Serializer,
{
let mut ser = serializer.serialize_map(Some(entries.len()))?;
entries
.iter()
.try_for_each(|(key, value)| ser.serialize_entry(key, value))?;
ser.end()
}
fn serialize_tags<S>(tags: &BTreeMap<String, Tag>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
#[derive(Serialize)]
pub struct OpgTagHelper<'a> {
name: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
description: &'a Option<String>,
}
let mut ser = serializer.serialize_seq(Some(tags.len()))?;
tags.iter().try_for_each(|(name, tag)| {
ser.serialize_element(&OpgTagHelper {
name,
description: &tag.description,
})
})?;
ser.end()
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct Info {
pub title: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub version: String,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct Tag {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Server {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Path(#[serde(serialize_with = "serialize_path_elements")] pub Vec<PathElement>);
fn serialize_path_elements<S>(elements: &[PathElement], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut result = String::new();
for element in elements.iter().map(|element| match element {
PathElement::Path(path) => Either::Left(path),
PathElement::Parameter(param) => Either::Right(format!("{{{}}}", param)),
}) {
write!(&mut result, "/{}", element).unwrap();
}
serializer.serialize_str(&result)
}
#[derive(Debug, Clone)]
pub enum PathElement {
Path(String),
Parameter(String),
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct PathValue {
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(flatten, skip_serializing_if = "BTreeMap::is_empty")]
pub operations: BTreeMap<HttpMethod, Operation>,
#[serde(
skip_serializing_if = "BTreeMap::is_empty",
serialize_with = "serialize_parameters"
)]
pub parameters: BTreeMap<String, OperationParameter>,
}
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum HttpMethod {
GET,
PUT,
POST,
DELETE,
OPTIONS,
HEAD,
PATCH,
TRACE,
}
impl HttpMethod {
fn as_str(&self) -> &'static str {
match self {
HttpMethod::GET => "get",
HttpMethod::PUT => "put",
HttpMethod::POST => "post",
HttpMethod::DELETE => "delete",
HttpMethod::OPTIONS => "options",
HttpMethod::HEAD => "head",
HttpMethod::PATCH => "patch",
HttpMethod::TRACE => "trace",
}
}
}
impl Serialize for HttpMethod {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Operation {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "is_false")]
pub deprecated: bool,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub security: Vec<BTreeMap<String, Vec<String>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_body: Option<RequestBody>,
pub responses: BTreeMap<u16, Response>,
#[serde(
skip_serializing_if = "BTreeMap::is_empty",
serialize_with = "serialize_parameters"
)]
pub parameters: BTreeMap<String, OperationParameter>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub callbacks: BTreeMap<String, CallbackObject>,
}
impl Operation {
pub fn with_summary<T>(&mut self, summary: &T) -> &mut Self
where
T: ToString + ?Sized,
{
self.summary = Some(summary.to_string());
self
}
pub fn with_description<T>(&mut self, description: &T) -> &mut Self
where
T: ToString + ?Sized,
{
self.description = Some(description.to_string());
self
}
pub fn mark_deprecated(&mut self, deprecated: bool) -> &mut Self {
self.deprecated = deprecated;
self
}
pub fn with_request_body(&mut self, body: RequestBody) -> &mut Self {
self.request_body = Some(body);
self
}
}
#[derive(Debug, Clone)]
pub struct RequestBody {
pub description: Option<String>,
pub required: bool,
pub schema: ModelReference,
}
impl Serialize for RequestBody {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
#[derive(Serialize)]
struct RequestBodyHelper<'a> {
#[serde(skip_serializing_if = "is_false")]
required: bool,
description: &'a Option<String>,
content: ResponseContent<'a>,
}
RequestBodyHelper {
required: self.required,
description: &self.description,
content: ResponseContent {
media_type: ResponseMediaType {
schema: &self.schema,
},
},
}
.serialize(serializer)
}
}
#[derive(Debug, Clone)]
pub struct Response {
pub description: String,
pub schema: Option<ModelReference>,
}
impl Serialize for Response {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
#[derive(Serialize)]
struct ResponseHelper<'a> {
description: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
content: Option<ResponseContent<'a>>,
}
ResponseHelper {
description: &self.description,
content: self.schema.as_ref().map(|schema| ResponseContent {
media_type: ResponseMediaType { schema },
}),
}
.serialize(serializer)
}
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_false(value: &bool) -> bool {
!*value
}
#[derive(Serialize)]
struct ResponseMediaType<'a> {
schema: &'a ModelReference,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct CallbackObject {
#[serde(
flatten,
serialize_with = "serialize_ordered_entries",
skip_serializing_if = "Vec::is_empty"
)]
pub paths: Vec<(Path, PathValue)>,
}
#[derive(Serialize)]
struct ResponseContent<'a> {
#[serde(rename = "application/json")]
media_type: ResponseMediaType<'a>,
}
fn serialize_parameters<S>(
parameters: &BTreeMap<String, OperationParameter>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct OperationParameterHelper<'a> {
name: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
description: &'a Option<String>,
#[serde(rename = "in")]
parameter_in: ParameterIn,
#[serde(skip_serializing_if = "is_false")]
required: bool,
#[serde(skip_serializing_if = "is_false")]
deprecated: bool,
#[serde(skip_serializing_if = "Option::is_none")]
schema: &'a Option<ModelReference>,
}
let mut ser = serializer.serialize_seq(Some(parameters.len()))?;
parameters.iter().try_for_each(|(name, operation)| {
ser.serialize_element(&OperationParameterHelper {
name,
description: &operation.description,
parameter_in: operation.parameter_in,
required: operation.required,
deprecated: operation.deprecated,
schema: &operation.schema,
})
})?;
ser.end()
}
#[derive(Debug, Clone)]
pub struct OperationParameter {
pub description: Option<String>,
pub parameter_in: ParameterIn,
pub required: bool,
pub deprecated: bool,
pub schema: Option<ModelReference>,
}
#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ParameterIn {
Query,
Header,
Path,
Cookie,
}
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Components {
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub schemas: BTreeMap<String, Model>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub security_schemes: BTreeMap<String, SecurityScheme>,
}
impl Components {
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn contains_model(&self, name: &str) -> bool {
self.schemas.contains_key(name)
}
pub fn mention_schema<M>(&mut self, inline: bool, params: &ContextParams) -> ModelReference
where
M: OpgModel + ?Sized,
{
let reference = M::select_reference(self, inline, params);
if let ModelReference::Link(link) = &reference {
if !self.schemas.contains_key(link) {
let structure = M::get_schema(self);
self.schemas.insert(link.to_owned(), structure);
}
}
reference
}
pub fn mention_security_scheme<T>(&mut self, name: String, security_scheme: &T) -> String
where
T: Clone,
SecurityScheme: From<T>,
{
if !self.security_schemes.contains_key(&name) {
self.security_schemes
.insert(name.clone(), security_scheme.clone().into());
}
name
}
#[allow(dead_code)]
pub fn verify_schemas(&self) -> Result<(), String> {
let cx = TraverseContext(&self.schemas);
self.schemas
.iter()
.try_for_each(|(_, model)| model.traverse(cx))
.map_err(|first_occurrence| first_occurrence.to_owned())
}
pub fn add_model<N>(&mut self, name: N, model: Model)
where
N: ToString,
{
if let std::collections::btree_map::Entry::Vacant(entry) =
self.schemas.entry(name.to_string())
{
entry.insert(model);
}
}
}
pub trait OpgModel {
fn get_schema(cx: &mut Components) -> Model;
fn type_name() -> Option<Cow<'static, str>>;
fn get_schema_with_params(cx: &mut Components, params: &ContextParams) -> Model {
Self::get_schema(cx).apply_params(params)
}
#[inline]
fn select_reference(
cx: &mut Components,
inline: bool,
params: &ContextParams,
) -> ModelReference {
match Self::type_name() {
Some(link) if !inline => ModelReference::Link(link.into_owned()),
_ => ModelReference::Inline(Self::get_schema(cx).apply_params(params)),
}
}
}
#[derive(Default)]
pub struct ContextParams {
pub description: Option<String>,
pub nullable: Option<bool>,
pub variants: Option<Vec<String>>,
pub format: Option<String>,
pub example: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Model {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(flatten)]
pub data: ModelData,
}
impl Model {
#[inline]
pub fn apply_params(mut self, params: &ContextParams) -> Self {
if let Some(description) = ¶ms.description {
self.description = Some(description.clone());
}
self.data = self.data.apply_params(params);
self
}
pub fn try_merge(&mut self, other: Model) -> Result<(), ModelMergeError> {
match &mut self.data {
ModelData::Single(ModelType {
type_description: ModelTypeDescription::Object(self_object),
..
}) => match other.data {
ModelData::Single(ModelType {
type_description: ModelTypeDescription::Object(other_object),
..
}) => self_object.merge(other_object),
_ => Err(ModelMergeError),
},
_ => Err(ModelMergeError),
}
}
fn traverse<'a>(&'a self, cx: TraverseContext<'a>) -> Result<(), &'a str> {
self.data.traverse(cx)
}
}
#[derive(Debug, Copy, Clone)]
pub struct ModelMergeError;
#[derive(Debug, Clone, Serialize)]
#[serde(untagged, rename_all = "camelCase")]
pub enum ModelData {
Single(ModelType),
OneOf(ModelOneOf),
AllOf(ModelAllOf),
AnyOf(ModelAnyOf),
}
impl ModelData {
#[inline]
pub fn apply_params(self, params: &ContextParams) -> Self {
match self {
ModelData::Single(data) => ModelData::Single(data.apply_params(params)),
data => data, }
}
fn traverse<'a>(&'a self, cx: TraverseContext<'a>) -> Result<(), &'a str> {
match self {
ModelData::Single(single) => single.traverse(cx),
ModelData::OneOf(one_of) => one_of.traverse(cx),
ModelData::AllOf(all_of) => all_of.traverse(cx),
ModelData::AnyOf(any_of) => any_of.traverse(cx),
}
}
}
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelOneOf {
pub one_of: Vec<ModelReference>,
}
impl ModelOneOf {
fn traverse<'a>(&'a self, cx: TraverseContext<'a>) -> Result<(), &'a str> {
self.one_of.iter().try_for_each(|item| item.traverse(cx))
}
}
impl From<ModelOneOf> for ModelData {
fn from(data: ModelOneOf) -> Self {
ModelData::OneOf(data)
}
}
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelAllOf {
pub all_of: Vec<ModelReference>,
}
impl ModelAllOf {
fn traverse<'a>(&'a self, cx: TraverseContext<'a>) -> Result<(), &'a str> {
self.all_of.iter().try_for_each(|item| item.traverse(cx))
}
}
impl From<ModelAllOf> for ModelData {
fn from(data: ModelAllOf) -> Self {
ModelData::AllOf(data)
}
}
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelAnyOf {
pub any_of: Vec<ModelReference>,
}
impl ModelAnyOf {
fn traverse<'a>(&'a self, cx: TraverseContext<'a>) -> Result<(), &'a str> {
self.any_of.iter().try_for_each(|item| item.traverse(cx))
}
}
impl From<ModelAnyOf> for ModelData {
fn from(data: ModelAnyOf) -> Self {
ModelData::AnyOf(data)
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelType {
#[serde(skip_serializing_if = "is_false")]
pub nullable: bool,
#[serde(flatten)]
pub type_description: ModelTypeDescription,
}
impl ModelType {
#[inline]
pub fn apply_params(mut self, params: &ContextParams) -> Self {
if let Some(nullable) = params.nullable {
self.nullable = nullable;
}
self.type_description = self.type_description.apply_params(params);
self
}
fn traverse<'a>(&'a self, cx: TraverseContext<'a>) -> Result<(), &'a str> {
match &self.type_description {
ModelTypeDescription::Array(array) => array.traverse(cx),
ModelTypeDescription::Object(object) => object.traverse(cx),
_ => Ok(()),
}
}
}
impl From<ModelType> for ModelData {
fn from(data: ModelType) -> Self {
ModelData::Single(data)
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ModelTypeDescription {
String(ModelString),
Number(ModelSimple),
Integer(ModelSimple),
Boolean,
Array(ModelArray),
Object(ModelObject),
}
impl ModelTypeDescription {
#[inline]
pub fn apply_params(self, params: &ContextParams) -> Self {
match self {
ModelTypeDescription::String(string) => {
ModelTypeDescription::String(string.apply_params(params))
}
ModelTypeDescription::Number(number) => {
ModelTypeDescription::Number(number.apply_params(params))
}
ModelTypeDescription::Integer(integer) => {
ModelTypeDescription::Integer(integer.apply_params(params))
}
other => other,
}
}
}
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelString {
#[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
pub variants: Option<Vec<String>>,
#[serde(flatten)]
pub data: ModelSimple,
}
impl ModelString {
#[inline]
pub fn apply_params(mut self, params: &ContextParams) -> Self {
if let Some(variants) = ¶ms.variants {
self.variants = Some(variants.clone());
}
self.data = self.data.apply_params(params);
self
}
}
impl From<ModelString> for ModelTypeDescription {
fn from(data: ModelString) -> Self {
ModelTypeDescription::String(data)
}
}
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelSimple {
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<String>,
}
impl ModelSimple {
#[inline]
pub fn apply_params(mut self, params: &ContextParams) -> Self {
if let Some(format) = ¶ms.format {
self.format = Some(format.clone());
}
if let Some(example) = ¶ms.example {
self.example = Some(example.clone());
}
self
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelArray {
pub items: Box<ModelReference>,
}
impl ModelArray {
fn traverse<'a>(&'a self, cx: TraverseContext<'a>) -> Result<(), &'a str> {
self.items.traverse(cx)
}
}
impl From<ModelArray> for ModelTypeDescription {
fn from(data: ModelArray) -> Self {
ModelTypeDescription::Array(data)
}
}
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelObject {
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub properties: BTreeMap<String, ModelReference>,
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_properties: Option<Box<ModelReference>>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub required: Vec<String>,
}
impl ModelObject {
pub fn add_property(
&mut self,
property: String,
property_type: ModelReference,
is_required: bool,
) -> Result<(), ModelMergeError> {
let entry = match self.properties.entry(property.clone()) {
Entry::Vacant(entry) => entry,
_ => return Err(ModelMergeError),
};
entry.insert(property_type);
if is_required {
self.required.push(property);
}
Ok(())
}
pub fn merge(&mut self, another: ModelObject) -> Result<(), ModelMergeError> {
another
.properties
.into_iter()
.try_for_each(
|(property, property_model)| match self.properties.entry(property) {
Entry::Vacant(entry) => {
entry.insert(property_model);
Ok(())
}
_ => Err(ModelMergeError),
},
)?;
another
.required
.into_iter()
.for_each(|property| self.required.push(property));
Ok(())
}
fn traverse<'a>(&'a self, cx: TraverseContext<'a>) -> Result<(), &'a str> {
self.properties
.iter()
.map(|(_, reference)| reference)
.chain(self.additional_properties.iter().map(|item| item.as_ref()))
.try_for_each(|reference| reference.traverse(cx))
}
}
impl From<ModelObject> for ModelTypeDescription {
fn from(data: ModelObject) -> Self {
ModelTypeDescription::Object(data)
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum ModelReference {
#[serde(serialize_with = "serialize_model_reference_link")]
Link(String),
Inline(Model),
#[serde(serialize_with = "serialize_model_reference_any")]
Any,
}
fn serialize_model_reference_link<S, N>(name: &N, serializer: S) -> Result<S::Ok, S::Error>
where
N: std::fmt::Display,
S: Serializer,
{
let mut ser = serializer.serialize_map(Some(1))?;
ser.serialize_entry(
"$ref",
&format!("{}{}", crate::SCHEMA_REFERENCE_PREFIX, name),
)?;
ser.end()
}
fn serialize_model_reference_any<S>(serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_struct("Any", 0)?.end()
}
impl ModelReference {
fn traverse<'a>(&'a self, mut cx: TraverseContext<'a>) -> Result<(), &'a str> {
match &self {
ModelReference::Link(ref link) => cx.check(link),
_ => Ok(()),
}
}
}
#[derive(Copy, Clone)]
struct TraverseContext<'a>(&'a BTreeMap<String, Model>);
impl<'a> TraverseContext<'a> {
fn check(&mut self, link: &'a str) -> Result<(), &'a str> {
if self.0.contains_key(link) {
Ok(())
} else {
Err(link)
}
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum SecurityScheme {
Http(HttpSecurityScheme),
ApiKey(ApiKeySecurityScheme),
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase", tag = "scheme")]
pub enum HttpSecurityScheme {
Basic {
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
},
Bearer {
#[serde(rename = "bearerFormat", skip_serializing_if = "Option::is_none")]
format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
},
}
impl From<HttpSecurityScheme> for SecurityScheme {
fn from(data: HttpSecurityScheme) -> Self {
SecurityScheme::Http(data)
}
}
pub enum HttpSecuritySchemeKind {
Basic,
Bearer,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiKeySecurityScheme {
#[serde(rename = "in")]
pub parameter_in: ParameterIn,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl From<ApiKeySecurityScheme> for SecurityScheme {
fn from(data: ApiKeySecurityScheme) -> Self {
SecurityScheme::ApiKey(data)
}
}
pub struct ParameterNotSpecified;