use std::cmp::Ordering;
use std::fmt;
use qubit_datatype::NumericComparisonPolicy;
use qubit_redact::Redact;
use qubit_redact::RedactionWriter;
use qubit_redact::Redactor;
use qubit_redact::Sensitivity;
use qubit_value::Value;
use qubit_value::ValueRef;
use qubit_value::ValueWirePayloadRefV1;
use super::internal::MatchOutcome;
use crate::FilterLimitKind;
use crate::FilterLimits;
use crate::Metadata;
use crate::MetadataError;
use crate::MetadataResult;
#[derive(Clone, PartialEq)]
#[non_exhaustive]
pub enum Condition {
Equal {
key: String,
value: Value,
},
NotEqual {
key: String,
value: Value,
},
Less {
key: String,
value: Value,
},
LessEqual {
key: String,
value: Value,
},
Greater {
key: String,
value: Value,
},
GreaterEqual {
key: String,
value: Value,
},
In {
key: String,
values: Vec<Value>,
},
NotIn {
key: String,
values: Vec<Value>,
},
Exists {
key: String,
},
NotExists {
key: String,
},
}
impl Condition {
#[cfg(feature = "json")]
pub(crate) fn visit_operands<E>(&self, visitor: &mut impl FnMut(&Value) -> Result<(), E>) -> Result<(), E> {
match self {
Self::Equal { value, .. }
| Self::NotEqual { value, .. }
| Self::Less { value, .. }
| Self::LessEqual { value, .. }
| Self::Greater { value, .. }
| Self::GreaterEqual { value, .. } => visitor(value),
Self::In { values, .. } | Self::NotIn { values, .. } => {
for value in values {
visitor(value)?;
}
Ok(())
}
Self::Exists { .. } | Self::NotExists { .. } => Ok(()),
}
}
pub(crate) fn validate_operands(&self) -> MetadataResult<()> {
match self {
Self::Equal { value, .. } => validate_operand("eq", value),
Self::NotEqual { value, .. } => validate_operand("ne", value),
Self::Less { value, .. } => validate_operand("lt", value),
Self::LessEqual { value, .. } => validate_operand("le", value),
Self::Greater { value, .. } => validate_operand("gt", value),
Self::GreaterEqual { value, .. } => validate_operand("ge", value),
Self::In { values, .. } => validate_operands("in_set", values),
Self::NotIn { values, .. } => validate_operands("not_in_set", values),
Self::Exists { .. } | Self::NotExists { .. } => Ok(()),
}
}
pub(crate) fn validate_limits(&self, limits: FilterLimits) -> MetadataResult<()> {
let key = self.key();
if key.len() > limits.max_key_bytes() {
return Err(MetadataError::FilterLimitExceeded {
kind: FilterLimitKind::KeyBytes,
value: key.len(),
maximum: limits.max_key_bytes(),
});
}
let values = match self {
Self::In { values, .. } | Self::NotIn { values, .. } => values,
_ => return Ok(()),
};
if values.len() > limits.max_set_values() {
return Err(MetadataError::FilterLimitExceeded {
kind: FilterLimitKind::SetValues,
value: values.len(),
maximum: limits.max_set_values(),
});
}
Ok(())
}
pub(crate) fn evaluate(&self, meta: &Metadata, numeric_comparison_policy: NumericComparisonPolicy) -> MatchOutcome {
match self {
Condition::Equal { key, value } => evaluate_concrete(meta, key, |stored| {
values_equal(stored, value, numeric_comparison_policy)
.map_or(MatchOutcome::Unknown, MatchOutcome::from_bool)
}),
Condition::NotEqual { key, value } => evaluate_concrete(meta, key, |stored| {
values_equal(stored, value, numeric_comparison_policy)
.map_or(MatchOutcome::Unknown, |equal| MatchOutcome::from_bool(!equal))
}),
Condition::Less { key, value } => evaluate_concrete(meta, key, |stored| {
compare_values(stored, value, numeric_comparison_policy).map_or(MatchOutcome::Unknown, |ordering| {
MatchOutcome::from_bool(ordering == Ordering::Less)
})
}),
Condition::LessEqual { key, value } => evaluate_concrete(meta, key, |stored| {
compare_values(stored, value, numeric_comparison_policy).map_or(MatchOutcome::Unknown, |ordering| {
MatchOutcome::from_bool(matches!(ordering, Ordering::Less | Ordering::Equal))
})
}),
Condition::Greater { key, value } => evaluate_concrete(meta, key, |stored| {
compare_values(stored, value, numeric_comparison_policy).map_or(MatchOutcome::Unknown, |ordering| {
MatchOutcome::from_bool(ordering == Ordering::Greater)
})
}),
Condition::GreaterEqual { key, value } => evaluate_concrete(meta, key, |stored| {
compare_values(stored, value, numeric_comparison_policy).map_or(MatchOutcome::Unknown, |ordering| {
MatchOutcome::from_bool(matches!(ordering, Ordering::Greater | Ordering::Equal))
})
}),
Condition::In { key, values } => evaluate_concrete(meta, key, |stored| {
evaluate_membership(stored, values, numeric_comparison_policy, false)
}),
Condition::NotIn { key, values } => evaluate_concrete(meta, key, |stored| {
evaluate_membership(stored, values, numeric_comparison_policy, true)
}),
Condition::Exists { key } => MatchOutcome::from_bool(concrete_value(meta, key).is_some()),
Condition::NotExists { key } => MatchOutcome::from_bool(concrete_value(meta, key).is_none()),
}
}
#[inline]
fn key(&self) -> &str {
match self {
Self::Equal { key, .. }
| Self::NotEqual { key, .. }
| Self::Less { key, .. }
| Self::LessEqual { key, .. }
| Self::Greater { key, .. }
| Self::GreaterEqual { key, .. }
| Self::In { key, .. }
| Self::NotIn { key, .. }
| Self::Exists { key }
| Self::NotExists { key } => key,
}
}
}
fn validate_operand(operator: &'static str, value: &Value) -> MetadataResult<()> {
if value.is_unset() {
return Err(MetadataError::InvalidFilterOperand {
operator,
data_type: value.data_type(),
message: "filter operands must be concrete values".to_owned(),
});
}
if ValueWirePayloadRefV1::try_from(value).is_err() {
return Err(MetadataError::InvalidFilterOperand {
operator,
data_type: value.data_type(),
message: "filter operands must be representable by the V1 wire format".to_owned(),
});
}
Ok(())
}
fn validate_operands(operator: &'static str, values: &[Value]) -> MetadataResult<()> {
values.iter().try_for_each(|value| validate_operand(operator, value))
}
impl Redact for Condition {
fn write_redacted(&self, writer: &mut RedactionWriter<'_>) {
let (operator, operand): (&str, Option<&dyn fmt::Debug>) = match self {
Self::Equal { value, .. } => ("equal", Some(value)),
Self::NotEqual { value, .. } => ("not_equal", Some(value)),
Self::Less { value, .. } => ("less", Some(value)),
Self::LessEqual { value, .. } => ("less_equal", Some(value)),
Self::Greater { value, .. } => ("greater", Some(value)),
Self::GreaterEqual { value, .. } => ("greater_equal", Some(value)),
Self::In { values, .. } => ("in", Some(values)),
Self::NotIn { values, .. } => ("not_in", Some(values)),
Self::Exists { .. } => ("exists", None),
Self::NotExists { .. } => ("not_exists", None),
};
writer.record("Condition", |fields| {
fields.unredacted("operator", || operator);
fields.unredacted("key", || self.key());
if let Some(operand) = operand {
fields.sensitive_at_least(Sensitivity::Secret, "value", || operand);
}
});
}
}
impl fmt::Debug for Condition {
#[inline]
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let output = Redactor::strict().redact_text(self);
let text = output.text_or_marker("<redaction incomplete>");
formatter.write_str(text.as_ref())
}
}
#[inline]
fn evaluate_concrete<F>(meta: &Metadata, key: &str, predicate: F) -> MatchOutcome
where
F: FnOnce(&Value) -> MatchOutcome,
{
concrete_value(meta, key).map_or(MatchOutcome::Unknown, predicate)
}
fn evaluate_membership(
stored: &Value,
candidates: &[Value],
numeric_comparison_policy: NumericComparisonPolicy,
negated: bool,
) -> MatchOutcome {
let mut unknown = false;
for candidate in candidates {
match values_equal(stored, candidate, numeric_comparison_policy) {
Some(true) => return MatchOutcome::from_bool(!negated),
Some(false) => {}
None => unknown = true,
}
}
if unknown {
MatchOutcome::Unknown
} else {
MatchOutcome::from_bool(negated)
}
}
#[inline]
fn concrete_value<'a>(meta: &'a Metadata, key: &str) -> Option<&'a Value> {
meta.get_raw(key).filter(|value| !value.is_unset())
}
#[inline]
fn values_equal(left: &Value, right: &Value, numeric_comparison_policy: NumericComparisonPolicy) -> Option<bool> {
if left.is_numeric() && right.is_numeric() {
return left
.numeric_cmp(right, numeric_comparison_policy)
.ok()
.map(|ordering| ordering == Ordering::Equal);
}
if left.data_type() != right.data_type() {
return None;
}
Some(left == right)
}
#[inline]
fn compare_values(left: &Value, right: &Value, numeric_comparison_policy: NumericComparisonPolicy) -> Option<Ordering> {
if left.is_numeric() && right.is_numeric() {
return left.numeric_cmp(right, numeric_comparison_policy).ok();
}
match (left.view(), right.view()) {
(ValueRef::String(left), ValueRef::String(right)) => left.partial_cmp(right),
_ => None,
}
}