oasert 0.1.3

A library for verifying in-flight requests against a provided OpenAPI 3.1.x or 3.0.x specification.
Documentation
use std::fmt::{Display, Formatter};
use std::sync::{Arc, OnceLock};
use dashmap::{DashMap, VacantEntry};
use serde_json::Value;
use crate::validator::{OpenApiPayloadValidator, ValidationError};

/// Global instance of the validator cache.
///
/// This provides a singleton instance that can be accessed from anywhere in the application.
// Global singleton instance of the validator cache
static GLOBAL_CACHE: OnceLock<ValidatorCache> = OnceLock::new();

/// Gets the global validator cache instance.
///
/// The cache is created on first access and reused for subsequent calls.
pub fn global_validator_cache() -> &'static ValidatorCache {
    GLOBAL_CACHE.get_or_init(ValidatorCache::new)
}


/// Error types for cache operations
#[derive(Debug)]
pub enum CacheError {
    /// The validator with the specified ID was not found in the cache
    ValidatorNotFound,
    /// The validator with the specified ID already exists in the cache
    ValidatorAlreadyExists,
    /// Attempted to create a new validator but failed.
    FailedToCreateValidator(ValidationError)
}

impl Display for CacheError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            CacheError::ValidatorNotFound => write!(f, "Validator not found in cache"),
            CacheError::ValidatorAlreadyExists => write!(f, "Validator already exists in cache"),
            CacheError::FailedToCreateValidator(err) => write!(f, "Failed to create new validator: {}", err)
        }
    }
}

impl std::error::Error for CacheError {}

/// A global cache for OpenApiPayloadValidator instances.
///
/// This cache provides thread-safe storage and retrieval of validators
/// by their string identifiers using DashMap for concurrent access.
#[derive(Default)]
pub struct ValidatorCache {
    cache: DashMap<String, Arc<OpenApiPayloadValidator>>,
}

impl ValidatorCache {
    /// Creates a new empty validator cache
    pub fn new() -> Self {
        ValidatorCache {
            cache: DashMap::new(),
        }
    }

    /// Inserts a validator into the cache with the given ID.
    ///
    /// If a validator with the same ID already exists, returns an error.
    ///
    /// # Arguments
    /// * `id` - A string identifier for the validator
    /// * `validator` - The OpenApiPayloadValidator to store
    ///
    /// # Returns
    /// * `Ok(())` - If the validator was successfully inserted
    /// * `Err(CacheError)` - If a validator with the same ID already exists
    pub fn insert(&self, id: String, spec: Value) -> Result<Arc<OpenApiPayloadValidator>, CacheError> {
        match self.cache.entry(id) {
            dashmap::mapref::entry::Entry::Occupied(_) => {
                Err(CacheError::ValidatorAlreadyExists)
            },
            dashmap::mapref::entry::Entry::Vacant(entry) => {
                Self::create_validator(entry, spec)
            }
        }
    }
    
    fn create_validator(entry: VacantEntry<String, Arc<OpenApiPayloadValidator>>, spec: Value) -> Result<Arc<OpenApiPayloadValidator>, CacheError> {
        match OpenApiPayloadValidator::new(spec) {
            Ok(validator) => {
                log::debug!("Added validator to cache with ID: {}", entry.key());
                let validator = Arc::new(validator);
                entry.insert(validator.clone());

                Ok(validator)
            },
            Err(e) => {
                log::error!("Failed to create validator for ID {}: {}", entry.key(), e);
                Err(CacheError::FailedToCreateValidator(e))
            }
        }
    }

    /// Inserts or replaces a validator in the cache with the given ID.
    ///
    /// # Arguments
    /// * `id` - A string identifier for the validator
    /// * `validator` - The OpenApiPayloadValidator to store
    pub fn insert_or_replace(&self, id: String, validator: OpenApiPayloadValidator) {
        self.cache.insert(id, Arc::new(validator));
    }

    /// Retrieves a validator from the cache by its ID.
    ///
    /// # Arguments
    /// * `id` - The string identifier of the validator to retrieve
    ///
    /// # Returns
    /// * `Ok(Arc<OpenApiPayloadValidator>)` - A reference-counted pointer to the validator if found
    /// * `Err(CacheError)` - If no validator with the given ID exists in the cache
    pub fn get(&self, id: &str) -> Result<Arc<OpenApiPayloadValidator>, CacheError> {
        match self.cache.get(id) {
            Some(validator) => Ok(Arc::clone(validator.value())),
            None => Err(CacheError::ValidatorNotFound),
        }
    }

    /// Removes a validator from the cache by its ID.
    ///
    /// # Arguments
    /// * `id` - The string identifier of the validator to remove
    ///
    /// # Returns
    /// * `Ok(())` - If the validator was successfully removed
    /// * `Err(CacheError)` - If no validator with the given ID exists in the cache
    pub fn remove(&self, id: &str) -> Result<(), CacheError> {
        if self.cache.remove(id).is_none() {
            return Err(CacheError::ValidatorNotFound);
        }
        Ok(())
    }

    /// Gets a validator from the cache if it exists, or creates and caches a new one.
    ///
    /// # Arguments
    ///
    /// * `id` - The string identifier for the validator
    /// * `spec` - The OpenAPI specification as a JSON Value, used only if the validator isn't in the cache
    ///
    /// # Returns
    ///
    /// * `Result<Arc<OpenApiPayloadValidator>, ValidationError>` - The validator on success,
    ///   or a ValidationError if a new validator couldn't be created
    pub fn get_or_insert(&self, id: String, spec: Value) -> Result<Arc<OpenApiPayloadValidator>, CacheError> {
        match self.cache.entry(id) {
            dashmap::mapref::entry::Entry::Occupied(entry) => {
                Ok(Arc::clone(entry.get()))
            },
            dashmap::mapref::entry::Entry::Vacant(entry) => {
                Self::create_validator(entry, spec)
            }
        }
    }


    /// Checks if a validator with the given ID exists in the cache.
    ///
    /// # Arguments
    /// * `id` - The string identifier to check
    ///
    /// # Returns
    /// * `true` if a validator with the given ID exists in the cache, `false` otherwise
    pub fn contains(&self, id: &str) -> bool {
        self.cache.contains_key(id)
    }

    /// Returns the number of validators in the cache.
    pub fn len(&self) -> usize {
        self.cache.len()
    }

    /// Checks if the cache is empty.
    pub fn is_empty(&self) -> bool {
        self.cache.is_empty()
    }

    /// Clears all validators from the cache.
    pub fn clear(&self) {
        self.cache.clear();
        log::debug!("Cleared validator cache");
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn test_cache_get_insert() {
        let cache = ValidatorCache::new();
        assert!(cache.get("test").is_err());
        let spec = json!({
            "openapi": "3.1.0"
        });
        let validator = cache.insert("test".to_string(), spec).unwrap();
        assert!(!cache.is_empty());
        assert_eq!(cache.len(), 1);
        let cached = cache.get("test").unwrap();
        assert!(Arc::ptr_eq(&validator, &cached));
    }

    #[test]
    fn test_cache_get_or_insert() {
        let cache = ValidatorCache::new();
        assert!(cache.get("test").is_err());
        let spec = json!({
            "openapi": "3.1.0"
        });
        let validator1 = cache.get_or_insert("test".to_string(), spec.clone()).unwrap();
        let validator2 = cache.get_or_insert("test".to_string(), json!({"openapi": "3.0.0"})).unwrap();
        assert!(Arc::ptr_eq(&validator1, &validator2));
        assert_eq!(cache.len(), 1);
    }

    #[test]
    fn test_cache_clear() {
        let cache = ValidatorCache::new();
        let spec = json!({
            "openapi": "3.1.0"
        });
        cache.insert("test1".to_string(), spec.clone()).unwrap();
        cache.insert("test2".to_string(), spec.clone()).unwrap();
        cache.insert("test3".to_string(), spec).unwrap();
        assert_eq!(cache.len(), 3);
        cache.clear();
        assert!(cache.is_empty());
    }

    #[test]
    fn test_global_cache() {
        let cache = global_validator_cache();
        cache.clear();
        let spec = json!({
            "openapi": "3.1.0"
        });
        cache.insert("global_test".to_string(), spec).unwrap();
        let same_cache = global_validator_cache();
        assert!(same_cache.get("global_test").is_ok());
        cache.clear();
    }
}