use crate::extensions::Extensions;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct OpenApiSpec {
pub openapi: String,
pub info: Info,
#[serde(rename = "jsonSchemaDialect", default)]
pub json_schema_dialect: Option<String>,
#[serde(default)]
pub servers: Option<Vec<Server>>,
#[serde(default)]
pub paths: Option<BTreeMap<String, PathItem>>,
#[serde(default)]
pub webhooks: Option<BTreeMap<String, PathItem>>,
#[serde(default)]
pub components: Option<Components>,
#[serde(default)]
pub security: Option<Vec<BTreeMap<String, Vec<String>>>>,
#[serde(default)]
pub tags: Option<Vec<Tag>>,
#[serde(rename = "externalDocs", default)]
pub external_docs: Option<ExternalDocs>,
#[serde(rename = "$self", default)]
pub self_uri: Option<String>,
#[serde(flatten, default)]
pub extensions: Extensions,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Info {
pub title: String,
#[serde(default)]
pub summary: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(rename = "termsOfService", default)]
pub terms_of_service: Option<String>,
#[serde(default)]
pub contact: Option<Value>,
#[serde(default)]
pub license: Option<Value>,
#[serde(default)]
pub version: Option<String>,
#[serde(flatten, default)]
pub extensions: Extensions,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Components {
#[serde(default)]
pub schemas: Option<BTreeMap<String, Schema>>,
#[serde(default)]
pub responses: Option<BTreeMap<String, Response>>,
#[serde(default)]
pub parameters: Option<BTreeMap<String, Parameter>>,
#[serde(default)]
pub examples: Option<BTreeMap<String, Example>>,
#[serde(rename = "requestBodies", default)]
pub request_bodies: Option<BTreeMap<String, RequestBody>>,
#[serde(default)]
pub headers: Option<BTreeMap<String, Header>>,
#[serde(rename = "securitySchemes", default)]
pub security_schemes: Option<BTreeMap<String, SecurityScheme>>,
#[serde(default)]
pub links: Option<BTreeMap<String, Link>>,
#[serde(default)]
pub callbacks: Option<BTreeMap<String, Callback>>,
#[serde(rename = "pathItems", default)]
pub path_items: Option<BTreeMap<String, PathItem>>,
#[serde(rename = "mediaTypes", default)]
pub media_types: Option<BTreeMap<String, MediaType>>,
#[serde(flatten, default)]
pub extensions: Extensions,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum Schema {
Reference {
#[serde(rename = "$ref")]
reference: String,
#[serde(flatten)]
extra: BTreeMap<String, Value>,
},
RecursiveRef {
#[serde(rename = "$recursiveRef")]
recursive_ref: String,
#[serde(flatten)]
extra: BTreeMap<String, Value>,
},
DynamicRef {
#[serde(rename = "$dynamicRef")]
dynamic_ref: String,
#[serde(flatten)]
extra: BTreeMap<String, Value>,
},
OneOf {
#[serde(rename = "oneOf")]
one_of: Vec<Schema>,
discriminator: Option<Discriminator>,
#[serde(flatten)]
details: SchemaDetails,
},
AnyOf {
#[serde(rename = "type")]
schema_type: Option<SchemaType>,
#[serde(rename = "anyOf")]
any_of: Vec<Schema>,
discriminator: Option<Discriminator>,
#[serde(flatten)]
details: SchemaDetails,
},
TypedMulti {
#[serde(rename = "type")]
schema_types: Vec<SchemaType>,
#[serde(flatten)]
details: SchemaDetails,
},
Typed {
#[serde(rename = "type")]
schema_type: SchemaType,
#[serde(flatten)]
details: SchemaDetails,
},
AllOf {
#[serde(rename = "allOf")]
all_of: Vec<Schema>,
#[serde(flatten)]
details: SchemaDetails,
},
Untyped {
#[serde(flatten)]
details: SchemaDetails,
},
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum SchemaType {
String,
Integer,
Number,
Boolean,
Array,
Object,
#[serde(rename = "null")]
Null,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct SchemaDetails {
pub description: Option<String>,
pub nullable: Option<bool>,
#[serde(rename = "$recursiveAnchor")]
pub recursive_anchor: Option<bool>,
#[serde(rename = "$dynamicAnchor")]
pub dynamic_anchor: Option<String>,
#[serde(rename = "$id")]
pub schema_id: Option<String>,
#[serde(rename = "enum")]
pub enum_values: Option<Vec<Value>>,
pub format: Option<String>,
pub default: Option<Value>,
#[serde(rename = "const")]
pub const_value: Option<Value>,
pub properties: Option<BTreeMap<String, Schema>>,
pub required: Option<Vec<String>>,
#[serde(rename = "additionalProperties")]
pub additional_properties: Option<AdditionalProperties>,
pub items: Option<Box<Schema>>,
pub minimum: Option<f64>,
pub maximum: Option<f64>,
#[serde(rename = "minLength")]
pub min_length: Option<u64>,
#[serde(rename = "maxLength")]
pub max_length: Option<u64>,
pub pattern: Option<String>,
#[serde(rename = "exclusiveMinimum")]
pub exclusive_minimum: Option<ExclusiveBound>,
#[serde(rename = "exclusiveMaximum")]
pub exclusive_maximum: Option<ExclusiveBound>,
#[serde(rename = "multipleOf")]
pub multiple_of: Option<f64>,
#[serde(rename = "minItems")]
pub min_items: Option<u64>,
#[serde(rename = "maxItems")]
pub max_items: Option<u64>,
#[serde(rename = "uniqueItems")]
pub unique_items: Option<bool>,
#[serde(rename = "minProperties")]
pub min_properties: Option<u64>,
#[serde(rename = "maxProperties")]
pub max_properties: Option<u64>,
#[serde(rename = "prefixItems")]
pub prefix_items: Option<Vec<Schema>>,
pub contains: Option<Box<Schema>>,
#[serde(rename = "minContains")]
pub min_contains: Option<u64>,
#[serde(rename = "maxContains")]
pub max_contains: Option<u64>,
#[serde(rename = "patternProperties")]
pub pattern_properties: Option<BTreeMap<String, Schema>>,
#[serde(rename = "propertyNames")]
pub property_names: Option<Box<Schema>>,
#[serde(rename = "unevaluatedProperties")]
pub unevaluated_properties: Option<AdditionalProperties>,
#[serde(rename = "unevaluatedItems")]
pub unevaluated_items: Option<AdditionalProperties>,
#[serde(rename = "dependentRequired")]
pub dependent_required: Option<BTreeMap<String, Vec<String>>>,
#[serde(rename = "dependentSchemas")]
pub dependent_schemas: Option<BTreeMap<String, Schema>>,
#[serde(rename = "contentEncoding")]
pub content_encoding: Option<String>,
#[serde(rename = "contentMediaType")]
pub content_media_type: Option<String>,
#[serde(rename = "contentSchema")]
pub content_schema: Option<Box<Schema>>,
#[serde(rename = "if")]
pub if_schema: Option<Box<Schema>>,
#[serde(rename = "then")]
pub then_schema: Option<Box<Schema>>,
#[serde(rename = "else")]
pub else_schema: Option<Box<Schema>>,
pub not: Option<Box<Schema>>,
pub title: Option<String>,
pub deprecated: Option<bool>,
#[serde(rename = "readOnly")]
pub read_only: Option<bool>,
#[serde(rename = "writeOnly")]
pub write_only: Option<bool>,
pub examples: Option<Vec<Value>>,
pub example: Option<Value>,
#[serde(rename = "$comment")]
pub comment: Option<String>,
#[serde(rename = "$schema")]
pub schema_keyword: Option<String>,
#[serde(rename = "$defs")]
pub defs: Option<BTreeMap<String, Schema>>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum ExclusiveBound {
Bool(bool),
Number(f64),
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum AdditionalProperties {
Boolean(bool),
Schema(Box<Schema>),
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Example {
#[serde(default)]
pub summary: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub value: Option<Value>,
#[serde(rename = "externalValue", default)]
pub external_value: Option<String>,
#[serde(rename = "dataValue", default)]
pub data_value: Option<Value>,
#[serde(rename = "serializedValue", default)]
pub serialized_value: Option<String>,
#[serde(rename = "$ref", default)]
pub reference: Option<String>,
#[serde(flatten, default)]
pub extensions: Extensions,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Link {
#[serde(rename = "operationRef", default)]
pub operation_ref: Option<String>,
#[serde(rename = "operationId", default)]
pub operation_id: Option<String>,
#[serde(default)]
pub parameters: Option<BTreeMap<String, Value>>,
#[serde(rename = "requestBody", default)]
pub request_body: Option<Value>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub server: Option<Server>,
#[serde(rename = "$ref", default)]
pub reference: Option<String>,
#[serde(flatten, default)]
pub extensions: Extensions,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(transparent)]
pub struct Callback(pub BTreeMap<String, PathItem>);
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Encoding {
#[serde(rename = "contentType", default)]
pub content_type: Option<String>,
#[serde(default)]
pub headers: Option<BTreeMap<String, Header>>,
#[serde(default)]
pub style: Option<String>,
#[serde(default)]
pub explode: Option<bool>,
#[serde(rename = "allowReserved", default)]
pub allow_reserved: Option<bool>,
#[serde(rename = "itemEncoding", default)]
pub item_encoding: Option<Box<Encoding>>,
#[serde(flatten, default)]
pub extensions: Extensions,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Header {
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub required: Option<bool>,
#[serde(default)]
pub deprecated: Option<bool>,
#[serde(rename = "allowEmptyValue", default)]
pub allow_empty_value: Option<bool>,
#[serde(default)]
pub style: Option<String>,
#[serde(default)]
pub explode: Option<bool>,
#[serde(rename = "allowReserved", default)]
pub allow_reserved: Option<bool>,
#[serde(default)]
pub schema: Option<Schema>,
#[serde(default)]
pub content: Option<BTreeMap<String, MediaType>>,
#[serde(default)]
pub example: Option<Value>,
#[serde(default)]
pub examples: Option<Value>,
#[serde(rename = "$ref", default)]
pub reference: Option<String>,
#[serde(flatten, default)]
pub extensions: Extensions,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type")]
pub enum SecurityScheme {
#[serde(rename = "apiKey")]
ApiKey {
name: String,
#[serde(rename = "in")]
location: String, #[serde(default)]
description: Option<String>,
#[serde(default)]
deprecated: Option<bool>,
#[serde(flatten, default)]
extensions: Extensions,
},
#[serde(rename = "http")]
Http {
scheme: String, #[serde(rename = "bearerFormat", default)]
bearer_format: Option<String>,
#[serde(default)]
description: Option<String>,
#[serde(default)]
deprecated: Option<bool>,
#[serde(flatten, default)]
extensions: Extensions,
},
#[serde(rename = "mutualTLS")]
MutualTls {
#[serde(default)]
description: Option<String>,
#[serde(default)]
deprecated: Option<bool>,
#[serde(flatten, default)]
extensions: Extensions,
},
#[serde(rename = "oauth2")]
OAuth2 {
flows: Box<OAuthFlows>,
#[serde(default)]
description: Option<String>,
#[serde(rename = "oauth2MetadataUrl", default)]
oauth2_metadata_url: Option<String>,
#[serde(default)]
deprecated: Option<bool>,
#[serde(flatten, default)]
extensions: Extensions,
},
#[serde(rename = "openIdConnect")]
OpenIdConnect {
#[serde(rename = "openIdConnectUrl")]
open_id_connect_url: String,
#[serde(default)]
description: Option<String>,
#[serde(default)]
deprecated: Option<bool>,
#[serde(flatten, default)]
extensions: Extensions,
},
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct OAuthFlows {
#[serde(default)]
pub implicit: Option<OAuthFlow>,
#[serde(default)]
pub password: Option<OAuthFlow>,
#[serde(rename = "clientCredentials", default)]
pub client_credentials: Option<OAuthFlow>,
#[serde(rename = "authorizationCode", default)]
pub authorization_code: Option<OAuthFlow>,
#[serde(rename = "deviceAuthorization", default)]
pub device_authorization: Option<OAuthFlow>,
#[serde(flatten, default)]
pub extensions: Extensions,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct OAuthFlow {
#[serde(rename = "authorizationUrl", default)]
pub authorization_url: Option<String>,
#[serde(rename = "tokenUrl", default)]
pub token_url: Option<String>,
#[serde(rename = "refreshUrl", default)]
pub refresh_url: Option<String>,
#[serde(rename = "deviceAuthorizationUrl", default)]
pub device_authorization_url: Option<String>,
pub scopes: BTreeMap<String, String>,
#[serde(flatten, default)]
pub extensions: Extensions,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ExternalDocs {
pub url: String,
#[serde(default)]
pub description: Option<String>,
#[serde(flatten, default)]
pub extensions: Extensions,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Tag {
pub name: String,
#[serde(default)]
pub summary: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub parent: Option<String>,
#[serde(default)]
pub kind: Option<String>,
#[serde(rename = "externalDocs", default)]
pub external_docs: Option<ExternalDocs>,
#[serde(flatten, default)]
pub extensions: Extensions,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Server {
pub url: String,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub variables: Option<BTreeMap<String, ServerVariable>>,
#[serde(flatten, default)]
pub extensions: Extensions,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ServerVariable {
#[serde(default)]
pub default: Option<String>,
#[serde(rename = "enum", default)]
pub enum_values: Option<Vec<String>>,
#[serde(default)]
pub description: Option<String>,
#[serde(flatten, default)]
pub extensions: Extensions,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Discriminator {
#[serde(rename = "propertyName")]
pub property_name: String,
#[serde(default)]
pub mapping: Option<BTreeMap<String, String>>,
#[serde(rename = "defaultMapping", default)]
pub default_mapping: Option<String>,
#[serde(flatten, default)]
pub extensions: Extensions,
}
impl Schema {
pub fn schema_type(&self) -> Option<&SchemaType> {
match self {
Schema::Typed { schema_type, .. } => Some(schema_type),
Schema::TypedMulti { schema_types, .. } => schema_types
.iter()
.find(|t| **t != SchemaType::Null)
.or_else(|| schema_types.first()),
_ => None,
}
}
pub fn type_array_contains_null(&self) -> bool {
match self {
Schema::TypedMulti { schema_types, .. } => schema_types.contains(&SchemaType::Null),
_ => false,
}
}
pub fn details(&self) -> &SchemaDetails {
static EMPTY_DETAILS: Lazy<SchemaDetails> = Lazy::new(SchemaDetails::default);
match self {
Schema::Typed { details, .. } => details,
Schema::TypedMulti { details, .. } => details,
Schema::Reference { .. } | Schema::RecursiveRef { .. } | Schema::DynamicRef { .. } => {
&EMPTY_DETAILS
}
Schema::OneOf { details, .. } => details,
Schema::AnyOf { details, .. } => details,
Schema::AllOf { details, .. } => details,
Schema::Untyped { details } => details,
}
}
pub fn details_mut(&mut self) -> &mut SchemaDetails {
match self {
Schema::Typed { details, .. } => details,
Schema::TypedMulti { details, .. } => details,
Schema::Reference { .. } => {
panic!("Cannot get mutable details for reference schema")
}
Schema::RecursiveRef { .. } => {
panic!("Cannot get mutable details for recursive reference schema")
}
Schema::DynamicRef { .. } => {
panic!("Cannot get mutable details for dynamic reference schema")
}
Schema::OneOf { details, .. } => details,
Schema::AnyOf { details, .. } => details,
Schema::AllOf { details, .. } => details,
Schema::Untyped { details } => details,
}
}
pub fn is_reference(&self) -> bool {
matches!(self, Schema::Reference { .. } | Schema::RecursiveRef { .. })
}
pub fn reference(&self) -> Option<&str> {
match self {
Schema::Reference { reference, .. } => Some(reference),
_ => None,
}
}
pub fn recursive_reference(&self) -> Option<&str> {
match self {
Schema::RecursiveRef { recursive_ref, .. } => Some(recursive_ref),
_ => None,
}
}
pub fn is_discriminated_union(&self) -> bool {
match self {
Schema::OneOf { discriminator, .. } => discriminator.is_some(),
Schema::AnyOf { discriminator, .. } => discriminator.is_some(),
_ => false,
}
}
pub fn discriminator(&self) -> Option<&Discriminator> {
match self {
Schema::OneOf { discriminator, .. } => discriminator.as_ref(),
Schema::AnyOf { discriminator, .. } => discriminator.as_ref(),
_ => None,
}
}
pub fn union_variants(&self) -> Option<&[Schema]> {
match self {
Schema::OneOf { one_of, .. } => Some(one_of),
Schema::AnyOf { any_of, .. } => Some(any_of),
_ => None,
}
}
pub fn is_nullable_pattern(&self) -> bool {
let variants = match self {
Schema::AnyOf { any_of, .. } => any_of,
Schema::OneOf { one_of, .. } => one_of,
_ => return false,
};
variants.len() == 2
&& variants
.iter()
.any(|s| matches!(s.schema_type(), Some(SchemaType::Null)))
}
pub fn non_null_variant(&self) -> Option<&Schema> {
if !self.is_nullable_pattern() {
return None;
}
let variants = match self {
Schema::AnyOf { any_of, .. } => any_of,
Schema::OneOf { one_of, .. } => one_of,
_ => return None,
};
variants
.iter()
.find(|s| !matches!(s.schema_type(), Some(SchemaType::Null)))
}
pub fn inferred_type(&self) -> Option<SchemaType> {
match self {
Schema::Typed { schema_type, .. } => Some(schema_type.clone()),
Schema::TypedMulti { .. } => self.schema_type().cloned(),
Schema::Untyped { details } => {
if details.properties.is_some() {
Some(SchemaType::Object)
} else if details.items.is_some() {
Some(SchemaType::Array)
} else if details.enum_values.is_some() {
Some(SchemaType::String) } else {
None
}
}
_ => None,
}
}
}
impl SchemaDetails {
pub fn is_nullable(&self) -> bool {
self.nullable.unwrap_or(false)
}
pub fn is_string_enum(&self) -> bool {
self.enum_values.is_some() || self.const_string_value().is_some()
}
pub fn string_enum_values(&self) -> Option<Vec<String>> {
if let Some(values) = self.enum_values.as_ref() {
return Some(
values
.iter()
.map(|v| match v {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
Value::Null => "null".to_string(),
_ => v.to_string(),
})
.collect(),
);
}
self.const_string_value().map(|s| vec![s])
}
fn const_string_value(&self) -> Option<String> {
self.const_value
.as_ref()
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
pub fn is_field_required(&self, field_name: &str) -> bool {
self.required
.as_ref()
.map(|req| req.contains(&field_name.to_string()))
.unwrap_or(false)
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PathItem {
#[serde(default)]
pub summary: Option<String>,
#[serde(default)]
pub description: Option<String>,
pub get: Option<Operation>,
pub put: Option<Operation>,
pub post: Option<Operation>,
pub delete: Option<Operation>,
pub options: Option<Operation>,
pub head: Option<Operation>,
pub patch: Option<Operation>,
pub trace: Option<Operation>,
pub query: Option<Operation>,
#[serde(rename = "additionalOperations", default)]
pub additional_operations: Option<BTreeMap<String, Operation>>,
pub parameters: Option<Vec<Parameter>>,
#[serde(default)]
pub servers: Option<Vec<Server>>,
#[serde(rename = "$ref", default)]
pub reference: Option<String>,
#[serde(flatten, default)]
pub extensions: Extensions,
}
impl PathItem {
pub fn operations(&self) -> Vec<(&str, &Operation)> {
let mut ops = Vec::new();
if let Some(ref op) = self.get {
ops.push(("get", op));
}
if let Some(ref op) = self.put {
ops.push(("put", op));
}
if let Some(ref op) = self.post {
ops.push(("post", op));
}
if let Some(ref op) = self.delete {
ops.push(("delete", op));
}
if let Some(ref op) = self.options {
ops.push(("options", op));
}
if let Some(ref op) = self.head {
ops.push(("head", op));
}
if let Some(ref op) = self.patch {
ops.push(("patch", op));
}
if let Some(ref op) = self.trace {
ops.push(("trace", op));
}
if let Some(ref op) = self.query {
ops.push(("query", op));
}
if let Some(map) = &self.additional_operations {
for (verb, op) in map {
ops.push((verb.as_str(), op));
}
}
ops
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Operation {
#[serde(rename = "operationId", default)]
pub operation_id: Option<String>,
#[serde(default)]
pub summary: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub tags: Option<Vec<String>>,
#[serde(default)]
pub deprecated: Option<bool>,
pub parameters: Option<Vec<Parameter>>,
#[serde(rename = "requestBody")]
pub request_body: Option<RequestBody>,
pub responses: Option<BTreeMap<String, Response>>,
#[serde(default)]
pub callbacks: Option<BTreeMap<String, Callback>>,
#[serde(default)]
pub security: Option<Vec<BTreeMap<String, Vec<String>>>>,
#[serde(default)]
pub servers: Option<Vec<Server>>,
#[serde(rename = "externalDocs", default)]
pub external_docs: Option<ExternalDocs>,
#[serde(flatten, default)]
pub extensions: Extensions,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Parameter {
#[serde(default)]
pub name: Option<String>,
#[serde(rename = "in", default)]
pub location: Option<String>,
#[serde(default)]
pub required: Option<bool>,
#[serde(default)]
pub deprecated: Option<bool>,
#[serde(rename = "allowEmptyValue", default)]
pub allow_empty_value: Option<bool>,
#[serde(default)]
pub style: Option<String>,
#[serde(default)]
pub explode: Option<bool>,
#[serde(rename = "allowReserved", default)]
pub allow_reserved: Option<bool>,
#[serde(default)]
pub schema: Option<Schema>,
#[serde(default)]
pub content: Option<BTreeMap<String, MediaType>>,
#[serde(default)]
pub example: Option<Value>,
#[serde(default)]
pub examples: Option<BTreeMap<String, Example>>,
#[serde(default)]
pub description: Option<String>,
#[serde(rename = "$ref", default)]
pub reference: Option<String>,
#[serde(flatten, default)]
pub extensions: Extensions,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RequestBody {
pub content: Option<BTreeMap<String, MediaType>>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub required: Option<bool>,
#[serde(rename = "$ref", default)]
pub reference: Option<String>,
#[serde(flatten, default)]
pub extensions: Extensions,
}
pub fn is_json_media_type(ct: &str) -> bool {
let essence = ct
.split(';')
.next()
.unwrap_or(ct)
.trim()
.to_ascii_lowercase();
if essence == "application/json" {
return true;
}
if let Some(subtype) = essence.strip_prefix("application/") {
return subtype.ends_with("+json");
}
false
}
pub fn is_form_urlencoded_media_type(ct: &str) -> bool {
let essence = ct
.split(';')
.next()
.unwrap_or(ct)
.trim()
.to_ascii_lowercase();
essence == "application/x-www-form-urlencoded"
}
fn find_json_content(content: &BTreeMap<String, MediaType>) -> Option<(&str, &MediaType)> {
if let Some(mt) = content.get("application/json") {
return Some(("application/json", mt));
}
content
.iter()
.find(|(ct, _)| is_json_media_type(ct))
.map(|(ct, mt)| (ct.as_str(), mt))
}
impl RequestBody {
pub fn json_schema(&self) -> Option<&Schema> {
self.content
.as_ref()
.and_then(find_json_content)
.and_then(|(_, media_type)| media_type.schema.as_ref())
}
pub fn best_content(&self) -> Option<(&str, Option<&Schema>)> {
let content = self.content.as_ref()?;
if let Some((ct, media_type)) = find_json_content(content) {
return Some((ct, media_type.schema.as_ref()));
}
const PRIORITY: &[&str] = &[
"application/x-www-form-urlencoded",
"multipart/form-data",
"application/octet-stream",
"text/plain",
];
for ct in PRIORITY {
if let Some(media_type) = content.get(*ct) {
return Some((*ct, media_type.schema.as_ref()));
}
}
None
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Response {
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub headers: Option<BTreeMap<String, Header>>,
#[serde(default)]
pub content: Option<BTreeMap<String, MediaType>>,
#[serde(default)]
pub links: Option<Value>,
#[serde(rename = "$ref", default)]
pub reference: Option<String>,
#[serde(flatten, default)]
pub extensions: Extensions,
}
impl Response {
pub fn json_schema(&self) -> Option<&Schema> {
self.content
.as_ref()
.and_then(find_json_content)
.and_then(|(_, media_type)| media_type.schema.as_ref())
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MediaType {
#[serde(default)]
pub schema: Option<Schema>,
#[serde(default)]
pub example: Option<Value>,
#[serde(default)]
pub examples: Option<BTreeMap<String, Example>>,
#[serde(default)]
pub encoding: Option<BTreeMap<String, Encoding>>,
#[serde(rename = "itemSchema", default)]
pub item_schema: Option<Schema>,
#[serde(rename = "prefixEncoding", default)]
pub prefix_encoding: Option<Vec<Encoding>>,
#[serde(rename = "itemEncoding", default)]
pub item_encoding: Option<Encoding>,
#[serde(rename = "$ref", default)]
pub reference: Option<String>,
#[serde(flatten, default)]
pub extensions: Extensions,
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_parse_simple_object_schema() {
let schema_json = json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "User name"
},
"age": {
"type": "integer"
}
},
"required": ["name"]
});
let schema: Schema = serde_json::from_value(schema_json).unwrap();
match schema {
Schema::Typed {
schema_type: SchemaType::Object,
details,
} => {
assert!(details.properties.is_some());
assert_eq!(details.required, Some(vec!["name".to_string()]));
assert!(details.is_field_required("name"));
assert!(!details.is_field_required("age"));
}
_ => panic!("Expected object schema"),
}
}
#[test]
fn test_parse_string_enum() {
let schema_json = json!({
"type": "string",
"enum": ["active", "inactive", "pending"],
"description": "User status"
});
let schema: Schema = serde_json::from_value(schema_json).unwrap();
match schema {
Schema::Typed {
schema_type: SchemaType::String,
details,
} => {
assert!(details.is_string_enum());
let values = details.string_enum_values().unwrap();
assert_eq!(values, vec!["active", "inactive", "pending"]);
}
_ => panic!("Expected string enum schema"),
}
}
#[test]
fn test_parse_reference_schema() {
let schema_json = json!({
"$ref": "#/components/schemas/User"
});
let schema: Schema = serde_json::from_value(schema_json).unwrap();
assert!(schema.is_reference());
assert_eq!(schema.reference(), Some("#/components/schemas/User"));
}
#[test]
fn test_parse_discriminated_union() {
let schema_json = json!({
"oneOf": [
{"$ref": "#/components/schemas/Dog"},
{"$ref": "#/components/schemas/Cat"}
],
"discriminator": {
"propertyName": "petType"
}
});
let schema: Schema = serde_json::from_value(schema_json).unwrap();
assert!(schema.is_discriminated_union());
let discriminator = schema.discriminator().unwrap();
assert_eq!(discriminator.property_name, "petType");
}
#[test]
fn test_parse_nullable_pattern() {
let schema_json = json!({
"anyOf": [
{"$ref": "#/components/schemas/User"},
{"type": "null"}
]
});
let schema: Schema = serde_json::from_value(schema_json).unwrap();
assert!(schema.is_nullable_pattern());
let non_null = schema.non_null_variant().unwrap();
assert!(non_null.is_reference());
}
#[test]
fn is_json_media_type_accepts_canonical_and_structured_suffix() {
assert!(is_json_media_type("application/json"));
assert!(is_json_media_type("application/json; charset=utf-8"));
assert!(is_json_media_type("APPLICATION/JSON"));
assert!(is_json_media_type("application/vnd.api+json"));
assert!(is_json_media_type("application/hal+json"));
assert!(is_json_media_type("application/problem+json"));
assert!(is_json_media_type("application/ld+json"));
assert!(is_json_media_type(
"application/vnd.api+json; charset=utf-8"
));
assert!(!is_json_media_type("application/xml"));
assert!(!is_json_media_type("application/x-www-form-urlencoded"));
assert!(!is_json_media_type("text/plain"));
assert!(!is_json_media_type("application/jsonbutnotreally"));
assert!(!is_json_media_type("text/something+json"));
}
#[test]
fn request_body_json_schema_finds_vnd_api_plus_json() {
let body_json = json!({
"required": true,
"content": {
"application/vnd.api+json": {
"schema": {"$ref": "#/components/schemas/create_api_key"}
}
}
});
let body: RequestBody = serde_json::from_value(body_json).unwrap();
let schema = body.json_schema().expect("expected +json schema match");
assert!(schema.is_reference());
}
#[test]
fn request_body_best_content_prefers_canonical_json_over_plus_json() {
let body_json = json!({
"required": true,
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/A"}
},
"application/vnd.api+json": {
"schema": {"$ref": "#/components/schemas/B"}
}
}
});
let body: RequestBody = serde_json::from_value(body_json).unwrap();
let (ct, _) = body.best_content().expect("expected best_content");
assert_eq!(ct, "application/json");
}
#[test]
fn request_body_best_content_falls_back_to_plus_json() {
let body_json = json!({
"required": true,
"content": {
"application/vnd.api+json": {
"schema": {"$ref": "#/components/schemas/B"}
}
}
});
let body: RequestBody = serde_json::from_value(body_json).unwrap();
let (ct, _) = body.best_content().expect("expected best_content");
assert_eq!(ct, "application/vnd.api+json");
}
#[test]
fn response_json_schema_finds_vnd_api_plus_json() {
let resp_json = json!({
"description": "OK",
"content": {
"application/vnd.api+json": {
"schema": {"$ref": "#/components/schemas/api_keys"}
}
}
});
let resp: Response = serde_json::from_value(resp_json).unwrap();
let schema = resp.json_schema().expect("expected +json schema match");
assert!(schema.is_reference());
}
}