protovalidate-buffa 0.0.0

Runtime for protoc-gen-protovalidate-buffa — Validate trait, ValidationError types, CEL integration, rule helpers.
Documentation
use protovalidate_buffa::cel::{AsCelValue, CelConstraint};
use protovalidate_buffa::Violation;
use cel_interpreter::Value;

// Test fixture — a simple "message" that carries a string `id`.
struct Fake {
    id: String,
    ref_id: String,
}

impl AsCelValue for Fake {
    fn as_cel_value(&self) -> Value {
        let mut map = std::collections::HashMap::new();
        map.insert("id".to_string(), Value::String(self.id.clone().into()));
        map.insert(
            "ref_id".to_string(),
            Value::String(self.ref_id.clone().into()),
        );
        Value::Map(map.into())
    }
}

fn run(constraint: &CelConstraint, fake: &Fake) -> Result<(), Violation> {
    constraint.eval(fake)
}

static CHECK_ID_NOT_EMPTY: CelConstraint = CelConstraint::new(
    "fake.id_not_empty",
    "id must not be empty",
    "this.id != ''",
);

#[test]
fn bool_true_passes() {
    let fake = Fake {
        id: "abc".into(),
        ref_id: String::new(),
    };
    assert!(run(&CHECK_ID_NOT_EMPTY, &fake).is_ok());
}

#[test]
fn bool_false_fails_with_constraint_message() {
    let fake = Fake {
        id: String::new(),
        ref_id: String::new(),
    };
    let err = run(&CHECK_ID_NOT_EMPTY, &fake).unwrap_err();
    assert_eq!(err.rule_id, "fake.id_not_empty");
    assert!(err.message.contains("id must not be empty"));
}

static EMPTY_STRING_MEANS_PASS: CelConstraint = CelConstraint::new(
    "fake.legacy",
    "",
    "''", // always returns empty string
);

#[test]
fn empty_string_return_is_pass() {
    let fake = Fake {
        id: "abc".into(),
        ref_id: String::new(),
    };
    assert!(run(&EMPTY_STRING_MEANS_PASS, &fake).is_ok());
}

static NON_EMPTY_STRING_RETURN: CelConstraint = CelConstraint::new(
    "fake.dyn_msg",
    "", // empty → returned string used
    "this.id == '' ? 'id needs to be set' : ''",
);

#[test]
fn non_empty_string_return_is_fail_with_returned_text() {
    let fake = Fake {
        id: String::new(),
        ref_id: String::new(),
    };
    let err = run(&NON_EMPTY_STRING_RETURN, &fake).unwrap_err();
    assert!(err.message.contains("id needs to be set"));
}

static NON_EMPTY_STRING_STATIC_MSG_WINS: CelConstraint = CelConstraint::new(
    "fake.msg_wins",
    "the static message wins",
    "this.id == '' ? 'dynamic text that should be suppressed' : ''",
);

#[test]
fn static_message_overrides_returned_string() {
    let fake = Fake {
        id: String::new(),
        ref_id: String::new(),
    };
    let err = run(&NON_EMPTY_STRING_STATIC_MSG_WINS, &fake).unwrap_err();
    assert_eq!(err.message, "the static message wins");
    assert!(!err.message.contains("dynamic text"));
}

#[test]
fn is_uuid_custom_fn_registered() {
    static CHECK_UUID: CelConstraint = CelConstraint::new(
        "fake.ref_id_uuid",
        "ref_id must be a UUID",
        "this.ref_id == '' || this.ref_id.isUuid()",
    );
    let ok = Fake {
        id: String::new(),
        ref_id: "550e8400-e29b-41d4-a716-446655440000".into(),
    };
    let bad = Fake {
        id: String::new(),
        ref_id: "not-uuid".into(),
    };
    assert!(CHECK_UUID.eval(&ok).is_ok());
    assert!(CHECK_UUID.eval(&bad).is_err());
}