use crate::v3::extension::Extensions;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use url::Url;
use crate::{
v3::components::{BooleanObjectOrReference, Components, ObjectOrReference},
Error, Result, MINIMUM_OPENAPI30_VERSION,
};
impl Spec {
pub fn validate_version(&self) -> Result<semver::Version> {
let spec_version = &self.openapi;
let sem_ver = semver::Version::parse(spec_version)?;
let required_version = semver::VersionReq::parse(MINIMUM_OPENAPI30_VERSION).unwrap();
if required_version.matches(&sem_ver) {
Ok(sem_ver)
} else {
Err(Error::UnsupportedSpecFileVersion(sem_ver))
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct Spec {
pub openapi: String,
pub info: Info,
#[serde(skip_serializing_if = "Option::is_none")]
pub servers: Option<Vec<Server>>,
pub paths: BTreeMap<String, PathItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub components: Option<Components>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<Tag>>,
#[serde(skip_serializing_if = "Option::is_none", rename = "externalDocs")]
pub external_docs: Option<ExternalDoc>,
#[serde(flatten)]
pub extensions: Extensions,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct Info {
pub title: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "termsOfService", skip_serializing_if = "Option::is_none")]
pub terms_of_service: Option<Url>,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub contact: Option<Contact>,
#[serde(skip_serializing_if = "Option::is_none")]
pub license: Option<License>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct Contact {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<Url>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(flatten)]
pub extensions: Extensions,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct License {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<Url>,
#[serde(flatten)]
pub extensions: Extensions,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct Server {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub variables: Option<BTreeMap<String, ServerVariable>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct ServerVariable {
pub default: String,
#[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
pub substitutions_enum: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct PathItem {
#[serde(skip_serializing_if = "Option::is_none", rename = "$ref")]
pub reference: Option<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 = "Option::is_none")]
pub get: Option<Operation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub put: Option<Operation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub post: Option<Operation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub delete: Option<Operation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<Operation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub head: Option<Operation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub patch: Option<Operation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trace: Option<Operation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub servers: Option<Vec<Server>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<Vec<ObjectOrReference<Parameter>>>,
#[serde(flatten)]
pub extensions: Extensions,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct Operation {
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<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 = "Option::is_none", rename = "externalDocs")]
pub external_docs: Option<ExternalDoc>,
#[serde(skip_serializing_if = "Option::is_none", rename = "operationId")]
pub operation_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<Vec<ObjectOrReference<Parameter>>>,
#[serde(skip_serializing_if = "Option::is_none", rename = "requestBody")]
pub request_body: Option<ObjectOrReference<RequestBody>>,
pub responses: BTreeMap<String, Response>,
#[serde(skip_serializing_if = "Option::is_none")]
pub callbacks: Option<BTreeMap<String, Callback>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated: Option<bool>,
pub security: Option<Vec<SecurityRequirement>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub servers: Option<Vec<Server>>,
#[serde(flatten)]
pub extensions: Extensions,
}
pub type SecurityRequirement = BTreeMap<String, Vec<String>>;
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
pub enum ParameterLocation {
#[serde(rename = "path")]
Path,
#[serde(rename = "query")]
Query,
#[serde(rename = "header")]
Header,
#[serde(rename = "cookie")]
Cookie,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct Parameter {
pub name: String,
#[serde(rename = "in")]
pub location: ParameterLocation,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub schema: Option<Schema>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "uniqueItems")]
pub unique_items: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "type")]
pub param_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub style: Option<ParameterStyle>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum ParameterStyle {
Matrix,
Label,
Form,
Simple,
SpaceDelimited,
PipeDelimited,
DeepObject,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default, Eq)]
pub struct Schema {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "$ref")]
pub ref_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "type")]
pub schema_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "enum")]
pub enum_values: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub items: Option<Box<Schema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<BTreeMap<String, Schema>>,
#[serde(skip_serializing_if = "Option::is_none", rename = "readOnly")]
pub read_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nullable: Option<bool>,
#[serde(
skip_serializing_if = "Option::is_none",
rename = "additionalProperties"
)]
pub additional_properties: Option<BooleanObjectOrReference<Box<Schema>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<serde_json::value::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "multipleOf")]
pub multiple_of: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maximum: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none", rename = "exclusiveMaximum")]
pub exclusive_maximum: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub minimum: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none", rename = "exclusiveMinimum")]
pub exclusive_minimum: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none", rename = "maxLength")]
pub max_length: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none", rename = "minLength")]
pub min_length: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "maxItems")]
pub max_items: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none", rename = "minItems")]
pub min_items: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none", rename = "uniqueItems")]
pub unique_items: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none", rename = "maxProperties")]
pub max_properties: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none", rename = "minProperties")]
pub min_properties: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<serde_json::Value>,
#[serde(rename = "allOf", skip_serializing_if = "Option::is_none")]
pub all_of: Option<Vec<ObjectOrReference<Schema>>>,
#[serde(rename = "oneOf", skip_serializing_if = "Option::is_none")]
pub one_of: Option<Vec<ObjectOrReference<Schema>>>,
#[serde(rename = "anyOf", skip_serializing_if = "Option::is_none")]
pub any_of: Option<Vec<ObjectOrReference<Schema>>>,
#[serde(rename = "not", skip_serializing_if = "Option::is_none")]
pub not: Option<Vec<ObjectOrReference<Schema>>>,
#[serde(flatten)]
pub extensions: HashMap<String, String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct Response {
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option<BTreeMap<String, ObjectOrReference<Header>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<BTreeMap<String, MediaType>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub links: Option<BTreeMap<String, ObjectOrReference<Link>>>,
#[serde(flatten)]
pub extensions: Extensions,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct Header {
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub schema: Option<Schema>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "uniqueItems")]
pub unique_items: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "type")]
pub param_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct RequestBody {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub content: BTreeMap<String, MediaType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(untagged)]
pub enum Link {
Ref {
#[serde(rename = "operationRef")]
operation_ref: String,
#[serde(skip_serializing_if = "Option::is_none")]
parameters: Option<BTreeMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
server: Option<Server>,
#[serde(flatten)]
extensions: Extensions,
},
Id {
#[serde(rename = "operationId")]
operation_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
parameters: Option<BTreeMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
server: Option<Server>,
#[serde(flatten)]
extensions: Extensions,
},
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct MediaType {
#[serde(skip_serializing_if = "Option::is_none")]
pub schema: Option<ObjectOrReference<Schema>>,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub examples: Option<MediaTypeExample>,
#[serde(skip_serializing_if = "Option::is_none")]
pub encoding: Option<BTreeMap<String, Encoding>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(untagged)]
pub enum MediaTypeExample {
Example { example: serde_json::Value },
Examples {
examples: BTreeMap<String, ObjectOrReference<Example>>,
},
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct Encoding {
#[serde(skip_serializing_if = "Option::is_none", rename = "contentType")]
pub content_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option<BTreeMap<String, ObjectOrReference<Header>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub style: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub explode: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none", rename = "allowReserved")]
pub allow_reserved: Option<bool>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct Example {
#[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 = "Option::is_none")]
pub value: Option<serde_json::Value>,
#[serde(flatten)]
pub extensions: Extensions,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(tag = "type")]
pub enum SecurityScheme {
#[serde(rename = "apiKey")]
ApiKey {
name: String,
#[serde(rename = "in")]
location: String,
},
#[serde(rename = "http")]
Http {
scheme: String,
#[serde(rename = "bearerFormat")]
bearer_format: Option<String>,
},
#[serde(rename = "oauth2")]
OAuth2 { flows: Box<Flows> },
#[serde(rename = "openIdConnect")]
OpenIdConnect {
#[serde(rename = "openIdConnectUrl")]
open_id_connect_url: String,
},
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Flows {
#[serde(skip_serializing_if = "Option::is_none")]
pub implicit: Option<ImplicitFlow>,
#[serde(skip_serializing_if = "Option::is_none")]
pub password: Option<PasswordFlow>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_credentials: Option<ClientCredentialsFlow>,
#[serde(skip_serializing_if = "Option::is_none")]
pub authorization_code: Option<AuthorizationCodeFlow>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ImplicitFlow {
pub authorization_url: Url,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_url: Option<Url>,
pub scopes: BTreeMap<String, String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PasswordFlow {
token_url: Url,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_url: Option<Url>,
pub scopes: BTreeMap<String, String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ClientCredentialsFlow {
token_url: Url,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_url: Option<Url>,
pub scopes: BTreeMap<String, String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AuthorizationCodeFlow {
pub authorization_url: Url,
pub token_url: Url,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_url: Option<Url>,
pub scopes: BTreeMap<String, String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct Callback(
serde_json::Value, );
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct Tag {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(flatten)]
pub extensions: Extensions,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct ExternalDoc {
pub url: Url,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(flatten)]
pub extensions: Extensions,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_security_scheme_oauth_deser() {
const IMPLICIT_OAUTH2_SAMPLE: &str = r#"{
"type": "oauth2",
"flows": {
"implicit": {
"authorizationUrl": "https://example.com/api/oauth/dialog",
"scopes": {
"write:pets": "modify pets in your account",
"read:pets": "read your pets"
}
},
"authorizationCode": {
"authorizationUrl": "https://example.com/api/oauth/dialog",
"tokenUrl": "https://example.com/api/oauth/token",
"scopes": {
"write:pets": "modify pets in your account",
"read:pets": "read your pets"
}
}
}
}"#;
let obj: SecurityScheme = serde_json::from_str(&IMPLICIT_OAUTH2_SAMPLE).unwrap();
match obj {
SecurityScheme::OAuth2 { flows } => {
assert!(flows.implicit.is_some());
let implicit = flows.implicit.unwrap();
assert_eq!(
implicit.authorization_url,
Url::parse("https://example.com/api/oauth/dialog").unwrap()
);
assert!(implicit.scopes.contains_key("write:pets"));
assert!(implicit.scopes.contains_key("read:pets"));
assert!(flows.authorization_code.is_some());
let auth_code = flows.authorization_code.unwrap();
assert_eq!(
auth_code.authorization_url,
Url::parse("https://example.com/api/oauth/dialog").unwrap()
);
assert_eq!(
auth_code.token_url,
Url::parse("https://example.com/api/oauth/token").unwrap()
);
assert!(implicit.scopes.contains_key("write:pets"));
assert!(implicit.scopes.contains_key("read:pets"));
}
_ => assert!(false, "wrong security scheme type"),
}
}
}