protify 0.1.4

A Rust-first protobuf framework to generate packages from rust code, with validation included
Documentation
mod builder;
pub use builder::AnyValidatorBuilder;

use proto_types::Any;

use super::*;

/// Validator for the [`Any`] type.
#[non_exhaustive]
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AnyValidator {
	/// Adds custom validation using one or more [`CelRule`]s to this field.
	pub cel: Vec<CelProgram>,

	/// The conditions upon which this validator should be skipped.
	pub ignore: Ignore,

	/// Specifies that the field must be set in order to be valid.
	pub required: bool,

	/// Specifies that only the type URLs in this list will be considered valid for this field.
	pub in_: Option<SortedList<FixedStr>>,

	/// Specifies that the type URLs in this list will be considered NOT valid for this field.
	pub not_in: Option<SortedList<FixedStr>>,

	/// A map of custom error messages.
	pub error_messages: Option<ErrorMessages<AnyViolation>>,
}

impl Validator<Any> for AnyValidator {
	type Target = Any;

	impl_testing_methods!();

	#[inline(never)]
	#[cold]
	fn check_consistency(&self) -> Result<(), Vec<ConsistencyError>> {
		let mut errors = Vec::new();

		#[cfg(feature = "cel")]
		if let Err(e) = self.__check_cel_programs() {
			errors.extend(e.into_iter().map(ConsistencyError::from));
		}

		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 {
					AnyViolation::Required => self.required,
					AnyViolation::In => self.in_.is_some(),
					AnyViolation::NotIn => self.not_in.is_some(),
					_ => true,
				};

				if !is_used {
					unused_messages.push(format!("{key:?}"));
				}
			}

			if !unused_messages.is_empty() {
				errors.push(ConsistencyError::UnusedCustomMessages(unused_messages));
			}
		}

		if let Err(e) = check_list_rules(self.in_.as_ref(), self.not_in.as_ref()) {
			errors.push(e.into());
		}

		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.is_default()));

		let mut is_valid = IsValid::Yes;

		macro_rules! handle_violation {
			($id:ident, $default:expr) => {
				is_valid &= ctx.add_violation(
					ViolationKind::Any(AnyViolation::$id),
					self.error_messages
						.as_deref()
						.and_then(|map| map.get(&AnyViolation::$id))
						.map(|m| Cow::Borrowed(m.as_ref()))
						.unwrap_or_else(|| Cow::Owned($default)),
				)?;
			};
		}

		if let Some(val) = val {
			if let Some(allowed_list) = &self.in_
				&& !allowed_list.contains(val.type_url.as_str())
			{
				handle_violation!(
					In,
					format!(
						"must have one of these type URLs: {}",
						FixedStr::__format_list(allowed_list)
					)
				);
			}

			if let Some(forbidden_list) = &self.not_in
				&& forbidden_list.contains(val.type_url.as_str())
			{
				handle_violation!(
					NotIn,
					format!(
						"cannot have one of these type URLs: {}",
						FixedStr::__format_list(forbidden_list)
					)
				);
			}

			#[cfg(feature = "cel")]
			if !self.cel.is_empty() {
				let cel_ctx = ProgramsExecutionCtx {
					programs: &self.cel,
					value: val.clone(),
					ctx,
				};

				is_valid &= cel_ctx.execute_programs()?;
			}
		} else if self.required {
			handle_violation!(Required, "is required".to_string());
		}

		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 From<AnyValidator> for ProtoOption {
	#[inline(never)]
	#[cold]
	fn from(validator: AnyValidator) -> Self {
		let mut rules = OptionMessageBuilder::new();

		rules
			.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("any", OptionValue::Message(rules.build()));
		}

		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.build()),
		}
	}
}