use crate::types::Operation;
use crate::{
JsonPath, PATH_SEPARATOR, PATHS_FIELD, REF_FIELD,
ValidationError, ValidationErrorKind,
};
use dashmap::{DashMap, Entry};
use serde_json::{Map, Value};
use std::collections::HashSet;
use std::sync::Arc;
type TraverseResult<'a> = Result<SearchResult<'a>, ValidationError>;
#[derive(Debug)]
pub(crate) enum SearchResult<'a> {
Arc(Arc<Value>),
Ref(&'a Value),
}
impl<'a> SearchResult<'a> {
pub(crate) fn value(&'a self) -> &'a Value {
match self {
SearchResult::Arc(arc_val) => arc_val,
SearchResult::Ref(val) => val,
}
}
}
pub struct OpenApiTraverser {
specification: Value,
resolved_references: DashMap<String, Arc<Value>>,
resolved_operations: DashMap<(String, String), Arc<Operation>>,
}
impl OpenApiTraverser {
pub fn new(specification: Value) -> Self {
Self {
specification,
resolved_references: DashMap::new(),
resolved_operations: DashMap::new(),
}
}
pub fn get_operation(
&self,
request_path: &str,
request_method: &str,
) -> Result<Arc<Operation>, ValidationError> {
log::debug!("Looking for path '{request_path}' and method '{request_method}'");
let entry = self
.resolved_operations
.entry((String::from(request_path), String::from(request_method)));
match entry {
Entry::Occupied(e) => Ok(e.get().clone()),
Entry::Vacant(_) => {
if let Ok(spec_paths) = get_as_object(&self.specification, PATHS_FIELD) {
for (spec_path, spec_path_methods) in spec_paths.iter() {
let operations = require_object(spec_path_methods)?;
if let Some(operation) = operations.get(request_method) {
if Self::matches_spec_path(request_path, spec_path) {
log::debug!(
"OpenAPI path '{spec_path}' and method '{request_method}' match provided request path '{request_path}' and method '{request_method}'."
);
let mut json_path = JsonPath::new();
json_path
.add(PATHS_FIELD)
.add(spec_path)
.add(&request_method.to_lowercase());
let operation = Arc::new(Operation {
data: operation.clone(),
path: json_path,
});
if !Self::path_has_parameter(spec_path) {
self.resolved_operations.insert(
(request_path.to_string(), request_method.to_string()),
operation.clone(),
);
}
return Ok(operation);
}
}
}
}
Err(ValidationError::MissingOperation)
}
}
}
fn path_has_parameter(path: &str) -> bool {
path.contains("{") && path.contains("}")
}
fn matches_spec_path(path_to_match: &str, spec_path: &str) -> bool {
if !Self::path_has_parameter(spec_path) {
spec_path == path_to_match
} else {
let target_segments = path_to_match.split(PATH_SEPARATOR).collect::<Vec<&str>>();
let spec_segments = spec_path.split(PATH_SEPARATOR).collect::<Vec<&str>>();
if spec_segments.len() != target_segments.len() {
return false;
}
let (matching_segments, segment_count) =
spec_segments.iter().zip(target_segments.iter()).fold(
(0, 0),
|(mut matches, mut count), (spec_segment, target_segment)| {
count += 1;
if let Some(_) = spec_segment.find("{").and_then(|start| {
spec_segment
.find("}")
.map(|end| &spec_segment[start + 1..end])
}) {
matches += 1;
} else if spec_segment == target_segment {
matches += 1;
}
(matches, count)
},
);
matching_segments == segment_count
}
}
pub(crate) fn get_optional_spec_node<'a>(
&'a self,
node: &'a Value,
field: &str,
) -> Result<Option<SearchResult<'a>>, ValidationError>
where
Self: 'a,
{
log::trace!(
"Attempting to find optional field '{}' from '{}'",
field,
node.to_string()
);
match self.get_required_spec_node(node, field) {
Ok(security) => Ok(Some(security)),
Err(e) if e.kind() == ValidationErrorKind::MismatchingSchema => Ok(None),
Err(e) => Err(e),
}
}
pub(crate) fn get_required_spec_node<'a>(
&'a self,
node: &'a Value,
field: &str,
) -> Result<SearchResult<'a>, ValidationError> {
log::trace!(
"Attempting to find required field '{}' from '{}'",
field,
node.to_string()
);
let ref_result = self.resolve_possible_ref(node)?;
match ref_result {
SearchResult::Arc(val) => match val.get(field) {
None => Err(ValidationError::FieldMissing),
Some(v) => Ok(SearchResult::Arc(Arc::new(v.clone()))),
},
SearchResult::Ref(val) => match val.get(field) {
None => Err(ValidationError::FieldMissing),
Some(v) => Ok(SearchResult::Ref(v)),
},
}
}
fn resolve_possible_ref<'a>(&'a self, node: &'a Value) -> TraverseResult<'a> {
if let Ok(ref_string) = get_as_str(node, REF_FIELD) {
let entry = self.resolved_references.entry(String::from(ref_string));
return match entry {
Entry::Occupied(e) => Ok(SearchResult::Arc(e.get().clone())),
Entry::Vacant(_) => {
let mut seen_references = HashSet::new();
let res = self.get_reference_path(ref_string, &mut seen_references)?;
return Ok(res);
}
};
}
Ok(SearchResult::Ref(node))
}
fn get_reference_path<'a, 'b>(
&'a self,
ref_string: &str,
seen_references: &mut HashSet<String>,
) -> TraverseResult<'b>
where
'a: 'b,
{
if seen_references.contains(ref_string) {
return Err(ValidationError::CircularReference);
}
seen_references.insert(String::from(ref_string));
let path = ref_string
.split(PATH_SEPARATOR)
.filter(|node| !(*node).is_empty() && (*node != "#"))
.collect::<Vec<&str>>()
.join("/");
let current_schema = match &self.specification.pointer(&path) {
None => {
return Err(ValidationError::FieldMissing);
}
Some(v) => self.resolve_possible_ref(v)?,
};
Ok(current_schema)
}
}
pub(crate) fn get_as_bool<'a, 'b>(node: &'a Value, field: &str) -> Result<bool, ValidationError>
where
'a: 'b,
{
log::trace!("Grabbing {} from {} as a bool.", field, node.to_string());
match node.get(field) {
None => Err(ValidationError::FieldMissing),
Some(found) => require_bool(found),
}
}
pub(crate) fn get_as_any<'a, 'b>(node: &'a Value, field: &str) -> Result<&'b Value, ValidationError>
where
'a: 'b,
{
log::trace!("Grabbing {} from {} as a str.", field, node.to_string());
match node.get(field) {
None => Err(ValidationError::FieldMissing),
Some(found) => Ok(found),
}
}
pub(crate) fn get_as_str<'a, 'b>(node: &'a Value, field: &str) -> Result<&'b str, ValidationError>
where
'a: 'b,
{
log::trace!("Grabbing {} from {} as a str.", field, node.to_string());
match node.get(field) {
None => Err(ValidationError::FieldMissing),
Some(found) => require_str(found),
}
}
pub(crate) fn get_as_array<'a, 'b>(
node: &'a Value,
field: &str,
) -> Result<&'b Vec<Value>, ValidationError>
where
'a: 'b,
{
log::trace!("Grabbing {} from {} as an array.", field, node.to_string());
match node.get(field) {
None => Err(ValidationError::FieldMissing),
Some(found) => require_array(found),
}
}
pub(crate) fn get_as_object<'a, 'b>(
node: &'a Value,
field: &str,
) -> Result<&'b Map<String, Value>, ValidationError>
where
'a: 'b,
{
log::trace!("Grabbing {} from {} as an object.", field, node.to_string());
match node.get(field) {
None => Err(ValidationError::FieldMissing),
Some(found) => require_object(found),
}
}
pub(crate) fn require_bool<'a, 'b>(node: &'a Value) -> Result<bool, ValidationError>
where
'a: 'b,
{
match node.as_bool() {
None => Err(ValidationError::UnexpectedType),
Some(bool) => Ok(bool),
}
}
pub(crate) fn require_str<'a, 'b>(node: &'a Value) -> Result<&'b str, ValidationError>
where
'a: 'b,
{
match node.as_str() {
None => Err(ValidationError::UnexpectedType),
Some(string) => Ok(string),
}
}
pub(crate) fn require_object<'a, 'b>(
node: &'a Value,
) -> Result<&'b Map<String, Value>, ValidationError>
where
'a: 'b,
{
match node.as_object() {
None => Err(ValidationError::UnexpectedType),
Some(map) => Ok(map),
}
}
pub(crate) fn require_array<'a, 'b>(node: &'a Value) -> Result<&'b Vec<Value>, ValidationError>
where
'a: 'b,
{
match node.as_array() {
None => Err(ValidationError::UnexpectedType),
Some(array) => Ok(array),
}
}