jsonschema 0.26.2

JSON schema validaton library
Documentation
use crate::{
    compiler, ecma,
    error::ValidationError,
    keywords::CompilationResult,
    paths::{LazyLocation, Location},
    primitive_type::PrimitiveType,
    validator::Validate,
};
use ahash::AHashMap;
use once_cell::sync::Lazy;
use serde_json::{Map, Value};

use std::{collections::VecDeque, sync::Mutex};

static REGEX_CACHE: Lazy<Mutex<LruCache>> = Lazy::new(|| Mutex::new(LruCache::new(10)));

struct LruCache {
    map: AHashMap<String, fancy_regex::Regex>,
    queue: VecDeque<String>,
    capacity: usize,
}

impl LruCache {
    fn new(capacity: usize) -> Self {
        LruCache {
            map: AHashMap::new(),
            queue: VecDeque::new(),
            capacity,
        }
    }

    fn get(&mut self, key: &str) -> Option<&fancy_regex::Regex> {
        if let Some(value) = self.map.get(key) {
            let index = self.queue.iter().position(|x| x == key).unwrap();
            let k = self.queue.remove(index).unwrap();
            self.queue.push_back(k);
            Some(value)
        } else {
            None
        }
    }

    fn insert(&mut self, key: String, value: fancy_regex::Regex) -> Option<fancy_regex::Regex> {
        if self.map.len() >= self.capacity && !self.map.contains_key(&key) {
            if let Some(lru_key) = self.queue.pop_front() {
                self.map.remove(&lru_key);
            }
        }

        let old_value = self.map.insert(key.clone(), value);
        if old_value.is_some() {
            let index = self.queue.iter().position(|x| x == &key).unwrap();
            self.queue.remove(index);
        }
        self.queue.push_back(key);
        old_value
    }
}

pub(crate) struct PatternValidator {
    original: String,
    pattern: fancy_regex::Regex,
    location: Location,
}

impl PatternValidator {
    #[inline]
    pub(crate) fn compile<'a>(
        ctx: &compiler::Context,
        pattern: &'a Value,
    ) -> CompilationResult<'a> {
        match pattern {
            Value::String(item) => {
                let mut cache = REGEX_CACHE.lock().expect("Lock is poisoned");
                let pattern = if let Some(regex) = cache.get(item) {
                    regex.clone()
                } else {
                    let regex = match ecma::to_rust_regex(item)
                        .map(|pattern| fancy_regex::Regex::new(&pattern))
                    {
                        Ok(Ok(r)) => r,
                        _ => {
                            return Err(ValidationError::format(
                                Location::new(),
                                ctx.location().clone(),
                                pattern,
                                "regex",
                            ))
                        }
                    };
                    cache.insert(item.clone(), regex.clone());
                    regex
                };
                Ok(Box::new(PatternValidator {
                    original: item.clone(),
                    pattern,
                    location: ctx.location().join("pattern"),
                }))
            }
            _ => Err(ValidationError::single_type_error(
                Location::new(),
                ctx.location().clone(),
                pattern,
                PrimitiveType::String,
            )),
        }
    }
}

impl Validate for PatternValidator {
    fn validate<'i>(
        &self,
        instance: &'i Value,
        location: &LazyLocation,
    ) -> Result<(), ValidationError<'i>> {
        if let Value::String(item) = instance {
            match self.pattern.is_match(item) {
                Ok(is_match) => {
                    if !is_match {
                        return Err(ValidationError::pattern(
                            self.location.clone(),
                            location.into(),
                            instance,
                            self.original.clone(),
                        ));
                    }
                }
                Err(e) => {
                    return Err(ValidationError::backtrack_limit(
                        self.location.clone(),
                        location.into(),
                        instance,
                        e,
                    ));
                }
            }
        }
        Ok(())
    }

    fn is_valid(&self, instance: &Value) -> bool {
        if let Value::String(item) = instance {
            return self.pattern.is_match(item).unwrap_or(false);
        }
        true
    }
}

#[inline]
pub(crate) fn compile<'a>(
    ctx: &compiler::Context,
    _: &'a Map<String, Value>,
    schema: &'a Value,
) -> Option<CompilationResult<'a>> {
    Some(PatternValidator::compile(ctx, schema))
}

#[cfg(test)]
mod tests {
    use crate::tests_util;
    use serde_json::json;
    use test_case::test_case;

    #[test_case("^(?!eo:)", "eo:bands", false)]
    #[test_case("^(?!eo:)", "proj:epsg", true)]
    fn negative_lookbehind_match(pattern: &str, text: &str, is_matching: bool) {
        let text = json!(text);
        let schema = json!({"pattern": pattern});
        let validator = crate::validator_for(&schema).unwrap();
        assert_eq!(validator.is_valid(&text), is_matching)
    }

    #[test]
    fn location() {
        tests_util::assert_schema_location(&json!({"pattern": "^f"}), &json!("b"), "/pattern")
    }
}