| |
Unseen,
|
| |
}
|
| |
|
| - |
#[derive(Clone, Default, Debug, Eq, PartialEq)]
|
| - |
pub struct NotificationItemFilter {
|
| - |
state: Option<NotificationState>,
|
| - |
type_name: Option<NotificationType>,
|
| - |
authors: Vec<Did>,
|
| - |
search: Option<String>,
|
| + |
#[derive(Debug, Clone, PartialEq)]
|
| + |
pub enum NotificationItemFilter {
|
| + |
State(NotificationState),
|
| + |
Type(NotificationTypeFilter),
|
| + |
Author(NotificationAuthorFilter),
|
| + |
Search(String),
|
| + |
And(Vec<NotificationItemFilter>),
|
| |
}
|
| |
|
| - |
impl NotificationItemFilter {
|
| - |
pub fn state(&self) -> Option<NotificationState> {
|
| - |
self.state.clone()
|
| + |
impl Default for NotificationItemFilter {
|
| + |
fn default() -> Self {
|
| + |
Self::Type(NotificationTypeFilter::Or(vec![
|
| + |
NotificationType::Issue,
|
| + |
NotificationType::Patch,
|
| + |
]))
|
| |
}
|
| |
}
|
| |
|
| + |
#[derive(Debug, Clone, PartialEq)]
|
| + |
pub enum NotificationTypeFilter {
|
| + |
Single(NotificationType),
|
| + |
Or(Vec<NotificationType>),
|
| + |
}
|
| + |
|
| + |
#[derive(Debug, Clone, PartialEq)]
|
| + |
pub enum NotificationAuthorFilter {
|
| + |
Single(Did),
|
| + |
Or(Vec<Did>),
|
| + |
}
|
| + |
|
| |
impl Filter<NotificationItem> for NotificationItemFilter {
|
| |
fn matches(&self, notif: &NotificationItem) -> bool {
|
| |
use fuzzy_matcher::skim::SkimMatcherV2;
|
| |
|
| |
let matcher = SkimMatcherV2::default();
|
| |
|
| - |
let matches_state = match self.state {
|
| - |
Some(NotificationState::Seen) => notif.seen,
|
| - |
Some(NotificationState::Unseen) => !notif.seen,
|
| - |
None => true,
|
| - |
};
|
| - |
|
| - |
let matches_type = match self.type_name {
|
| - |
Some(NotificationType::Patch) => matches!(¬if.kind, NotificationKindItem::Cob {
|
| - |
type_name,
|
| - |
summary: _,
|
| - |
status: _,
|
| - |
id: _,
|
| - |
} if type_name == "patch"),
|
| - |
Some(NotificationType::Issue) => matches!(¬if.kind, NotificationKindItem::Cob {
|
| - |
type_name,
|
| - |
summary: _,
|
| - |
status: _,
|
| - |
id: _,
|
| - |
} if type_name == "issue"),
|
| - |
Some(NotificationType::Branch) => {
|
| + |
let match_type = |type_name: &NotificationType| match type_name {
|
| + |
NotificationType::Issue => matches!(¬if.kind, NotificationKindItem::Cob {
|
| + |
type_name,
|
| + |
summary: _,
|
| + |
status: _,
|
| + |
id: _,
|
| + |
} if type_name == "issue"),
|
| + |
NotificationType::Patch => matches!(¬if.kind, NotificationKindItem::Cob {
|
| + |
type_name,
|
| + |
summary: _,
|
| + |
status: _,
|
| + |
id: _,
|
| + |
} if type_name == "patch"),
|
| + |
NotificationType::Branch => {
|
| |
matches!(notif.kind, NotificationKindItem::Branch { .. })
|
| |
}
|
| - |
None => true,
|
| - |
};
|
| - |
|
| - |
let matches_authors = if !self.authors.is_empty() {
|
| - |
{
|
| - |
self.authors
|
| - |
.iter()
|
| - |
.any(|other| notif.author.nid == Some(**other))
|
| + |
NotificationType::Unknown => {
|
| + |
matches!(notif.kind, NotificationKindItem::Unknown { .. })
|
| |
}
|
| - |
} else {
|
| - |
true
|
| |
};
|
| |
|
| - |
let matches_search = match &self.search {
|
| - |
Some(search) => {
|
| + |
match self {
|
| + |
NotificationItemFilter::State(state) => match state {
|
| + |
NotificationState::Seen => notif.seen,
|
| + |
NotificationState::Unseen => !notif.seen,
|
| + |
},
|
| + |
NotificationItemFilter::Type(type_filter) => match type_filter {
|
| + |
NotificationTypeFilter::Single(type_name) => match_type(type_name),
|
| + |
NotificationTypeFilter::Or(types) => types.iter().any(|other| match_type(other)),
|
| + |
},
|
| + |
NotificationItemFilter::Author(author_filter) => match author_filter {
|
| + |
NotificationAuthorFilter::Single(author) => notif.author.nid == Some(**author),
|
| + |
NotificationAuthorFilter::Or(authors) => authors
|
| + |
.iter()
|
| + |
.any(|other| notif.author.nid == Some(**other)),
|
| + |
},
|
| + |
NotificationItemFilter::Search(search) => {
|
| |
let summary = match ¬if.kind {
|
| |
NotificationKindItem::Cob {
|
| |
type_name: _,
|
| |
_ => false,
|
| |
}
|
| |
}
|
| - |
None => true,
|
| - |
};
|
| - |
|
| - |
matches_state && matches_type && matches_authors && matches_search
|
| + |
NotificationItemFilter::And(filters) => filters.iter().all(|f| f.matches(notif)),
|
| + |
}
|
| |
}
|
| |
}
|
| |
|
| |
impl FromStr for NotificationItemFilter {
|
| |
type Err = anyhow::Error;
|
| |
|
| - |
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
| - |
let mut state = None;
|
| - |
let mut type_name = None;
|
| - |
let mut search = String::new();
|
| - |
let mut authors = vec![];
|
| + |
fn from_str(filter_exp: &str) -> Result<Self, Self::Err> {
|
| + |
fn parse_did(input: &str) -> IResult<&str, Did> {
|
| + |
match Did::from_str(input) {
|
| + |
Ok(did) => IResult::Ok(("", did)),
|
| + |
Err(_) => IResult::Err(nom::Err::Error(nom::error::Error::new(
|
| + |
input,
|
| + |
nom::error::ErrorKind::Verify,
|
| + |
))),
|
| + |
}
|
| + |
}
|
| |
|
| - |
let mut authors_parser = |input| -> IResult<&str, Vec<&str>> {
|
| - |
preceded(
|
| - |
tag("authors:"),
|
| + |
fn parse_state(input: &str) -> IResult<&str, NotificationState> {
|
| + |
alt((
|
| + |
value(NotificationState::Seen, tag_no_case("seen")),
|
| + |
value(NotificationState::Unseen, tag_no_case("unseen")),
|
| + |
))(input)
|
| + |
}
|
| + |
|
| + |
fn parse_state_filter(input: &str) -> IResult<&str, NotificationItemFilter> {
|
| + |
map(
|
| + |
preceded(
|
| + |
tuple((
|
| + |
tag_no_case("state"),
|
| + |
multispace0,
|
| + |
tag_no_case("="),
|
| + |
multispace0,
|
| + |
)),
|
| + |
parse_state,
|
| + |
),
|
| + |
NotificationItemFilter::State,
|
| + |
)(input)
|
| + |
}
|
| + |
|
| + |
fn parse_type(input: &str) -> IResult<&str, NotificationType> {
|
| + |
alt((
|
| + |
value(NotificationType::Patch, tag_no_case("patch")),
|
| + |
value(NotificationType::Issue, tag_no_case("issue")),
|
| + |
value(NotificationType::Branch, tag_no_case("branch")),
|
| + |
value(NotificationType::Unknown, tag_no_case("unknown")),
|
| + |
))(input)
|
| + |
}
|
| + |
|
| + |
fn parse_type_single(input: &str) -> IResult<&str, NotificationTypeFilter> {
|
| + |
map(parse_type, NotificationTypeFilter::Single)(input)
|
| + |
}
|
| + |
|
| + |
fn parse_type_or(input: &str) -> IResult<&str, NotificationTypeFilter> {
|
| + |
map(
|
| |
delimited(
|
| - |
tag("["),
|
| - |
separated_list0(tag(","), take(56_usize)),
|
| - |
tag("]"),
|
| + |
tuple((multispace0, char('('), multispace0)),
|
| + |
separated_list1(
|
| + |
delimited(multispace0, tag_no_case("or"), multispace0),
|
| + |
parse_type,
|
| + |
),
|
| + |
tuple((multispace0, char(')'), multispace0)),
|
| |
),
|
| + |
NotificationTypeFilter::Or,
|
| |
)(input)
|
| - |
};
|
| + |
}
|
| |
|
| - |
let parts = value.split(' ');
|
| - |
for part in parts {
|
| - |
match part {
|
| - |
"is:seen" => state = Some(NotificationState::Seen),
|
| - |
"is:unseen" => state = Some(NotificationState::Unseen),
|
| - |
"is:patch" => type_name = Some(NotificationType::Patch),
|
| - |
"is:issue" => type_name = Some(NotificationType::Issue),
|
| - |
"is:branch" => type_name = Some(NotificationType::Branch),
|
| - |
other => {
|
| - |
if let Ok((_, dids)) = authors_parser.parse(other) {
|
| - |
for did in dids {
|
| - |
authors.push(Did::from_str(did)?);
|
| - |
}
|
| + |
fn parse_type_filter(input: &str) -> IResult<&str, NotificationItemFilter> {
|
| + |
map(
|
| + |
preceded(
|
| + |
tuple((
|
| + |
tag_no_case("type"),
|
| + |
multispace0,
|
| + |
tag_no_case("="),
|
| + |
multispace0,
|
| + |
)),
|
| + |
alt((parse_type_or, parse_type_single)),
|
| + |
),
|
| + |
NotificationItemFilter::Type,
|
| + |
)(input)
|
| + |
}
|
| + |
|
| + |
fn parse_author_single(input: &str) -> IResult<&str, NotificationAuthorFilter> {
|
| + |
map(parse_did, NotificationAuthorFilter::Single)(input)
|
| + |
}
|
| + |
|
| + |
fn parse_author_or(input: &str) -> IResult<&str, NotificationAuthorFilter> {
|
| + |
map(
|
| + |
delimited(
|
| + |
tuple((multispace0, char('('), multispace0)),
|
| + |
separated_list1(
|
| + |
delimited(multispace0, tag_no_case("or"), multispace0),
|
| + |
take(56_usize),
|
| + |
),
|
| + |
tuple((multispace0, char(')'), multispace0)),
|
| + |
),
|
| + |
|dids: Vec<&str>| {
|
| + |
NotificationAuthorFilter::Or(
|
| + |
dids.iter()
|
| + |
.filter_map(|did| Did::from_str(did).ok())
|
| + |
.collect::<Vec<_>>(),
|
| + |
)
|
| + |
},
|
| + |
)(input)
|
| + |
}
|
| + |
|
| + |
fn parse_author_filter(input: &str) -> IResult<&str, NotificationItemFilter> {
|
| + |
map(
|
| + |
preceded(
|
| + |
tuple((
|
| + |
tag_no_case("author"),
|
| + |
multispace0,
|
| + |
tag_no_case("="),
|
| + |
multispace0,
|
| + |
)),
|
| + |
alt((parse_author_single, parse_author_or)),
|
| + |
),
|
| + |
NotificationItemFilter::Author,
|
| + |
)(input)
|
| + |
}
|
| + |
|
| + |
fn parse_search_filter(input: &str) -> IResult<&str, NotificationItemFilter> {
|
| + |
map(
|
| + |
take_while1(|c: char| c.is_alphanumeric() || c == '_' || c == '-'),
|
| + |
|s: &str| NotificationItemFilter::Search(s.to_string()),
|
| + |
)(input)
|
| + |
}
|
| + |
|
| + |
fn parse_single_filter(input: &str) -> IResult<&str, NotificationItemFilter> {
|
| + |
alt((
|
| + |
parse_state_filter,
|
| + |
parse_type_filter,
|
| + |
parse_author_filter,
|
| + |
parse_search_filter,
|
| + |
))(input)
|
| + |
}
|
| + |
|
| + |
fn parse_filters(input: &str) -> IResult<&str, Vec<NotificationItemFilter>> {
|
| + |
many0(preceded(multispace0, parse_single_filter))(input)
|
| + |
}
|
| + |
|
| + |
let parse_filter_expression = |input: &str| -> Result<NotificationItemFilter, String> {
|
| + |
match parse_filters(input) {
|
| + |
Ok((remaining, filters)) => {
|
| + |
let remaining = remaining.trim();
|
| + |
if !remaining.is_empty() {
|
| + |
return Err(format!("Unparsed input remaining: '{}'", remaining));
|
| + |
}
|
| + |
|
| + |
if filters.is_empty() {
|
| + |
return Err("No filters provided".to_string());
|
| + |
}
|
| + |
|
| + |
if filters.len() == 1 {
|
| + |
Ok(filters.into_iter().next().unwrap())
|
| |
} else {
|
| - |
search.push_str(other);
|
| + |
Ok(NotificationItemFilter::And(filters))
|
| |
}
|
| |
}
|
| + |
Err(e) => Err(format!("Parse error: {}", e)),
|
| |
}
|
| - |
}
|
| + |
};
|
| |
|
| - |
Ok(Self {
|
| - |
state,
|
| - |
type_name,
|
| - |
authors,
|
| - |
search: Some(search),
|
| - |
})
|
| + |
parse_filter_expression(filter_exp).map_err(|err| anyhow::format_err!(err))
|
| |
}
|
| |
}
|
| |
|
| |
}
|
| |
|
| |
#[test]
|
| - |
fn notification_item_filter_from_str_should_succeed() -> Result<()> {
|
| - |
let search = r#"is:seen is:patch authors:[did:key:z6MkkpTPzcq1ybmjQyQpyre15JUeMvZY6toxoZVpLZ8YarsB,did:key:z6Mku8hpprWTmCv3BqkssCYDfr2feUdyLSUnycVajFo9XVAx] cli"#;
|
| + |
fn notification_item_filter_with_type_should_succeed() -> Result<()> {
|
| + |
let search = r#"type=patch"#;
|
| |
let actual = NotificationItemFilter::from_str(search)?;
|
| |
|
| - |
let expected = NotificationItemFilter {
|
| - |
state: Some(NotificationState::Seen),
|
| - |
type_name: Some(NotificationType::Patch),
|
| - |
authors: vec![
|
| + |
let expected =
|
| + |
NotificationItemFilter::Type(NotificationTypeFilter::Single(NotificationType::Patch));
|
| + |
|
| + |
assert_eq!(expected, actual);
|
| + |
|
| + |
Ok(())
|
| + |
}
|
| + |
|
| + |
#[test]
|
| + |
fn notification_item_filter_with_author_should_succeed() -> Result<()> {
|
| + |
let search = r#"author=did:key:z6MkkpTPzcq1ybmjQyQpyre15JUeMvZY6toxoZVpLZ8YarsB"#;
|
| + |
let actual = NotificationItemFilter::from_str(search)?;
|
| + |
|
| + |
let expected = NotificationItemFilter::Author(NotificationAuthorFilter::Single(
|
| + |
Did::from_str("did:key:z6MkkpTPzcq1ybmjQyQpyre15JUeMvZY6toxoZVpLZ8YarsB")?,
|
| + |
));
|
| + |
|
| + |
assert_eq!(expected, actual);
|
| + |
|
| + |
Ok(())
|
| + |
}
|
| + |
|
| + |
#[test]
|
| + |
fn notification_item_filter_with_author_should_not_succeed() -> Result<()> {
|
| + |
let search = r#"author=foo"#;
|
| + |
let result = NotificationItemFilter::from_str(search);
|
| + |
|
| + |
println!("{result:?}");
|
| + |
|
| + |
assert!(matches!(result.unwrap_err(), anyhow::Error { .. }));
|
| + |
|
| + |
Ok(())
|
| + |
}
|
| + |
|
| + |
#[test]
|
| + |
fn notification_item_filter_with_all_should_succeed() -> Result<()> {
|
| + |
let search = r#"state=seen type=(patch or issue) author=(did:key:z6MkkpTPzcq1ybmjQyQpyre15JUeMvZY6toxoZVpLZ8YarsB or did:key:z6Mku8hpprWTmCv3BqkssCYDfr2feUdyLSUnycVajFo9XVAx) cli"#;
|
| + |
let actual = NotificationItemFilter::from_str(search)?;
|
| + |
|
| + |
let expected = NotificationItemFilter::And(vec![
|
| + |
NotificationItemFilter::State(NotificationState::Seen),
|
| + |
NotificationItemFilter::Type(NotificationTypeFilter::Or(vec![
|
| + |
NotificationType::Patch,
|
| + |
NotificationType::Issue,
|
| + |
])),
|
| + |
NotificationItemFilter::Author(NotificationAuthorFilter::Or(vec![
|
| |
Did::from_str("did:key:z6MkkpTPzcq1ybmjQyQpyre15JUeMvZY6toxoZVpLZ8YarsB")?,
|
| |
Did::from_str("did:key:z6Mku8hpprWTmCv3BqkssCYDfr2feUdyLSUnycVajFo9XVAx")?,
|
| - |
],
|
| - |
search: Some("cli".to_string()),
|
| - |
};
|
| + |
])),
|
| + |
NotificationItemFilter::Search("cli".to_string()),
|
| + |
]);
|
| |
|
| |
assert_eq!(expected, actual);
|
| |
|