use std::path::{Path, PathBuf};
use radicle_crypto::PublicKey;
use regex::Regex;
use serde::{Deserialize, Serialize};
use radicle::{
cob::patch::PatchId,
git::{BranchName, Oid, raw::ObjectType},
node::NodeId,
prelude::{Profile, RepoId},
storage::{ReadRepository, git::Repository},
};
use crate::{
ci_event::{CiEvent, CiEventV1},
config::TriggerConfig,
logger,
refs::ref_string,
};
#[cfg(test)]
pub mod arbitrary;
#[derive(Clone)]
pub struct Trigger {
adapter: String,
filters: Vec<EventFilter>,
}
impl Trigger {
pub fn adapter(&self) -> &str {
&self.adapter
}
pub fn allows(&self, e: &CiEvent) -> bool {
self.filters.iter().any(|filter| filter.allows(e))
}
}
impl From<&TriggerConfig> for Trigger {
fn from(config: &TriggerConfig) -> Self {
Self {
adapter: config.adapter.clone(),
filters: config.filters.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub enum EventFilter {
Repository(RepoId),
Branch(BranchName),
Tag(String),
DefaultBranch,
BranchCreated,
BranchUpdated,
BranchDeleted,
Patch(Oid),
PatchCreated,
PatchUpdated,
TagCreated,
TagUpdated,
TagDeleted,
Node(NodeId),
AnyDelegate,
HasFile(PathBuf),
Allow,
Deny,
#[serde(alias = "NoneOf")]
Not(Vec<EventFilter>),
#[serde(alias = "AllOf")]
And(Vec<EventFilter>),
#[serde(alias = "AnyOf")]
Or(Vec<EventFilter>),
}
impl EventFilter {
pub fn decide(&self, event: &CiEvent) -> Decision {
if matches!(event, CiEvent::V1(CiEventV1::Shutdown)) {
return Decision::new("Shutdown", true, "shutdown event is always allowed");
}
match self {
Self::Not(expr) => {
let conds: Vec<Decision> = expr.iter().map(|op| op.decide(event)).collect();
let allowed = !conds.iter().any(|op| op.allowed);
Decision::parent("Not", allowed, "(combo)", conds)
}
Self::And(expr) => {
let conds: Vec<Decision> = expr.iter().map(|op| op.decide(event)).collect();
let allowed = conds.iter().all(|op| op.allowed);
Decision::parent("And", allowed, "(combo)", conds)
}
Self::Or(expr) => {
let conds: Vec<Decision> = expr.iter().map(|op| op.decide(event)).collect();
let allowed = conds.iter().any(|op| op.allowed);
Decision::parent("Or", allowed, "(combo)", conds)
}
Self::Allow => Decision::new("Allow", true, "always allowed"),
Self::Deny => Decision::new("Deny", false, "never allowed"),
Self::Node(wanted) => {
let actual = event.from_node();
let allowed = Some(wanted) == actual;
Decision::string(
"Node",
allowed,
format!("wanted={wanted} actual={actual:?}"),
)
}
#[allow(clippy::unwrap_used)]
Self::AnyDelegate => {
let repo_id = event.repository().unwrap();
let radicle = crate::ergo::Radicle::new().unwrap();
let repo = radicle.repository(repo_id).unwrap();
let origin = event.from_node().unwrap();
let delegates: Vec<PublicKey> = repo
.delegates()
.iter()
.flatten()
.map(|d| *d.as_key())
.collect();
let allowed = delegates.contains(origin);
Decision::string(
"AnyDelegate",
allowed,
format!("wanted={origin} delegates={delegates:?}",),
)
}
Self::Repository(wanted) => {
let actual = event.repository();
let allowed = Some(wanted) == actual;
Decision::string(
"Repository",
allowed,
format!("wanted={wanted} actual={actual:?}"),
)
}
Self::Branch(wanted) => {
let actual = event.branch();
let allowed = Some(wanted) == actual;
Decision::string(
"Branch",
allowed,
format!("wanted={wanted} actual={actual:?}"),
)
}
Self::Tag(wanted) => match Regex::new(wanted) {
Ok(re) => {
let actual = event.tag();
let allowed = match &actual {
Some(actual) => {
let actual = actual.as_str();
if let Some(m) = re.find(actual) {
m.start() == 0 && m.end() == actual.len()
} else {
false
}
}
_ => false,
};
Decision::string(
"Tag",
allowed,
format!("wanted={wanted:?} actual={actual:?}"),
)
}
Err(err) => {
logger::queueproc_filter_regex_error(wanted, err);
Decision::new("Tag", false, "regex syntax error")
}
},
Self::DefaultBranch => {
let repo = event.repository();
let actual = event.branch();
let allowed = match (repo, actual) {
(Some(repo), Some(actual)) => is_default_branch(repo, actual),
_ => false,
};
Decision::string(
"DefaultBranch",
allowed,
format!("repo={repo:?} actual={actual:?}"),
)
}
Self::BranchCreated => {
let allowed = matches!(event, CiEvent::V1(CiEventV1::BranchCreated { .. }));
Decision::new("BranchCreated", allowed, "")
}
Self::BranchUpdated => {
let allowed = matches!(event, CiEvent::V1(CiEventV1::BranchUpdated { .. }));
Decision::new("BranchUpdated", allowed, "")
}
Self::BranchDeleted => {
let allowed = matches!(event, CiEvent::V1(CiEventV1::BranchDeleted { .. }));
Decision::new("BranchDeleted", allowed, "")
}
Self::TagCreated => {
let allowed = matches!(event, CiEvent::V1(CiEventV1::TagCreated { .. }));
Decision::new("TagCreated", allowed, "")
}
Self::TagUpdated => {
let allowed = matches!(event, CiEvent::V1(CiEventV1::TagUpdated { .. }));
Decision::new("TagUpdated", allowed, "")
}
Self::TagDeleted => {
let allowed = matches!(event, CiEvent::V1(CiEventV1::TagDeleted { .. }));
Decision::new("TagDeleted", allowed, "")
}
Self::Patch(wanted) => {
let actual = event.patch_id();
let allowed = Some(&PatchId::from(wanted)) == actual;
Decision::string(
"Patch",
allowed,
format!("wanted={wanted} actual={actual:?}"),
)
}
Self::PatchCreated => {
let allowed = matches!(event, CiEvent::V1(CiEventV1::PatchCreated { .. }));
Decision::new("PatchCreated", allowed, "")
}
Self::PatchUpdated => {
let allowed = matches!(event, CiEvent::V1(CiEventV1::PatchUpdated { .. }));
Decision::new("PatchUpdated", allowed, "")
}
Self::HasFile(wanted) => {
let repo = event.repository();
let tip = event.tip();
let allowed = match (repo, tip) {
(Some(repo), Some(tip)) => has_file(repo, tip, wanted),
_ => false,
};
Decision::string(
"HasFile",
allowed,
format!("repo={repo:?} tip={tip:?} wanted={wanted:?}"),
)
}
}
}
pub fn allows(&self, event: &CiEvent) -> bool {
let dec = self.decide(event);
logger::queueproc_filter_decision(event, self, &dec);
dec.allowed
}
pub fn from_file(filename: &Path) -> Result<Vec<Self>, FilterError> {
Filters::from_file(filename)
}
}
#[derive(Debug, Serialize)]
pub struct Decision {
filter: &'static str,
allowed: bool,
reason: String,
children: Vec<Self>,
}
impl Decision {
fn new(filter: &'static str, allowed: bool, reason: &'static str) -> Self {
Self {
filter,
allowed,
reason: reason.into(),
children: vec![],
}
}
fn parent(
filter: &'static str,
allowed: bool,
reason: &'static str,
children: Vec<Self>,
) -> Self {
Self {
filter,
allowed,
reason: reason.into(),
children,
}
}
fn string(filter: &'static str, allowed: bool, reason: String) -> Self {
Self {
filter,
allowed,
reason,
children: vec![],
}
}
pub fn allowed(&self) -> bool {
self.allowed
}
pub fn print(&self, level: usize) {
let mut x = String::new();
for _ in 0..level {
x.push_str(" ");
}
println!(
"{x}{} allowed={} {}",
self.filter, self.allowed, self.reason
);
for kid in self.children.iter() {
kid.print(level + 1);
}
}
}
fn is_default_branch(repo_id: &RepoId, wanted: &str) -> bool {
if let Ok(wanted) = ref_string(wanted) {
if let Ok(default) = get_default_branch(repo_id) {
return wanted == default;
}
}
false
}
pub fn get_default_branch(repo_id: &RepoId) -> Result<BranchName, Box<dyn std::error::Error>> {
let profile = Profile::load()?;
let path = profile.storage.path().join(repo_id.canonical());
let repo = Repository::open(path, *repo_id)?;
let proj = repo.project()?;
Ok(proj.default_branch().clone())
}
#[allow(clippy::unwrap_used)]
fn has_file(repo_id: &RepoId, oid: &Oid, filename: &Path) -> bool {
fn helper(
repo_id: &RepoId,
oid: Oid,
filename: &Path,
) -> Result<bool, Box<dyn std::error::Error>> {
let profile = Profile::load()?;
let repo = Repository::open(profile.storage.path().join(repo_id.canonical()), *repo_id)?;
let obj = repo.backend.find_object(*oid, None);
let obj = obj?;
let commit = match obj.kind() {
None => return Ok(false),
Some(ObjectType::Any) => return Ok(false),
Some(ObjectType::Tree) => return Ok(false),
Some(ObjectType::Blob) => return Ok(false),
Some(ObjectType::Commit) => obj.into_commit(),
Some(ObjectType::Tag) => {
let tag = obj.into_tag().unwrap();
repo.backend
.find_object(tag.target_id(), None)
.unwrap()
.into_commit()
}
};
let commit = if let Ok(commit) = commit {
commit
} else {
return Ok(false);
};
let tree = commit.tree()?;
let entry = if let Ok(entry) = tree.get_path(filename) {
entry
} else {
return Ok(false);
};
let obj = entry.to_object(&repo.backend)?;
let ok = obj.into_blob().is_ok();
Ok(ok)
}
helper(repo_id, *oid, filename).unwrap_or(false)
}
#[derive(Deserialize)]
struct Filters {
filters: Vec<EventFilter>,
}
impl Filters {
fn from_file(filename: &Path) -> Result<Vec<EventFilter>, FilterError> {
let data =
std::fs::read(filename).map_err(|e| FilterError::ReadFile(filename.into(), e))?;
let filters: Self = serde_norway::from_slice(&data)
.map_err(|e| FilterError::ParseYaml(filename.into(), e))?;
Ok(filters.filters)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
use qcheck_macros::quickcheck;
use radicle::prelude::{Did, RepoId};
use crate::refs::{TagName, branch_from_str};
use super::*;
fn did() -> Did {
Did::decode("did:key:z6MkgEMYod7Hxfy9qCvDv5hYHkZ4ciWmLFgfvm3Wn1b2w2FV").unwrap()
}
fn other_did() -> Did {
Did::decode("did:key:z6MkfXa53s1ZSFy8rktvyXt5ADCojnxvjAoQpzajaXyLqG5n").unwrap()
}
fn rid() -> RepoId {
const RID: &str = "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5";
RepoId::from_urn(RID).unwrap()
}
fn other_rid() -> RepoId {
const RID: &str = "rad:zwTxygwuz5LDGBq255RA2CbNGrz8";
RepoId::from_urn(RID).unwrap()
}
fn oid_from(oid: &str) -> Oid {
Oid::try_from(oid).unwrap()
}
fn oid() -> Oid {
const OID: &str = "ff3099ba5de28d954c41d0b5a84316f943794ea4";
oid_from(OID)
}
fn other_oid() -> Oid {
const OID: &str = "bde68ac76ce093bcc583aa612f45e13fee2353a0";
oid_from(OID)
}
fn patch_id() -> PatchId {
PatchId::from(oid())
}
fn other_patch_id() -> PatchId {
PatchId::from(other_oid())
}
fn shutdown() -> CiEvent {
CiEvent::V1(CiEventV1::Shutdown)
}
fn tag_created(name: &str, did: Did, repo: RepoId, tip: Oid) -> CiEvent {
CiEvent::V1(CiEventV1::TagCreated {
from_node: did.into(),
repo,
tag: TagName::try_from(name).unwrap(),
tip,
})
}
fn all_events(
did: Did,
repo: RepoId,
branch: BranchName,
patch: PatchId,
tip: Oid,
old_tip: Oid,
) -> Vec<CiEvent> {
vec![
CiEvent::V1(CiEventV1::BranchCreated {
from_node: did.into(),
repo,
branch: branch.clone(),
tip,
}),
CiEvent::V1(CiEventV1::BranchUpdated {
from_node: did.into(),
repo,
branch: branch.clone(),
tip,
old_tip,
}),
CiEvent::V1(CiEventV1::BranchDeleted {
from_node: did.into(),
repo,
branch,
tip,
}),
CiEvent::V1(CiEventV1::PatchCreated {
from_node: did.into(),
repo,
patch,
new_tip: tip,
}),
CiEvent::V1(CiEventV1::PatchUpdated {
from_node: did.into(),
repo,
patch,
new_tip: tip,
}),
CiEvent::V1(CiEventV1::TagCreated {
from_node: did.into(),
repo,
tag: TagName::try_from("test-tag").unwrap(),
tip,
}),
]
}
#[test]
fn allows_shutdown() {
let filter = EventFilter::Repository(rid());
assert!(filter.allows(&shutdown()))
}
#[test]
fn allows_all_for_default_repository() {
let filter = EventFilter::Repository(rid());
let events = all_events(
did(),
rid(),
branch_from_str("main").unwrap(),
patch_id(),
oid(),
oid(),
);
assert!(events.iter().all(|e| filter.allows(e)));
}
#[test]
fn doesnt_allow_any_for_other_repository() {
let filter = EventFilter::Repository(rid());
let events = all_events(
did(),
other_rid(),
branch_from_str("main").unwrap(),
patch_id(),
oid(),
oid(),
);
eprintln!("filter: {filter:#?}");
for e in events.iter() {
eprintln!("{:#?} → {}", e, filter.allows(e));
assert!(!filter.allows(e));
}
}
#[test]
fn allows_all_for_main_branch() {
let filter = EventFilter::Branch(branch_from_str("main").unwrap());
let events = all_events(
did(),
rid(),
branch_from_str("main").unwrap(),
patch_id(),
oid(),
oid(),
);
eprintln!("filter: {filter:#?}");
for e in events.iter().filter(|e| {
matches!(
e,
CiEvent::V1(CiEventV1::BranchCreated { .. })
| CiEvent::V1(CiEventV1::BranchUpdated { .. })
| CiEvent::V1(CiEventV1::BranchDeleted { .. })
)
}) {
eprintln!("{:#?} → {}", e, filter.allows(e));
assert!(filter.allows(e));
}
}
#[test]
fn doesnt_allow_any_for_other_branch() {
let filter = EventFilter::Branch(branch_from_str("main").unwrap());
let events = all_events(
did(),
other_rid(),
branch_from_str("other").unwrap(),
patch_id(),
oid(),
oid(),
);
eprintln!("filter: {filter:#?}");
for e in events.iter() {
eprintln!("{:#?} → {}", e, filter.allows(e));
assert!(!filter.allows(e));
}
}
#[test]
fn allows_branch_creation() {
let filter = EventFilter::BranchCreated;
eprintln!("filter: {filter:#?}");
for e in all_events(
did(),
rid(),
branch_from_str("main").unwrap(),
patch_id(),
oid(),
oid(),
)
.iter()
.filter(|e| matches!(e, CiEvent::V1(CiEventV1::BranchCreated { .. })))
{
eprintln!("{:#?} → {}", e, filter.allows(e));
assert!(filter.allows(e));
}
}
#[test]
fn only_allows_branch_creation() {
let filter = EventFilter::BranchCreated;
eprintln!("filter: {filter:#?}");
for e in all_events(
did(),
rid(),
branch_from_str("main").unwrap(),
patch_id(),
oid(),
oid(),
)
.iter()
.filter(|e| !matches!(e, CiEvent::V1(CiEventV1::BranchCreated { .. })))
{
eprintln!("{:#?} → {}", e, filter.allows(e));
assert!(!filter.allows(e));
}
}
#[test]
fn allows_branch_update() {
let filter = EventFilter::BranchUpdated;
eprintln!("filter: {filter:#?}");
for e in all_events(
did(),
rid(),
branch_from_str("main").unwrap(),
patch_id(),
oid(),
oid(),
)
.iter()
.filter(|e| matches!(e, CiEvent::V1(CiEventV1::BranchUpdated { .. })))
{
eprintln!("{:#?} → {}", e, filter.allows(e));
assert!(filter.allows(e));
}
}
#[test]
fn only_allows_branch_update() {
let filter = EventFilter::BranchUpdated;
eprintln!("filter: {filter:#?}");
for e in all_events(
did(),
rid(),
branch_from_str("main").unwrap(),
patch_id(),
oid(),
oid(),
)
.iter()
.filter(|e| !matches!(e, CiEvent::V1(CiEventV1::BranchUpdated { .. })))
{
eprintln!("{:#?} → {}", e, filter.allows(e));
assert!(!filter.allows(e));
}
}
#[test]
fn allows_branch_deletion() {
let filter = EventFilter::BranchDeleted;
eprintln!("filter: {filter:#?}");
for e in all_events(
did(),
rid(),
branch_from_str("main").unwrap(),
patch_id(),
oid(),
oid(),
)
.iter()
.filter(|e| matches!(e, CiEvent::V1(CiEventV1::BranchDeleted { .. })))
{
eprintln!("{:#?} → {}", e, filter.allows(e));
assert!(filter.allows(e));
}
}
#[test]
fn only_allows_branch_deletion() {
let filter = EventFilter::BranchDeleted;
eprintln!("filter: {filter:#?}");
for e in all_events(
did(),
rid(),
branch_from_str("main").unwrap(),
patch_id(),
oid(),
oid(),
)
.iter()
.filter(|e| !matches!(e, CiEvent::V1(CiEventV1::BranchDeleted { .. })))
{
eprintln!("{:#?} → {}", e, filter.allows(e));
assert!(!filter.allows(e));
}
}
#[test]
fn allows_specific_patch() {
let filter = EventFilter::Patch(oid());
let events = all_events(
did(),
rid(),
branch_from_str("main").unwrap(),
patch_id(),
oid(),
oid(),
);
eprintln!("filter: {filter:#?}");
for e in events.iter().filter(|e| {
matches!(
e,
CiEvent::V1(CiEventV1::PatchCreated { .. })
| CiEvent::V1(CiEventV1::PatchUpdated { .. })
)
}) {
eprintln!("{:#?} → {}", e, filter.allows(e));
assert!(filter.allows(e));
}
}
#[test]
fn doesnt_allows_other_patch() {
let filter = EventFilter::Patch(oid());
let events = all_events(
did(),
rid(),
branch_from_str("main").unwrap(),
other_patch_id(),
oid(),
oid(),
);
eprintln!("filter: {filter:#?}");
for e in events.iter().filter(|e| {
matches!(
e,
CiEvent::V1(CiEventV1::PatchCreated { .. })
| CiEvent::V1(CiEventV1::PatchUpdated { .. })
)
}) {
eprintln!("{:#?} → {}", e, filter.allows(e));
assert!(!filter.allows(e));
}
}
#[test]
fn allows_patch_creation() {
let filter = EventFilter::PatchCreated;
eprintln!("filter: {filter:#?}");
for e in all_events(
did(),
rid(),
branch_from_str("main").unwrap(),
patch_id(),
oid(),
oid(),
)
.iter()
.filter(|e| matches!(e, CiEvent::V1(CiEventV1::PatchCreated { .. })))
{
eprintln!("{:#?} → {}", e, filter.allows(e));
assert!(filter.allows(e));
}
}
#[test]
fn only_allows_patch_creation() {
let filter = EventFilter::PatchCreated;
eprintln!("filter: {filter:#?}");
for e in all_events(
did(),
rid(),
branch_from_str("main").unwrap(),
patch_id(),
oid(),
oid(),
)
.iter()
.filter(|e| !matches!(e, CiEvent::V1(CiEventV1::PatchCreated { .. })))
{
eprintln!("{:#?} → {}", e, filter.allows(e));
assert!(!filter.allows(e));
}
}
#[test]
fn allows_patch_update() {
let filter = EventFilter::PatchUpdated;
eprintln!("filter: {filter:#?}");
for e in all_events(
did(),
rid(),
branch_from_str("main").unwrap(),
patch_id(),
oid(),
oid(),
)
.iter()
.filter(|e| matches!(e, CiEvent::V1(CiEventV1::PatchUpdated { .. })))
{
eprintln!("{:#?} → {}", e, filter.allows(e));
assert!(filter.allows(e));
}
}
#[test]
fn only_allows_patch_update() {
let filter = EventFilter::PatchUpdated;
eprintln!("filter: {filter:#?}");
for e in all_events(
did(),
rid(),
branch_from_str("main").unwrap(),
patch_id(),
oid(),
oid(),
)
.iter()
.filter(|e| !matches!(e, CiEvent::V1(CiEventV1::PatchUpdated { .. })))
{
eprintln!("{:#?} → {}", e, filter.allows(e));
assert!(!filter.allows(e));
}
}
#[test]
fn allows_all_for_right_node() {
let filter = EventFilter::Node(*did());
let events = all_events(
did(),
rid(),
branch_from_str("main").unwrap(),
patch_id(),
oid(),
oid(),
);
assert!(events.iter().all(|e| filter.allows(e)));
}
#[test]
fn allows_none_for_wrong_node() {
let filter = EventFilter::Node(*other_did());
let events = all_events(
did(),
rid(),
branch_from_str("main").unwrap(),
patch_id(),
oid(),
oid(),
);
assert!(!events.iter().any(|e| filter.allows(e)));
}
#[test]
fn allows_any_event() {
let filter = EventFilter::Allow;
eprintln!("filter: {filter:#?}");
for e in all_events(
did(),
rid(),
branch_from_str("main").unwrap(),
patch_id(),
oid(),
oid(),
)
.iter()
{
eprintln!("{:#?} → {}", e, filter.allows(e));
assert!(filter.allows(e));
}
}
#[test]
fn allows_no_event() {
let filter = EventFilter::Deny;
eprintln!("filter: {filter:#?}");
for e in all_events(
did(),
rid(),
branch_from_str("main").unwrap(),
patch_id(),
oid(),
oid(),
)
.iter()
{
eprintln!("{:#?} → {}", e, filter.allows(e));
assert!(!filter.allows(e));
}
}
#[test]
fn allows_opposite() {
let filter = EventFilter::Not(vec![EventFilter::Deny]);
eprintln!("filter: {filter:#?}");
for e in all_events(
did(),
rid(),
branch_from_str("main").unwrap(),
patch_id(),
oid(),
oid(),
)
.iter()
{
eprintln!("{:#?} → {}", e, filter.allows(e));
assert!(filter.allows(e));
}
}
#[test]
fn allows_if_all_allow() {
let filter = EventFilter::And(vec![EventFilter::Allow, EventFilter::Allow]);
eprintln!("filter: {filter:#?}");
for e in all_events(
did(),
rid(),
branch_from_str("main").unwrap(),
patch_id(),
oid(),
oid(),
)
.iter()
{
eprintln!("{:#?} → {}", e, filter.allows(e));
assert!(filter.allows(e));
}
}
#[test]
fn allows_if_any_allows() {
let filter = EventFilter::Or(vec![EventFilter::Deny, EventFilter::Allow]);
eprintln!("filter: {filter:#?}");
for e in all_events(
did(),
rid(),
branch_from_str("main").unwrap(),
patch_id(),
oid(),
oid(),
)
.iter()
{
eprintln!("{:#?} → {}", e, filter.allows(e));
assert!(filter.allows(e));
}
}
#[test]
fn deserialize_yaml_nested_not() {
let expected = EventFilter::And(vec![
EventFilter::Not(vec![EventFilter::Repository(
"rad:z32iyJDyFLqvPFzwHm8YadK4HQ2EY"
.parse::<RepoId>()
.unwrap(),
)]),
EventFilter::BranchCreated,
EventFilter::PatchCreated,
]);
let filters = r#"
!And
- !Not
- !Repository "rad:z32iyJDyFLqvPFzwHm8YadK4HQ2EY"
- !BranchCreated
- !PatchCreated
"#;
let evf = serde_norway::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_norway::Error> {
let ser = serde_norway::to_string(&filter)?;
let de = serde_norway::from_str(&ser)?;
Ok(filter == de)
}
#[test]
fn allows_wanted_tag() {
let filter = EventFilter::Tag("test-tag".to_string());
eprintln!("filter: {filter:#?}");
let e = tag_created("test-tag", did(), rid(), oid());
eprintln!("{:#?} → {}", e, filter.allows(&e));
assert!(filter.allows(&e));
}
#[test]
fn doesnt_allow_unexpected_tag() {
let filter = EventFilter::Tag("test-tag".to_string());
eprintln!("filter: {filter:#?}");
let e = tag_created("xyzzy", did(), rid(), oid());
eprintln!("{:#?} → {}", e, filter.allows(&e));
assert!(!filter.allows(&e));
}
#[test]
fn doesnt_allow_unexpected_tag_even_if_wanted_is_prefix() {
let filter = EventFilter::Tag("test-tag".to_string());
eprintln!("filter: {filter:#?}");
let e = tag_created("test-tag-with-junk", did(), rid(), oid());
eprintln!("{:#?} → {}", e, filter.allows(&e));
assert!(!filter.allows(&e));
}
#[test]
fn doesnt_allow_unexpected_tag_even_if_wanted_is_suffix() {
let filter = EventFilter::Tag("test-tag".to_string());
eprintln!("filter: {filter:#?}");
let e = tag_created("junk-test-tag", did(), rid(), oid());
eprintln!("{:#?} → {}", e, filter.allows(&e));
assert!(!filter.allows(&e));
}
}
#[derive(Debug, thiserror::Error)]
pub enum FilterError {
#[error("failed to read event filters file {0}")]
ReadFile(PathBuf, #[source] std::io::Error),
#[error("failed to parse YAML event filters file {0}")]
ParseYaml(PathBuf, #[source] serde_norway::Error),
}