use std::str::FromStr;
use regex::Regex;
use serde::de::Error as DeError;
use serde::ser::Serializer;
use serde::{Deserialize, Deserializer, Serialize};
use crate::error::WyrdError;
pub const MAX_REGEX_PATTERN_LEN: usize = 4096;
fn validate_regex_pattern(pattern: &str) -> Result<(), WyrdError> {
if pattern.len() > MAX_REGEX_PATTERN_LEN {
return Err(WyrdError::Validation {
message: format!(
"regex pattern length {} exceeds MAX_REGEX_PATTERN_LEN {MAX_REGEX_PATTERN_LEN}",
pattern.len()
),
details: serde_json::Value::Null,
});
}
Regex::new(pattern).map_err(|e| WyrdError::Validation {
message: format!("regex pattern is invalid: {e}"),
details: serde_json::Value::Null,
})?;
Ok(())
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub enum ComparisonOperator {
Equals,
NotEquals,
GreaterThan,
GreaterThanOrEquals,
LessThan,
LessThanOrEquals,
InRange {
min: f64,
max: f64,
inclusive: bool,
},
NotInRange {
min: f64,
max: f64,
inclusive: bool,
},
ApproximatelyEquals {
tolerance: f64,
},
IsPositive,
IsNegative,
IsZero,
Contains,
NotContains,
ContainsIgnoreCase,
StartsWith,
EndsWith,
MatchesRegex {
pattern: String,
},
NotMatchesRegex {
pattern: String,
},
IsEmail,
IsUrl,
IsUuid,
IsIpv4,
IsIpv6,
HasMinLength {
min: usize,
},
HasMaxLength {
max: usize,
},
IsJson,
In,
NotIn,
IsSubset,
IsSuperset,
IsDisjoint,
AllOf,
AnyOf,
NoneOf,
IsEmpty,
IsNonEmpty,
Length {
expected: usize,
},
LengthGreaterThan {
min: usize,
},
LengthLessThan {
max: usize,
},
UniqueValues,
IsTruthy,
IsFalsy,
IsNull,
IsNotNull,
IsType {
expected: JsonValueType,
},
IsString,
IsNumber,
IsBoolean,
IsObject,
WithinAbsTolerance {
tolerance: f64,
},
WithinPctTolerance {
pct: f64,
},
WithinStdDev {
sigma: f64,
mean: f64,
std_dev: f64,
},
BetweenPercentiles {
lower_pct: f64,
upper_pct: f64,
},
DivergenceLessThan {
metric: DivergenceMetric,
threshold: f64,
},
CosineSimilarityAtLeast {
threshold: f64,
},
}
impl Serialize for ComparisonOperator {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Some(parameterized) = ParameterizedComparisonOperator::from_operator(self) {
parameterized.serialize(serializer)
} else {
serializer.serialize_str(self.discriminator())
}
}
}
impl<'de> Deserialize<'de> for ComparisonOperator {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
match value {
serde_json::Value::String(discriminator) => {
Self::from_discriminator_parameterless(&discriminator).map_err(D::Error::custom)
}
serde_json::Value::Object(_) => {
let parameterized = ParameterizedComparisonOperator::deserialize(value)
.map_err(D::Error::custom)?;
Ok(parameterized.into())
}
other => Err(D::Error::custom(format!(
"operator must be a scalar discriminator or parameterized object; got {other}"
))),
}
}
}
impl schemars::JsonSchema for ComparisonOperator {
fn schema_name() -> String {
"ComparisonOperator".to_string()
}
fn json_schema(schema_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
<ComparisonOperatorWireSchema as schemars::JsonSchema>::json_schema(schema_gen)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
enum ParameterizedComparisonOperator {
InRange {
min: f64,
max: f64,
inclusive: bool,
},
NotInRange {
min: f64,
max: f64,
inclusive: bool,
},
ApproximatelyEquals {
tolerance: f64,
},
MatchesRegex {
pattern: String,
},
NotMatchesRegex {
pattern: String,
},
HasMinLength {
min: usize,
},
HasMaxLength {
max: usize,
},
Length {
expected: usize,
},
LengthGreaterThan {
min: usize,
},
LengthLessThan {
max: usize,
},
IsType {
expected: JsonValueType,
},
WithinAbsTolerance {
tolerance: f64,
},
WithinPctTolerance {
pct: f64,
},
WithinStdDev {
sigma: f64,
mean: f64,
std_dev: f64,
},
BetweenPercentiles {
lower_pct: f64,
upper_pct: f64,
},
DivergenceLessThan {
metric: DivergenceMetric,
threshold: f64,
},
CosineSimilarityAtLeast {
threshold: f64,
},
}
impl ParameterizedComparisonOperator {
fn from_operator(operator: &ComparisonOperator) -> Option<Self> {
use ComparisonOperator::{
AllOf, AnyOf, ApproximatelyEquals, BetweenPercentiles, Contains, ContainsIgnoreCase,
CosineSimilarityAtLeast, DivergenceLessThan, EndsWith, Equals, GreaterThan,
GreaterThanOrEquals, HasMaxLength, HasMinLength, In, InRange, IsBoolean, IsDisjoint,
IsEmail, IsEmpty, IsFalsy, IsIpv4, IsIpv6, IsJson, IsNegative, IsNonEmpty, IsNotNull,
IsNull, IsNumber, IsObject, IsPositive, IsString, IsSubset, IsSuperset, IsTruthy,
IsType, IsUrl, IsUuid, IsZero, Length, LengthGreaterThan, LengthLessThan, LessThan,
LessThanOrEquals, MatchesRegex, NoneOf, NotContains, NotEquals, NotIn, NotInRange,
NotMatchesRegex, StartsWith, UniqueValues, WithinAbsTolerance, WithinPctTolerance,
WithinStdDev,
};
Some(match operator {
InRange {
min,
max,
inclusive,
} => Self::InRange {
min: *min,
max: *max,
inclusive: *inclusive,
},
NotInRange {
min,
max,
inclusive,
} => Self::NotInRange {
min: *min,
max: *max,
inclusive: *inclusive,
},
ApproximatelyEquals { tolerance } => Self::ApproximatelyEquals {
tolerance: *tolerance,
},
MatchesRegex { pattern } => Self::MatchesRegex {
pattern: pattern.clone(),
},
NotMatchesRegex { pattern } => Self::NotMatchesRegex {
pattern: pattern.clone(),
},
HasMinLength { min } => Self::HasMinLength { min: *min },
HasMaxLength { max } => Self::HasMaxLength { max: *max },
Length { expected } => Self::Length {
expected: *expected,
},
LengthGreaterThan { min } => Self::LengthGreaterThan { min: *min },
LengthLessThan { max } => Self::LengthLessThan { max: *max },
IsType { expected } => Self::IsType {
expected: *expected,
},
WithinAbsTolerance { tolerance } => Self::WithinAbsTolerance {
tolerance: *tolerance,
},
WithinPctTolerance { pct } => Self::WithinPctTolerance { pct: *pct },
WithinStdDev {
sigma,
mean,
std_dev,
} => Self::WithinStdDev {
sigma: *sigma,
mean: *mean,
std_dev: *std_dev,
},
BetweenPercentiles {
lower_pct,
upper_pct,
} => Self::BetweenPercentiles {
lower_pct: *lower_pct,
upper_pct: *upper_pct,
},
DivergenceLessThan { metric, threshold } => Self::DivergenceLessThan {
metric: *metric,
threshold: *threshold,
},
CosineSimilarityAtLeast { threshold } => Self::CosineSimilarityAtLeast {
threshold: *threshold,
},
Equals | NotEquals | GreaterThan | GreaterThanOrEquals | LessThan
| LessThanOrEquals | IsPositive | IsNegative | IsZero | Contains | NotContains
| ContainsIgnoreCase | StartsWith | EndsWith | IsEmail | IsUrl | IsUuid | IsIpv4
| IsIpv6 | IsJson | In | NotIn | IsSubset | IsSuperset | IsDisjoint | AllOf | AnyOf
| NoneOf | IsEmpty | IsNonEmpty | UniqueValues | IsTruthy | IsFalsy | IsNull
| IsNotNull | IsString | IsNumber | IsBoolean | IsObject => return None,
})
}
}
impl From<ParameterizedComparisonOperator> for ComparisonOperator {
fn from(operator: ParameterizedComparisonOperator) -> Self {
match operator {
ParameterizedComparisonOperator::InRange {
min,
max,
inclusive,
} => Self::InRange {
min,
max,
inclusive,
},
ParameterizedComparisonOperator::NotInRange {
min,
max,
inclusive,
} => Self::NotInRange {
min,
max,
inclusive,
},
ParameterizedComparisonOperator::ApproximatelyEquals { tolerance } => {
Self::ApproximatelyEquals { tolerance }
}
ParameterizedComparisonOperator::MatchesRegex { pattern } => {
Self::MatchesRegex { pattern }
}
ParameterizedComparisonOperator::NotMatchesRegex { pattern } => {
Self::NotMatchesRegex { pattern }
}
ParameterizedComparisonOperator::HasMinLength { min } => Self::HasMinLength { min },
ParameterizedComparisonOperator::HasMaxLength { max } => Self::HasMaxLength { max },
ParameterizedComparisonOperator::Length { expected } => Self::Length { expected },
ParameterizedComparisonOperator::LengthGreaterThan { min } => {
Self::LengthGreaterThan { min }
}
ParameterizedComparisonOperator::LengthLessThan { max } => Self::LengthLessThan { max },
ParameterizedComparisonOperator::IsType { expected } => Self::IsType { expected },
ParameterizedComparisonOperator::WithinAbsTolerance { tolerance } => {
Self::WithinAbsTolerance { tolerance }
}
ParameterizedComparisonOperator::WithinPctTolerance { pct } => {
Self::WithinPctTolerance { pct }
}
ParameterizedComparisonOperator::WithinStdDev {
sigma,
mean,
std_dev,
} => Self::WithinStdDev {
sigma,
mean,
std_dev,
},
ParameterizedComparisonOperator::BetweenPercentiles {
lower_pct,
upper_pct,
} => Self::BetweenPercentiles {
lower_pct,
upper_pct,
},
ParameterizedComparisonOperator::DivergenceLessThan { metric, threshold } => {
Self::DivergenceLessThan { metric, threshold }
}
ParameterizedComparisonOperator::CosineSimilarityAtLeast { threshold } => {
Self::CosineSimilarityAtLeast { threshold }
}
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
enum ParameterlessComparisonOperatorSchema {
Equals,
NotEquals,
GreaterThan,
GreaterThanOrEquals,
LessThan,
LessThanOrEquals,
IsPositive,
IsNegative,
IsZero,
Contains,
NotContains,
ContainsIgnoreCase,
StartsWith,
EndsWith,
IsEmail,
IsUrl,
IsUuid,
IsIpv4,
IsIpv6,
IsJson,
In,
NotIn,
IsSubset,
IsSuperset,
IsDisjoint,
AllOf,
AnyOf,
NoneOf,
IsEmpty,
IsNonEmpty,
UniqueValues,
IsTruthy,
IsFalsy,
IsNull,
IsNotNull,
IsString,
IsNumber,
IsBoolean,
IsObject,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(untagged)]
enum ComparisonOperatorWireSchema {
Parameterless(ParameterlessComparisonOperatorSchema),
Parameterized(ParameterizedComparisonOperator),
}
impl ComparisonOperator {
pub fn validate(&self) -> Result<(), WyrdError> {
match self {
Self::MatchesRegex { pattern } | Self::NotMatchesRegex { pattern } => {
validate_regex_pattern(pattern)
}
_ => Ok(()),
}
}
#[must_use]
pub fn discriminator(&self) -> &'static str {
use ComparisonOperator::{
AllOf, AnyOf, ApproximatelyEquals, BetweenPercentiles, Contains, ContainsIgnoreCase,
CosineSimilarityAtLeast, DivergenceLessThan, EndsWith, Equals, GreaterThan,
GreaterThanOrEquals, HasMaxLength, HasMinLength, In, InRange, IsBoolean, IsDisjoint,
IsEmail, IsEmpty, IsFalsy, IsIpv4, IsIpv6, IsJson, IsNegative, IsNonEmpty, IsNotNull,
IsNull, IsNumber, IsObject, IsPositive, IsString, IsSubset, IsSuperset, IsTruthy,
IsType, IsUrl, IsUuid, IsZero, Length, LengthGreaterThan, LengthLessThan, LessThan,
LessThanOrEquals, MatchesRegex, NoneOf, NotContains, NotEquals, NotIn, NotInRange,
NotMatchesRegex, StartsWith, UniqueValues, WithinAbsTolerance, WithinPctTolerance,
WithinStdDev,
};
match self {
Equals => "equals",
NotEquals => "not_equals",
GreaterThan => "greater_than",
GreaterThanOrEquals => "greater_than_or_equals",
LessThan => "less_than",
LessThanOrEquals => "less_than_or_equals",
InRange { .. } => "in_range",
NotInRange { .. } => "not_in_range",
ApproximatelyEquals { .. } => "approximately_equals",
IsPositive => "is_positive",
IsNegative => "is_negative",
IsZero => "is_zero",
Contains => "contains",
NotContains => "not_contains",
ContainsIgnoreCase => "contains_ignore_case",
StartsWith => "starts_with",
EndsWith => "ends_with",
MatchesRegex { .. } => "matches_regex",
NotMatchesRegex { .. } => "not_matches_regex",
IsEmail => "is_email",
IsUrl => "is_url",
IsUuid => "is_uuid",
IsIpv4 => "is_ipv4",
IsIpv6 => "is_ipv6",
HasMinLength { .. } => "has_min_length",
HasMaxLength { .. } => "has_max_length",
IsJson => "is_json",
In => "in",
NotIn => "not_in",
IsSubset => "is_subset",
IsSuperset => "is_superset",
IsDisjoint => "is_disjoint",
AllOf => "all_of",
AnyOf => "any_of",
NoneOf => "none_of",
IsEmpty => "is_empty",
IsNonEmpty => "is_non_empty",
Length { .. } => "length",
LengthGreaterThan { .. } => "length_greater_than",
LengthLessThan { .. } => "length_less_than",
UniqueValues => "unique_values",
IsTruthy => "is_truthy",
IsFalsy => "is_falsy",
IsNull => "is_null",
IsNotNull => "is_not_null",
IsType { .. } => "is_type",
IsString => "is_string",
IsNumber => "is_number",
IsBoolean => "is_boolean",
IsObject => "is_object",
WithinAbsTolerance { .. } => "within_abs_tolerance",
WithinPctTolerance { .. } => "within_pct_tolerance",
WithinStdDev { .. } => "within_std_dev",
BetweenPercentiles { .. } => "between_percentiles",
DivergenceLessThan { .. } => "divergence_less_than",
CosineSimilarityAtLeast { .. } => "cosine_similarity_at_least",
}
}
#[must_use]
pub fn as_str(&self) -> &'static str {
self.discriminator()
}
pub fn from_discriminator_parameterless(s: &str) -> Result<Self, WyrdError> {
use ComparisonOperator::{
AllOf, AnyOf, Contains, ContainsIgnoreCase, EndsWith, Equals, GreaterThan,
GreaterThanOrEquals, In, IsBoolean, IsDisjoint, IsEmail, IsEmpty, IsFalsy, IsIpv4,
IsIpv6, IsJson, IsNegative, IsNonEmpty, IsNotNull, IsNull, IsNumber, IsObject,
IsPositive, IsString, IsSubset, IsSuperset, IsTruthy, IsUrl, IsUuid, IsZero, LessThan,
LessThanOrEquals, NoneOf, NotContains, NotEquals, NotIn, StartsWith, UniqueValues,
};
Ok(match s {
"equals" => Equals,
"not_equals" => NotEquals,
"greater_than" => GreaterThan,
"greater_than_or_equals" => GreaterThanOrEquals,
"less_than" => LessThan,
"less_than_or_equals" => LessThanOrEquals,
"is_positive" => IsPositive,
"is_negative" => IsNegative,
"is_zero" => IsZero,
"contains" => Contains,
"not_contains" => NotContains,
"contains_ignore_case" => ContainsIgnoreCase,
"starts_with" => StartsWith,
"ends_with" => EndsWith,
"is_email" => IsEmail,
"is_url" => IsUrl,
"is_uuid" => IsUuid,
"is_ipv4" => IsIpv4,
"is_ipv6" => IsIpv6,
"is_json" => IsJson,
"in" => In,
"not_in" => NotIn,
"is_subset" => IsSubset,
"is_superset" => IsSuperset,
"is_disjoint" => IsDisjoint,
"all_of" => AllOf,
"any_of" => AnyOf,
"none_of" => NoneOf,
"is_empty" => IsEmpty,
"is_non_empty" => IsNonEmpty,
"unique_values" => UniqueValues,
"is_truthy" => IsTruthy,
"is_falsy" => IsFalsy,
"is_null" => IsNull,
"is_not_null" => IsNotNull,
"is_string" => IsString,
"is_number" => IsNumber,
"is_boolean" => IsBoolean,
"is_object" => IsObject,
"in_range"
| "not_in_range"
| "approximately_equals"
| "matches_regex"
| "not_matches_regex"
| "has_min_length"
| "has_max_length"
| "length"
| "length_greater_than"
| "length_less_than"
| "is_type"
| "within_abs_tolerance"
| "within_pct_tolerance"
| "within_std_dev"
| "between_percentiles"
| "divergence_less_than"
| "cosine_similarity_at_least" => {
return Err(WyrdError::Validation {
message: format!("operator {s:?} is parameterized; use the full tagged form"),
details: serde_json::Value::Null,
});
}
other => {
return Err(WyrdError::Validation {
message: format!("unknown operator discriminator: {other:?}"),
details: serde_json::Value::Null,
});
}
})
}
}
impl FromStr for ComparisonOperator {
type Err = WyrdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_discriminator_parameterless(s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum JsonValueType {
Null,
Bool,
Number,
String,
Array,
Object,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum DivergenceMetric {
Kl,
Js,
Wasserstein,
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::{ComparisonOperator, DivergenceMetric, JsonValueType};
#[test]
fn parameterless_round_trip_equals() {
let op = ComparisonOperator::Equals;
let json = serde_json::to_string(&op).expect("operator serializes");
assert_eq!(json, r#""equals""#);
let back: ComparisonOperator = serde_json::from_str(&json).expect("operator deserializes");
assert_eq!(op, back);
}
#[test]
fn parameterized_round_trip_in_range() {
let op = ComparisonOperator::InRange {
min: 0.0,
max: 1.0,
inclusive: true,
};
let json = serde_json::to_string(&op).expect("operator serializes");
assert_eq!(
json,
r#"{"kind":"in_range","min":0.0,"max":1.0,"inclusive":true}"#,
);
let back: ComparisonOperator = serde_json::from_str(&json).expect("operator deserializes");
assert_eq!(op, back);
}
#[test]
fn parameterized_round_trip_matches_regex() {
let op = ComparisonOperator::MatchesRegex {
pattern: "^foo".into(),
};
let json = serde_json::to_string(&op).expect("operator serializes");
let back: ComparisonOperator = serde_json::from_str(&json).expect("operator deserializes");
assert_eq!(op, back);
}
#[test]
fn parameterized_round_trip_is_type() {
let op = ComparisonOperator::IsType {
expected: JsonValueType::String,
};
let json = serde_json::to_string(&op).expect("operator serializes");
assert_eq!(json, r#"{"kind":"is_type","expected":"string"}"#);
let back: ComparisonOperator = serde_json::from_str(&json).expect("operator deserializes");
assert_eq!(op, back);
}
#[test]
fn redundant_object_shape_is_rejected() {
let err = serde_json::from_str::<ComparisonOperator>(r#"{"operator":"is_not_null"}"#)
.expect_err("redundant object shape must be rejected");
assert!(
err.to_string().contains("kind"),
"error should point callers to the parameterized object discriminator: {err}"
);
}
#[test]
fn parameterless_object_shape_is_rejected() {
let err = serde_json::from_str::<ComparisonOperator>(r#"{"kind":"is_not_null"}"#)
.expect_err("parameterless operators must use scalar strings");
assert!(
err.to_string().contains("is_not_null"),
"error should name the bad discriminator: {err}"
);
}
#[test]
fn parameterized_round_trip_divergence() {
let op = ComparisonOperator::DivergenceLessThan {
metric: DivergenceMetric::Kl,
threshold: 0.05,
};
let json = serde_json::to_string(&op).expect("operator serializes");
let back: ComparisonOperator = serde_json::from_str(&json).expect("operator deserializes");
assert_eq!(op, back);
}
#[test]
fn discriminator_matches_serde_tag() {
use ComparisonOperator::*;
let pairs: &[(ComparisonOperator, &str)] = &[
(Equals, "equals"),
(NotEquals, "not_equals"),
(
WithinAbsTolerance { tolerance: 0.0 },
"within_abs_tolerance",
),
(
IsType {
expected: JsonValueType::Null,
},
"is_type",
),
(
DivergenceLessThan {
metric: DivergenceMetric::Js,
threshold: 0.0,
},
"divergence_less_than",
),
];
for (op, want) in pairs {
assert_eq!(op.discriminator(), *want);
assert_eq!(op.as_str(), *want);
let value = serde_json::to_value(op).expect("operator converts to json");
if value.is_string() {
assert_eq!(value.as_str(), Some(*want));
} else {
assert_eq!(value["kind"].as_str(), Some(*want));
}
}
}
#[test]
fn from_str_parses_parameterless() {
assert_eq!(
ComparisonOperator::from_str("equals").expect("equals parses"),
ComparisonOperator::Equals,
);
assert_eq!(
ComparisonOperator::from_discriminator_parameterless("equals").expect("equals parses"),
ComparisonOperator::Equals,
);
assert_eq!(
ComparisonOperator::from_str("is_uuid").expect("is_uuid parses"),
ComparisonOperator::IsUuid,
);
}
#[test]
fn from_str_rejects_parameterized_without_params() {
assert!(ComparisonOperator::from_str("in_range").is_err());
assert!(ComparisonOperator::from_str("matches_regex").is_err());
assert!(ComparisonOperator::from_str("divergence_less_than").is_err());
}
#[test]
fn from_str_rejects_unknown() {
assert!(ComparisonOperator::from_str("not_a_real_operator").is_err());
}
#[test]
fn catalog_count_locked_at_56() {
let all = [
ComparisonOperator::Equals,
ComparisonOperator::NotEquals,
ComparisonOperator::GreaterThan,
ComparisonOperator::GreaterThanOrEquals,
ComparisonOperator::LessThan,
ComparisonOperator::LessThanOrEquals,
ComparisonOperator::InRange {
min: 0.0,
max: 1.0,
inclusive: true,
},
ComparisonOperator::NotInRange {
min: 0.0,
max: 1.0,
inclusive: false,
},
ComparisonOperator::ApproximatelyEquals { tolerance: 0.0 },
ComparisonOperator::IsPositive,
ComparisonOperator::IsNegative,
ComparisonOperator::IsZero,
ComparisonOperator::Contains,
ComparisonOperator::NotContains,
ComparisonOperator::ContainsIgnoreCase,
ComparisonOperator::StartsWith,
ComparisonOperator::EndsWith,
ComparisonOperator::MatchesRegex {
pattern: "x".into(),
},
ComparisonOperator::NotMatchesRegex {
pattern: "x".into(),
},
ComparisonOperator::IsEmail,
ComparisonOperator::IsUrl,
ComparisonOperator::IsUuid,
ComparisonOperator::IsIpv4,
ComparisonOperator::IsIpv6,
ComparisonOperator::HasMinLength { min: 1 },
ComparisonOperator::HasMaxLength { max: 100 },
ComparisonOperator::IsJson,
ComparisonOperator::In,
ComparisonOperator::NotIn,
ComparisonOperator::IsSubset,
ComparisonOperator::IsSuperset,
ComparisonOperator::IsDisjoint,
ComparisonOperator::AllOf,
ComparisonOperator::AnyOf,
ComparisonOperator::NoneOf,
ComparisonOperator::IsEmpty,
ComparisonOperator::IsNonEmpty,
ComparisonOperator::Length { expected: 1 },
ComparisonOperator::LengthGreaterThan { min: 1 },
ComparisonOperator::LengthLessThan { max: 1 },
ComparisonOperator::UniqueValues,
ComparisonOperator::IsTruthy,
ComparisonOperator::IsFalsy,
ComparisonOperator::IsNull,
ComparisonOperator::IsNotNull,
ComparisonOperator::IsType {
expected: JsonValueType::Null,
},
ComparisonOperator::IsString,
ComparisonOperator::IsNumber,
ComparisonOperator::IsBoolean,
ComparisonOperator::IsObject,
ComparisonOperator::WithinAbsTolerance { tolerance: 0.0 },
ComparisonOperator::WithinPctTolerance { pct: 0.0 },
ComparisonOperator::WithinStdDev {
sigma: 0.0,
mean: 0.0,
std_dev: 1.0,
},
ComparisonOperator::BetweenPercentiles {
lower_pct: 0.0,
upper_pct: 1.0,
},
ComparisonOperator::DivergenceLessThan {
metric: DivergenceMetric::Kl,
threshold: 0.0,
},
ComparisonOperator::CosineSimilarityAtLeast { threshold: 0.0 },
];
assert_eq!(all.len(), 56, "catalog count locked at 56");
for op in &all {
let json = serde_json::to_string(op).expect("operator serializes");
let back: ComparisonOperator =
serde_json::from_str(&json).expect("operator deserializes");
assert_eq!(*op, back, "round-trip failed for {}", op.discriminator());
}
}
#[test]
fn discriminators_are_unique() {
use std::collections::HashSet;
let names: HashSet<&str> = [
"equals",
"not_equals",
"greater_than",
"greater_than_or_equals",
"less_than",
"less_than_or_equals",
"in_range",
"not_in_range",
"approximately_equals",
"is_positive",
"is_negative",
"is_zero",
"contains",
"not_contains",
"contains_ignore_case",
"starts_with",
"ends_with",
"matches_regex",
"not_matches_regex",
"is_email",
"is_url",
"is_uuid",
"is_ipv4",
"is_ipv6",
"has_min_length",
"has_max_length",
"is_json",
"in",
"not_in",
"is_subset",
"is_superset",
"is_disjoint",
"all_of",
"any_of",
"none_of",
"is_empty",
"is_non_empty",
"length",
"length_greater_than",
"length_less_than",
"unique_values",
"is_truthy",
"is_falsy",
"is_null",
"is_not_null",
"is_type",
"is_string",
"is_number",
"is_boolean",
"is_object",
"within_abs_tolerance",
"within_pct_tolerance",
"within_std_dev",
"between_percentiles",
"divergence_less_than",
"cosine_similarity_at_least",
]
.into_iter()
.collect();
assert_eq!(names.len(), 56);
}
}