Radish alpha
r
rad:zwTxygwuz5LDGBq255RA2CbNGrz8
Radicle CI broker
Radicle
Git
Fix filter predicates for `Not`
Merged fintohaps opened 1 year ago

Surfaced by the issue:

[rad issue show 46fc1376e12a964b5435e6594efb1cbc1731d56d](https://explorer.radicle.gr/nodes/seed.radicle.gr/rad:zwTxygwuz5LDGBq255RA2CbNGrz8/issues/46fc1376e12a964b5435e6594efb1cbc1731d56d)

When attempting to deserialize the Not variant an error would occur:

deserializing nested enum in EventFilter::Not from YAML is not supported yet

This limitation can be fixed by doing the following to the Not variant:

    Not(#[serde(with = "serde_yml::with::singleton_map")] Box<Self>)

I believe this allows serd_yml to perform some indirection to allow for the nested enum.

4 files changed +146 -12 d0c6a8c8 9e84afec
modified Cargo.lock
@@ -1876,6 +1876,17 @@ dependencies = [
]

[[package]]
+
name = "qcheck-macros"
+
version = "1.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2742b9af5a690615904b18f11983f4db9ea7ad1c7e6ed3fb4b2402cdaaf5b1b5"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn 1.0.109",
+
]
+

+
[[package]]
name = "quick-xml"
version = "0.37.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1936,6 +1947,8 @@ dependencies = [
 "culpa",
 "duration-str",
 "html-page",
+
 "qcheck",
+
 "qcheck-macros",
 "radicle",
 "radicle-crypto",
 "radicle-git-ext",
modified Cargo.toml
@@ -41,6 +41,8 @@ features = ["default", "test"]
[dev-dependencies]
ctor = "0.2.8"
culpa = "1.0.2"
+
qcheck = { version = "1", default-features = false }
+
qcheck-macros = { version = "1", default-features = false }

[build-dependencies]
subplot-build = "0.12.0"
modified src/filter.rs
@@ -11,6 +11,9 @@ use crate::{
    logger,
};

+
#[cfg(test)]
+
pub mod arbitrary;
+

#[derive(Clone)]
pub struct Trigger {
    adapter: String,
@@ -37,7 +40,7 @@ impl From<&TriggerConfig> for Trigger {
}

/// A Boolean expression for filtering broker events.
-
#[derive(Debug, Clone, Deserialize, Serialize)]
+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub enum EventFilter {
    /// Event for a specific repository.
@@ -74,13 +77,13 @@ pub enum EventFilter {
    Deny,

    /// Allow the opposite of the contained filter.
-
    Not(Box<Self>),
+
    Not(#[serde(with = "serde_yml::with::singleton_map")] Box<Self>),

    /// Allow if all contained filters allow.
-
    And(Vec<Box<Self>>),
+
    And(Vec<EventFilter>),

    /// Allow if any contained filter allow.
-
    Or(Vec<Box<Self>>),
+
    Or(Vec<EventFilter>),
}

impl EventFilter {
@@ -188,6 +191,7 @@ impl Filters {
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
+
    use qcheck_macros::quickcheck;
    use radicle::prelude::{Did, RepoId};

    use super::*;
@@ -582,10 +586,7 @@ mod test {

    #[test]
    fn allows_if_all_allow() {
-
        let filter = EventFilter::And(vec![
-
            Box::new(EventFilter::Allow),
-
            Box::new(EventFilter::Allow),
-
        ]);
+
        let filter = EventFilter::And(vec![EventFilter::Allow, EventFilter::Allow]);

        eprintln!("filter: {filter:#?}");
        for e in all_events(did(), rid(), refstring("main"), patch_id(), oid(), oid()).iter() {
@@ -596,10 +597,7 @@ mod test {

    #[test]
    fn allows_if_any_allows() {
-
        let filter = EventFilter::Or(vec![
-
            Box::new(EventFilter::Deny),
-
            Box::new(EventFilter::Allow),
-
        ]);
+
        let filter = EventFilter::Or(vec![EventFilter::Deny, EventFilter::Allow]);

        eprintln!("filter: {filter:#?}");
        for e in all_events(did(), rid(), refstring("main"), patch_id(), oid(), oid()).iter() {
@@ -607,6 +605,39 @@ mod test {
            assert!(filter.allows(e));
        }
    }
+

+
    /// This test ensures that we can deserialize a nested enum, which is only
+
    /// possible using the `serde_yml::with::singleton_map` helper on the
+
    /// `EventFilter::Not` variant.
+
    #[test]
+
    fn deserialize_yaml_nested_not() {
+
        let expected = EventFilter::And(vec![
+
            EventFilter::Not(Box::new(EventFilter::Repository(
+
                "rad:z32iyJDyFLqvPFzwHm8YadK4HQ2EY"
+
                    .parse::<RepoId>()
+
                    .unwrap(),
+
            ))),
+
            EventFilter::BranchCreated,
+
            EventFilter::PatchCreated,
+
        ]);
+
        let filters = r#"
+
!And
+
- !Not
+
    Repository: rad:z32iyJDyFLqvPFzwHm8YadK4HQ2EY
+
- !BranchCreated
+
- !PatchCreated
+
"#;
+
        let evf = serde_yml::from_str::<EventFilter>(filters);
+
        assert!(evf.is_ok(), "Failed to deserialize filters: {evf:?}");
+
        assert_eq!(evf.unwrap(), expected);
+
    }
+

+
    #[quickcheck]
+
    fn yaml_roundtrip(filter: EventFilter) -> Result<bool, serde_yml::Error> {
+
        let ser = serde_yml::to_string(&filter)?;
+
        let de = serde_yml::from_str(&ser)?;
+
        Ok(filter == de)
+
    }
}

#[derive(Debug, thiserror::Error)]
added src/filter/arbitrary.rs
@@ -0,0 +1,88 @@
+
use qcheck::Arbitrary;
+
use radicle::identity::RepoId;
+
use radicle::node::NodeId;
+
use radicle::test::arbitrary::{oid, refstring};
+

+
use super::EventFilter;
+

+
impl Arbitrary for EventFilter {
+
    fn arbitrary(g: &mut qcheck::Gen) -> Self {
+
        use EventFilter::*;
+

+
        #[derive(Clone, Copy)]
+
        enum Variants {
+
            Repository,
+
            Branch,
+
            BranchCreated,
+
            BranchUpdated,
+
            BranchDeleted,
+
            Patch,
+
            PatchCreated,
+
            PatchUpdated,
+
            Node,
+
            Allow,
+
            Deny,
+
            Not,
+
            And,
+
            Or,
+
        }
+
        let leaves = [
+
            Variants::Repository,
+
            Variants::Branch,
+
            Variants::BranchCreated,
+
            Variants::BranchUpdated,
+
            Variants::BranchDeleted,
+
            Variants::Patch,
+
            Variants::PatchCreated,
+
            Variants::PatchUpdated,
+
            Variants::Node,
+
            Variants::Allow,
+
            Variants::Deny,
+
        ];
+
        let branches = [Variants::Not, Variants::And, Variants::Or];
+

+
        let n = i8::arbitrary(g).clamp(1, 10);
+

+
        // Choose a branch 7 out of 10 times, to ensure that we get more complex
+
        // filters when generating them
+
        //
+
        // SAFETY: the leaves and branches slices are non-empty so this will
+
        // always return a value
+
        let variant = if n < 7 {
+
            *g.choose(&branches)
+
                .expect("BUG: will always provide an EventFilter")
+
        } else {
+
            *g.choose(&leaves)
+
                .expect("BUG: will always provide an EventFilter")
+
        };
+

+
        // N.b. reduce the size to so that the recursive type eventually reduces
+
        // to pick a leaf branch
+
        let size = (3 * g.size() / 4).max(1);
+
        match variant {
+
            Variants::Repository => Repository(RepoId::arbitrary(g)),
+
            Variants::Branch => Branch(refstring(10)),
+
            Variants::BranchCreated => BranchCreated,
+
            Variants::BranchUpdated => BranchUpdated,
+
            Variants::BranchDeleted => BranchDeleted,
+
            Variants::Patch => Patch(oid()),
+
            Variants::PatchCreated => PatchCreated,
+
            Variants::PatchUpdated => PatchUpdated,
+
            Variants::Node => Node(NodeId::arbitrary(g)),
+
            Variants::Allow => Allow,
+
            Variants::Deny => Deny,
+
            Variants::Not => {
+
                g.set_size(size);
+
                Not(Box::new(Self::arbitrary(g)))
+
            }
+
            Variants::And => {
+
                g.set_size(size);
+
                And(Vec::arbitrary(g))
+
            }
+
            Variants::Or => {
+
                g.set_size(size);
+
                Or(Vec::arbitrary(g))
+
            }
+
        }
+
    }
+
}