use fancy_regex::Regex;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::rc::Rc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IntegerBounds {
lower: Option<i64>,
upper: Option<i64>,
}
impl IntegerBounds {
#[must_use]
pub const fn unbounded() -> Self {
Self {
lower: None,
upper: None,
}
}
#[must_use]
pub const fn lower(self) -> Option<i64> {
self.lower
}
#[must_use]
pub const fn upper(self) -> Option<i64> {
self.upper
}
#[must_use]
pub fn new(lower: Option<i64>, upper: Option<i64>) -> Option<Self> {
if let (Some(lower), Some(upper)) = (lower, upper)
&& lower > upper
{
return None;
}
Some(Self { lower, upper })
}
#[must_use]
pub fn contains_i64(self, value: i64) -> bool {
self.lower.is_none_or(|lower| value >= lower)
&& self.upper.is_none_or(|upper| value <= upper)
}
#[must_use]
pub fn contains_i128(self, value: i128) -> bool {
self.lower.is_none_or(|lower| value >= i128::from(lower))
&& self.upper.is_none_or(|upper| value <= i128::from(upper))
}
#[must_use]
pub fn contains_bounds(self, sub: Self) -> bool {
self.lower <= sub.lower
&& match (self.upper, sub.upper) {
(None, _) => true,
(Some(_), None) => false,
(Some(sup_upper), Some(sub_upper)) => sub_upper <= sup_upper,
}
}
#[must_use]
pub fn as_number_bounds(self) -> NumberBounds {
NumberBounds::new(
self.lower.map_or(NumberBound::Unbounded, |value| {
NumberBound::Inclusive(value as f64)
}),
self.upper.map_or(NumberBound::Unbounded, |value| {
NumberBound::Inclusive(value as f64)
}),
)
.expect("finite i64 endpoints must project to valid f64 number bounds")
}
pub(crate) fn from_json_schema_keywords(
minimum: Option<i64>,
exclusive_minimum: Option<i64>,
maximum: Option<i64>,
exclusive_maximum: Option<i64>,
) -> Option<Self> {
let lower = if let Some(bound) = exclusive_minimum {
Some(bound.checked_add(1)?)
} else {
minimum
};
let upper = if let Some(bound) = exclusive_maximum {
Some(bound.checked_sub(1)?)
} else {
maximum
};
Self::new(lower, upper)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum NumberBound {
Unbounded,
Inclusive(f64),
Exclusive(f64),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NumberBounds {
lower: NumberBound,
upper: NumberBound,
}
impl NumberBounds {
#[must_use]
pub const fn unbounded() -> Self {
Self {
lower: NumberBound::Unbounded,
upper: NumberBound::Unbounded,
}
}
#[must_use]
pub fn new(lower: NumberBound, upper: NumberBound) -> Option<Self> {
if !number_bound_is_finite(lower) || !number_bound_is_finite(upper) {
return None;
}
if let (Some((lower_value, lower_inclusive)), Some((upper_value, upper_inclusive))) =
(number_bound_value(lower), number_bound_value(upper))
{
match lower_value.partial_cmp(&upper_value)? {
std::cmp::Ordering::Greater => return None,
std::cmp::Ordering::Equal if !(lower_inclusive && upper_inclusive) => return None,
std::cmp::Ordering::Less | std::cmp::Ordering::Equal => {}
}
}
Some(Self { lower, upper })
}
#[must_use]
pub const fn lower(self) -> NumberBound {
self.lower
}
#[must_use]
pub const fn upper(self) -> NumberBound {
self.upper
}
#[must_use]
pub fn contains(self, value: f64) -> bool {
number_lower_bound_contains(self.lower, value)
&& number_upper_bound_contains(self.upper, value)
}
#[must_use]
pub fn contains_bounds(self, sub: Self) -> bool {
number_lower_bound_is_at_most(self.lower, sub.lower)
&& number_upper_bound_is_at_least(self.upper, sub.upper)
}
}
fn number_bound_is_finite(bound: NumberBound) -> bool {
match bound {
NumberBound::Unbounded => true,
NumberBound::Inclusive(value) | NumberBound::Exclusive(value) => value.is_finite(),
}
}
fn number_bound_value(bound: NumberBound) -> Option<(f64, bool)> {
match bound {
NumberBound::Unbounded => None,
NumberBound::Inclusive(value) => Some((value, true)),
NumberBound::Exclusive(value) => Some((value, false)),
}
}
fn number_lower_bound_contains(bound: NumberBound, value: f64) -> bool {
match bound {
NumberBound::Unbounded => true,
NumberBound::Inclusive(bound) => value >= bound,
NumberBound::Exclusive(bound) => value > bound,
}
}
fn number_upper_bound_contains(bound: NumberBound, value: f64) -> bool {
match bound {
NumberBound::Unbounded => true,
NumberBound::Inclusive(bound) => value <= bound,
NumberBound::Exclusive(bound) => value < bound,
}
}
fn number_lower_bound_is_at_most(sup: NumberBound, sub: NumberBound) -> bool {
match (sup, sub) {
(NumberBound::Unbounded, _) => true,
(_, NumberBound::Unbounded) => false,
(NumberBound::Inclusive(sup), NumberBound::Inclusive(sub))
| (NumberBound::Inclusive(sup), NumberBound::Exclusive(sub))
| (NumberBound::Exclusive(sup), NumberBound::Exclusive(sub)) => sub >= sup,
(NumberBound::Exclusive(sup), NumberBound::Inclusive(sub)) => sub > sup,
}
}
fn number_upper_bound_is_at_least(sup: NumberBound, sub: NumberBound) -> bool {
match (sup, sub) {
(NumberBound::Unbounded, _) => true,
(_, NumberBound::Unbounded) => false,
(NumberBound::Inclusive(sup), NumberBound::Inclusive(sub))
| (NumberBound::Inclusive(sup), NumberBound::Exclusive(sub))
| (NumberBound::Exclusive(sup), NumberBound::Exclusive(sub)) => sub <= sup,
(NumberBound::Exclusive(sup), NumberBound::Inclusive(sub)) => sub < sup,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CountRange<T> {
min: T,
max: Option<T>,
}
impl<T: Copy + Ord> CountRange<T> {
#[must_use]
pub const fn unbounded_from(min: T) -> Self {
Self { min, max: None }
}
#[must_use]
pub fn new(min: T, max: Option<T>) -> Option<Self> {
if max.is_some_and(|max| min > max) {
return None;
}
Some(Self { min, max })
}
#[must_use]
pub const fn min(self) -> T {
self.min
}
#[must_use]
pub const fn max(self) -> Option<T> {
self.max
}
#[must_use]
pub fn contains(self, value: T) -> bool {
value >= self.min && self.max.is_none_or(|max| value <= max)
}
#[must_use]
pub fn contains_range(self, sub: Self) -> bool {
self.min <= sub.min
&& match (self.max, sub.max) {
(None, _) => true,
(Some(_), None) => false,
(Some(sup_max), Some(sub_max)) => sub_max <= sup_max,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PatternSupport {
Supported,
Unsupported,
}
#[derive(Clone)]
pub struct PatternConstraint {
source: String,
support: PatternSupport,
matcher: Option<Rc<Regex>>,
}
impl PatternConstraint {
pub(crate) fn new(source: String) -> Self {
let matcher = Regex::new(&source).ok().map(Rc::new);
let support = if matcher.is_some() {
PatternSupport::Supported
} else {
PatternSupport::Unsupported
};
Self {
source,
support,
matcher,
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.source
}
#[must_use]
pub const fn support(&self) -> PatternSupport {
self.support
}
#[must_use]
pub fn is_match(&self, candidate: &str) -> bool {
match self.support {
PatternSupport::Supported => self
.matcher
.as_ref()
.is_some_and(|regex| regex.is_match(candidate).unwrap_or(false)),
PatternSupport::Unsupported => false,
}
}
}
impl fmt::Debug for PatternConstraint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("PatternConstraint")
.field(&self.source)
.finish()
}
}
impl PartialEq for PatternConstraint {
fn eq(&self, other: &Self) -> bool {
self.source == other.source
}
}
impl Eq for PatternConstraint {}
impl Hash for PatternConstraint {
fn hash<H: Hasher>(&self, state: &mut H) {
self.source.hash(state);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PatternProperty<Node> {
pub pattern: PatternConstraint,
pub schema: Node,
}
impl<Node> PatternProperty<Node> {
#[must_use]
pub fn new(pattern: PatternConstraint, schema: Node) -> Self {
Self { pattern, schema }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContainsConstraint<Node> {
pub schema: Node,
count: CountRange<u64>,
}
impl<Node> ContainsConstraint<Node> {
#[must_use]
pub fn new(schema: Node, count: CountRange<u64>) -> Self {
Self { schema, count }
}
#[must_use]
pub const fn count(&self) -> CountRange<u64> {
self.count
}
}