mod builder;
pub use builder::EnumValidatorBuilder;
use super::*;
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EnumValidator<T: ProtoEnum> {
pub cel: Vec<CelProgram>,
pub ignore: Ignore,
#[cfg_attr(feature = "serde", serde(skip))]
_enum: PhantomData<T>,
pub defined_only: bool,
pub required: bool,
pub in_: Option<SortedList<i32>>,
pub not_in: Option<SortedList<i32>>,
pub const_: Option<i32>,
pub error_messages: Option<ErrorMessages<EnumViolation>>,
}
impl<T: ProtoEnum> EnumValidator<T> {
fn validate_as_int(&self, ctx: &mut ValidationCtx, val: i32) -> ValidationResult {
let mut is_valid = IsValid::Yes;
macro_rules! handle_violation {
($id:ident, $default:expr) => {
is_valid &= ctx.add_violation(
ViolationKind::Enum(EnumViolation::$id),
self.error_messages
.as_deref()
.and_then(|map| map.get(&EnumViolation::$id))
.map(|m| Cow::Borrowed(m.as_ref()))
.unwrap_or_else(|| Cow::Owned($default)),
)?;
};
}
if let Some(const_val) = self.const_ {
if val != const_val {
handle_violation!(Const, format!("must be equal to {const_val}"));
}
return Ok(is_valid);
}
if let Some(allowed_list) = &self.in_
&& !allowed_list.items.contains(&val)
{
handle_violation!(
In,
format!(
"must be one of these values: {}",
i32::__format_list(allowed_list)
)
);
}
if let Some(forbidden_list) = &self.not_in
&& forbidden_list.items.contains(&val)
{
handle_violation!(
NotIn,
format!(
"cannot be one of these values: {}",
i32::__format_list(forbidden_list)
)
);
}
Ok(is_valid)
}
}
impl<T: ProtoEnum> Default for EnumValidator<T> {
#[inline]
fn default() -> Self {
Self {
cel: Default::default(),
ignore: Default::default(),
_enum: PhantomData,
defined_only: Default::default(),
required: Default::default(),
in_: Default::default(),
not_in: Default::default(),
const_: Default::default(),
error_messages: None,
}
}
}
impl<T: ProtoEnum> Validator<T> for EnumValidator<T> {
type Target = i32;
impl_testing_methods!();
#[inline(never)]
#[cold]
fn check_consistency(&self) -> Result<(), Vec<ConsistencyError>> {
let mut errors = Vec::new();
macro_rules! check_prop_some {
($($id:ident),*) => {
$(self.$id.is_some()) ||*
};
}
if self.const_.is_some()
&& (!self.cel.is_empty() || self.defined_only || check_prop_some!(in_, not_in))
{
errors.push(ConsistencyError::ConstWithOtherRules);
}
if let Some(custom_messages) = self.error_messages.as_deref() {
let mut unused_messages: Vec<String> = Vec::new();
for key in custom_messages.keys() {
let is_used = match key {
EnumViolation::Required => self.required,
EnumViolation::In => self.in_.is_some(),
EnumViolation::Const => self.const_.is_some(),
EnumViolation::NotIn => self.not_in.is_some(),
EnumViolation::DefinedOnly => self.defined_only,
_ => true,
};
if !is_used {
unused_messages.push(format!("{key:?}"));
}
}
if !unused_messages.is_empty() {
errors.push(ConsistencyError::UnusedCustomMessages(unused_messages));
}
}
#[cfg(feature = "cel")]
if let Err(e) = self.__check_cel_programs() {
errors.extend(e.into_iter().map(ConsistencyError::from));
}
if let Err(e) = check_list_rules(self.in_.as_ref(), self.not_in.as_ref()) {
errors.push(e.into());
}
if let Some(const_val) = &self.const_
&& T::try_from(*const_val).is_err()
{
errors.push(ConsistencyError::ContradictoryInput(format!(
"The `const` value for the enum `{}` is {const_val} but this number is not among its variants.",
T::proto_name()
)));
}
if let Some(in_list) = &self.in_ {
for num in in_list.items.iter() {
if T::try_from(*num).is_err() {
errors.push(ConsistencyError::ContradictoryInput(format!(
"Number {num} is in the allowed list but it does not belong to the enum {}",
T::proto_name()
)));
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn execute_validation(
&self,
ctx: &mut ValidationCtx,
val: Option<&Self::Target>,
) -> ValidationResult {
handle_ignore_always!(&self.ignore);
handle_ignore_if_zero_value!(&self.ignore, val.is_none_or(|v| *v == 0));
let mut is_valid = IsValid::Yes;
macro_rules! handle_violation {
($id:ident, $default:expr) => {
is_valid &= ctx.add_violation(
ViolationKind::Enum(EnumViolation::$id),
self.error_messages
.as_deref()
.and_then(|map| map.get(&EnumViolation::$id))
.map(|m| Cow::Borrowed(m.as_ref()))
.unwrap_or_else(|| Cow::Owned($default)),
)?;
};
}
if self.required && val.is_none_or(|v| *v == 0) {
handle_violation!(Required, "is required".to_string());
return Ok(is_valid);
}
if let Some(&val) = val {
is_valid &= self.validate_as_int(ctx, val)?;
if self.defined_only && T::try_from(val).is_err() {
handle_violation!(DefinedOnly, "must be a known enum value".to_string());
}
#[cfg(feature = "cel")]
if !self.cel.is_empty() {
let cel_ctx = ProgramsExecutionCtx {
programs: &self.cel,
value: val,
ctx,
};
is_valid &= cel_ctx.execute_programs()?;
}
}
Ok(is_valid)
}
#[inline(never)]
#[cold]
fn schema(&self) -> Option<ValidatorSchema> {
Some(ValidatorSchema {
schema: self.clone().into(),
cel_rules: self.__cel_rules(),
imports: vec!["buf/validate/validate.proto".into()],
})
}
}
impl<T: ProtoEnum> From<EnumValidator<T>> for ProtoOption {
#[inline(never)]
#[cold]
fn from(validator: EnumValidator<T>) -> Self {
let mut rules = OptionMessageBuilder::new();
rules
.maybe_set("const", validator.const_)
.set_boolean("defined_only", validator.defined_only)
.maybe_set(
"in",
validator
.in_
.map(|list| OptionValue::new_list(list)),
)
.maybe_set(
"not_in",
validator
.not_in
.map(|list| OptionValue::new_list(list)),
);
let mut outer_rules = OptionMessageBuilder::new();
if !rules.is_empty() {
outer_rules.set("enum", OptionValue::Message(rules.into()));
}
outer_rules
.add_cel_options(validator.cel)
.set_required(validator.required)
.set_ignore(validator.ignore);
Self {
name: "(buf.validate.field)".into(),
value: OptionValue::Message(outer_rules.into()),
}
}
}