pub mod traverser;
pub mod types;
mod validator;
use crate::traverser::OpenApiTraverser;
use crate::types::{
OpenApiVersion, Operation, ParameterLocation, RequestBodyData, RequestParamData,
};
use crate::validator::{RequestBodyValidator, RequestParameterValidator, RequestScopeValidator, Validator};
use jsonschema::{Resource, ValidationOptions, Validator as JsonValidator};
use serde_json::{Value, json};
use std::cmp::PartialEq;
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use unicase::UniCase;
const CONTENT_FIELD: &'static str = "content";
const SCHEMA_FIELD: &'static str = "schema";
const REQUEST_BODY_FIELD: &'static str = "requestBody";
const PATHS_FIELD: &'static str = "paths";
const PARAMETERS_FIELD: &'static str = "parameters";
const REF_FIELD: &'static str = "$ref";
const SECURITY_FIELD: &'static str = "security";
const PATH_SEPARATOR: &'static str = "/";
const TILDE: &'static str = "~";
const ENCODED_BACKSLASH: &'static str = "~1";
const ENCODED_TILDE: &'static str = "~0";
const NAME_FIELD: &'static str = "name";
const OPENAPI_FIELD: &'static str = "openapi";
const REQUIRED_FIELD: &'static str = "required";
const IN_FIELD: &'static str = "in";
pub struct OpenApiPayloadValidator {
traverser: OpenApiTraverser,
options: ValidationOptions,
}
impl OpenApiPayloadValidator {
pub fn new(mut value: Value) -> Result<Self, ValidationError> {
value["$id"] = json!("@@root");
let version = traverser::get_as_str(&value, OPENAPI_FIELD)?;
let version = OpenApiVersion::from_str(version)?;
let draft = version.get_draft();
let resource = match Resource::from_contents(value.clone()) {
Ok(res) => res,
Err(e) => {
return Err(ValidationError::SchemaValidationFailed);
}
};
let options = JsonValidator::options()
.with_draft(draft)
.with_resource("@@inner", resource);
Ok(Self {
traverser: OpenApiTraverser::new(value),
options,
})
}
fn extract_content_type(headers_instance: &HashMap<UniCase<String>, String>) -> Option<String> {
if let Some(content_type_header) = headers_instance.get(&UniCase::from("content-type")) {
if let Some(split_content_type) =
content_type_header.split(";").find(|content_type_segment| {
content_type_segment.contains("/")
&& (content_type_segment.starts_with("application")
|| content_type_segment.starts_with("text")
|| content_type_segment.starts_with("xml")
|| content_type_segment.starts_with("audio")
|| content_type_segment.starts_with("example")
|| content_type_segment.starts_with("font")
|| content_type_segment.starts_with("image")
|| content_type_segment.starts_with("model")
|| content_type_segment.starts_with("video")
|| content_type_segment.starts_with("multipart")
|| content_type_segment.starts_with("message"))
})
{
return Some(split_content_type.to_string());
}
}
None
}
pub fn validate_request_body(
&self,
operation: &Operation,
body_instance: Option<&impl RequestBodyData>,
headers_instance: &impl RequestParamData,
) -> Result<(), ValidationError> {
let headers_instance = headers_instance.get();
let content_type = Self::extract_content_type(&headers_instance);
let validator = match body_instance {
None => RequestBodyValidator::new(None, content_type),
Some(val) => RequestBodyValidator::new(Some(val.get()), content_type),
};
validator.validate(&self.traverser, operation, &self.options)
}
pub fn validate_request_header_params(
&self,
operation: &Operation,
headers: &impl RequestParamData,
) -> Result<(), ValidationError> {
let headers = headers.get();
let validator = RequestParameterValidator::new(headers, ParameterLocation::Header);
validator.validate(&self.traverser, operation, &self.options)
}
pub fn validate_request_query_parameters(
&self,
operation: &Operation,
query_params: &impl RequestParamData,
) -> Result<(), ValidationError> {
let query_params = query_params.get();
let validator = RequestParameterValidator::new(query_params, ParameterLocation::Query);
validator.validate(&self.traverser, operation, &self.options)
}
pub fn validate_request_scopes(
&self,
operation: &Operation,
scopes: &Vec<String>
) -> Result<(), ValidationError> {
let validator = RequestScopeValidator::new(scopes);
validator.validate(&self.traverser, operation, &self.options)
}
}
#[derive(Ord, PartialOrd, Eq, PartialEq)]
pub enum ValidationErrorKind {
InvalidPayload,
InvalidSpec,
MismatchingSchema,
}
#[derive(Debug)]
pub enum ValidationError {
RequiredPropertyMissing,
RequiredParameterMissing,
UnsupportedSpecVersion,
SchemaValidationFailed,
ValueExpected,
DefinitionExpected,
UnexpectedType,
MissingOperation,
CircularReference,
FieldMissing,
InvalidRef,
InvalidType,
}
impl Display for ValidationError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ValidationError::SchemaValidationFailed => {
todo!()
}
ValidationError::DefinitionExpected => {
todo!()
}
ValidationError::ValueExpected => {
todo!()
}
ValidationError::RequiredPropertyMissing => {
todo!()
}
ValidationError::RequiredParameterMissing => {
todo!()
}
ValidationError::UnsupportedSpecVersion => {
todo!()
}
ValidationError::UnexpectedType => {
todo!()
}
ValidationError::MissingOperation => {
todo!()
}
ValidationError::FieldMissing => {
todo!()
}
ValidationError::CircularReference => {
todo!()
}
ValidationError::InvalidRef => {
todo!()
},
ValidationError::InvalidType => {
todo!()
},
}
}
}
impl ValidationError {
pub fn kind(&self) -> ValidationErrorKind {
match self {
ValidationError::ValueExpected
| ValidationError::SchemaValidationFailed
| ValidationError::RequiredParameterMissing
| ValidationError::MissingOperation
| ValidationError::RequiredPropertyMissing => ValidationErrorKind::InvalidPayload,
ValidationError::FieldMissing => ValidationErrorKind::MismatchingSchema,
ValidationError::InvalidRef
| ValidationError::UnsupportedSpecVersion
| ValidationError::InvalidType
| ValidationError::DefinitionExpected
| ValidationError::UnexpectedType
| ValidationError::CircularReference => ValidationErrorKind::InvalidSpec,
}
}
}
impl std::error::Error for ValidationError {}
#[derive(Debug, Clone)]
pub struct JsonPath(pub Vec<String>);
impl JsonPath {
fn new() -> Self {
JsonPath(Vec::new())
}
fn add(&mut self, segment: &str) -> &mut Self {
if segment.contains(TILDE) || segment.contains(PATH_SEPARATOR) {
let segment = segment
.replace(TILDE, ENCODED_TILDE)
.replace(PATH_SEPARATOR, ENCODED_BACKSLASH);
self.0.push(segment);
} else {
self.0.push(segment.to_owned());
}
self
}
fn format_path(&self) -> String {
self.0.join(PATH_SEPARATOR)
}
}
#[cfg(test)]
mod test {
use crate::types::{Operation, RequestBodyData, RequestParamData};
use crate::{JsonPath, OpenApiPayloadValidator, ValidationError};
use serde_json::{Value, json};
use std::collections::HashMap;
use unicase::UniCase;
struct TestParamStruct {
data: HashMap<UniCase<String>, String>,
}
impl RequestParamData for TestParamStruct {
fn get(&self) -> &HashMap<UniCase<String>, String> {
&self.data
}
}
struct TestBodyStruct {
data: Value,
}
impl RequestBodyData for TestBodyStruct {
fn get(&self) -> &Value {
&self.data
}
}
#[test]
fn test_validate_request_header_params_no_parameters() {
let validator = OpenApiPayloadValidator::new(json!({
"openapi": "3.1.0"
}))
.unwrap();
let operation = Operation {
data: json!({}),
path: JsonPath::new(),
};
let headers = TestParamStruct {
data: HashMap::new(),
};
let result = validator.validate_request_header_params(&operation, &headers);
assert!(result.is_ok());
}
#[test]
fn test_validate_request_header_params_required_parameter_present() {
let validator = OpenApiPayloadValidator::new(json!({
"openapi": "3.1.0"
}))
.unwrap();
let operation = Operation {
data: json!({
"parameters": [
{
"name": "Authorization",
"in": "header",
"required": true,
"schema": {
"type": "string"
}
}
]
}),
path: JsonPath::new(),
};
let mut headers = HashMap::new();
headers.insert(
UniCase::from("Authorization".to_string()),
"Bearer token".to_string(),
);
let headers_struct = TestParamStruct { data: headers };
let result = validator.validate_request_header_params(&operation, &headers_struct);
assert!(result.is_ok());
}
#[test]
fn test_validate_request_header_params_required_parameter_missing() {
let validator = OpenApiPayloadValidator::new(json!({
"openapi": "3.1.0"
}))
.unwrap();
let operation = Operation {
data: json!({
"parameters": [
{
"name": "Authorization",
"in": "header",
"required": true,
"schema": {
"type": "string"
}
}
]
}),
path: JsonPath::new(),
};
let headers: HashMap<UniCase<String>, String> = HashMap::new();
let headers_struct = TestParamStruct { data: headers };
let result = validator.validate_request_header_params(&operation, &headers_struct);
assert!(result.is_err());
if let Err(ValidationError::RequiredParameterMissing) = result {
assert!(true, "Expected error")
} else {
panic!("Expected ValidationError::RequiredParameterMissing");
}
}
#[test]
fn test_validate_request_header_params_optional_parameter_missing() {
let validator = OpenApiPayloadValidator::new(json!({
"openapi": "3.1.0"
}))
.unwrap();
let operation = Operation {
data: json!({
"parameters": [
{
"name": "X-Optional-Header",
"in": "header",
"required": false,
"schema": {
"type": "string"
}
}
]
}),
path: JsonPath::new(),
};
let headers: HashMap<UniCase<String>, String> = HashMap::new();
let headers_struct = TestParamStruct { data: headers };
let result = validator.validate_request_header_params(&operation, &headers_struct);
assert!(result.is_ok());
}
#[test]
fn test_validate_request_header_params_invalid_schema_structure() {
let validator = OpenApiPayloadValidator::new(json!({
"openapi": "3.1.0"
}))
.unwrap();
let operation = Operation {
data: json!({
"parameters": [
{
"required": true,
"in": "header",
"schema": {
"type": "string"
}
}
]
}),
path: JsonPath::new(),
};
let headers: HashMap<UniCase<String>, String> = HashMap::new();
let headers_struct = TestParamStruct { data: headers };
let result = validator.validate_request_header_params(&operation, &headers_struct);
assert!(result.is_err());
if let Err(ValidationError::FieldMissing) = result {
assert!(true, "Expected error")
} else {
panic!("Expected ValidationError::FieldMissing");
}
}
#[test]
fn test_validate_request_header_params_multiple_parameters() {
let validator = OpenApiPayloadValidator::new(json!({
"openapi": "3.1.0"
}))
.unwrap();
let operation = Operation {
data: json!({
"parameters": [
{
"name": "Authorization",
"in": "header",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "X-Optional-Header",
"in": "header",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "Content-Type",
"in": "header",
"required": true,
"schema": {
"type": "string"
}
}
]
}),
path: JsonPath::new(),
};
let mut headers = HashMap::new();
headers.insert(
UniCase::from("Authorization".to_string()),
"Bearer token".to_string(),
);
headers.insert(
UniCase::from("Content-Type".to_string()),
"application/json".to_string(),
);
let headers_struct = TestParamStruct { data: headers };
let result = validator.validate_request_header_params(&operation, &headers_struct);
assert!(result.is_ok());
}
fn create_operation(data: Value) -> Operation {
Operation {
data,
path: JsonPath::new(),
}
}
#[test]
fn test_validate_request_body_no_body_no_content_type() {
let validator = OpenApiPayloadValidator::new(json!({
"openapi": "3.1.0",
}))
.unwrap();
let operation = create_operation(json!({}));
let headers: HashMap<UniCase<String>, String> = HashMap::new();
let headers_struct = TestParamStruct { data: headers };
let result =
validator.validate_request_body(&operation, None::<&TestBodyStruct>, &headers_struct);
assert!(result.is_ok());
}
#[test]
fn test_validate_request_body_with_body_missing_content_type() {
let validator = OpenApiPayloadValidator::new(json!({
"openapi": "3.1.0",
}))
.unwrap();
let operation = create_operation(json!({}));
let headers: HashMap<UniCase<String>, String> = HashMap::new();
let headers_struct = TestParamStruct { data: headers };
let body = json!({});
let body_struct = TestBodyStruct { data: body };
let result =
validator.validate_request_body(&operation, Some(&body_struct), &headers_struct);
assert!(result.is_err());
if let Err(ValidationError::DefinitionExpected) = result {
assert!(true, "Expected error")
} else {
panic!("Expected ValidationError::RequiredParameterMissing");
}
}
#[test]
fn test_validate_request_body_no_body_with_content_type() {
let validator = OpenApiPayloadValidator::new(json!({
"openapi": "3.1.0",
}))
.unwrap();
let operation = create_operation(json!({}));
let mut headers = HashMap::new();
headers.insert(
UniCase::new("Content-Type".to_string()),
"application/json".to_string(),
);
let headers_struct = TestParamStruct { data: headers };
let result =
validator.validate_request_body(&operation, None::<&TestBodyStruct>, &headers_struct);
assert!(result.is_ok());
}
#[test]
fn test_validate_request_body_no_request_body_schema_in_spec() {
let validator = OpenApiPayloadValidator::new(json!({
"openapi": "3.1.0",
}))
.unwrap();
let operation = create_operation(json!({}));
let mut headers = HashMap::new();
headers.insert(
UniCase::new("Content-Type".to_string()),
"application/json".to_string(),
);
let headers_struct = TestParamStruct { data: headers };
let body = json!({});
let body_struct = TestBodyStruct { data: body };
let result =
validator.validate_request_body(&operation, Some(&body_struct), &headers_struct);
assert!(result.is_err());
if let Err(ValidationError::DefinitionExpected) = result {
assert!(true, "Expected error")
} else {
panic!("Expected ValidationError::DefinitionExpected");
}
}
#[test]
fn test_validate_request_body_body_matches_schema() {
let operation_json = json!({
"openapi": "3.1.0",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
}
}
}
}
});
let validator = OpenApiPayloadValidator::new(operation_json.clone()).unwrap();
let operation = create_operation(operation_json);
let mut headers = HashMap::new();
headers.insert(
UniCase::new("Content-Type".to_string()),
"application/json".to_string(),
);
let headers_struct = TestParamStruct { data: headers };
let body = json!({});
let body_struct = TestBodyStruct { data: body };
let result =
validator.validate_request_body(&operation, Some(&body_struct), &headers_struct);
assert!(result.is_ok());
}
#[test]
fn test_validate_request_body_body_does_not_match_schema() {
let operation_json = json!({
"openapi": "3.1.0",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["id"]
}
}
}
}
});
let validator = OpenApiPayloadValidator::new(operation_json.clone()).unwrap();
let operation = create_operation(operation_json);
let mut headers = HashMap::new();
headers.insert(
UniCase::new("Content-Type".to_string()),
"application/json".to_string(),
);
let headers_struct = TestParamStruct { data: headers };
let body = json!({});
let body_struct = TestBodyStruct { data: body };
let result =
validator.validate_request_body(&operation, Some(&body_struct), &headers_struct);
assert!(result.is_err());
if let Err(ValidationError::RequiredPropertyMissing) = result {
assert!(true, "Expected error")
} else {
panic!("Expected ValidationError::RequiredPropertyMissing");
}
}
#[test]
fn test_validate_request_body_missing_content_schema() {
let operation_json = json!({
"openapi": "3.1.0",
"requestBody": {}
});
let validator = OpenApiPayloadValidator::new(operation_json.clone()).unwrap();
let operation = create_operation(operation_json);
let mut headers = HashMap::new();
headers.insert(
UniCase::new("Content-Type".to_string()),
"application/json".to_string(),
);
let headers_struct = TestParamStruct { data: headers };
let body = json!({});
let body_struct = TestBodyStruct { data: body };
let result =
validator.validate_request_body(&operation, Some(&body_struct), &headers_struct);
assert!(result.is_err());
if let Err(ValidationError::FieldMissing) = result {
assert!(true, "Expected error")
} else {
panic!("Expected ValidationError::FieldMissing");
}
}
#[test]
fn test_validate_request_body_missing_schema_field() {
let operation_json = json!({
"openapi": "3.1.0",
"requestBody": {
"content": {
"application/json": {}
}
}
});
let validator = OpenApiPayloadValidator::new(operation_json.clone()).unwrap();
let operation = create_operation(operation_json);
let mut headers = HashMap::new();
headers.insert(
UniCase::new("Content-Type".to_string()),
"application/json".to_string(),
);
let headers_struct = TestParamStruct { data: headers };
let body = json!({});
let body_struct = TestBodyStruct { data: body };
let result =
validator.validate_request_body(&operation, Some(&body_struct), &headers_struct);
assert!(result.is_err());
if let Err(ValidationError::FieldMissing) = result {
assert!(true, "Expected error")
} else {
panic!("Expected ValidationError::FieldMissing");
}
}
}