| |
}
|
| |
}
|
| |
|
| - |
#[derive(Clone, Debug, Eq, PartialEq)]
|
| - |
pub enum NotificationType {
|
| - |
Patch,
|
| - |
Issue,
|
| - |
Branch,
|
| - |
Unknown,
|
| - |
}
|
| + |
pub mod filter {
|
| + |
use std::str::FromStr;
|
| |
|
| - |
#[derive(Clone, Debug, Eq, PartialEq)]
|
| - |
pub enum NotificationState {
|
| - |
Seen,
|
| - |
Unseen,
|
| - |
}
|
| + |
use nom::branch::alt;
|
| + |
use nom::bytes::complete::{tag_no_case, take, take_while1};
|
| + |
use nom::character::complete::{char, multispace0};
|
| + |
use nom::combinator::{map, value};
|
| + |
use nom::multi::{many0, separated_list1};
|
| + |
use nom::sequence::{delimited, preceded, tuple};
|
| + |
use nom::IResult;
|
| |
|
| - |
#[derive(Debug, Clone, PartialEq)]
|
| - |
pub enum NotificationFilter {
|
| - |
State(NotificationState),
|
| - |
Type(NotificationTypeFilter),
|
| - |
Author(NotificationAuthorFilter),
|
| - |
Search(String),
|
| - |
And(Vec<NotificationFilter>),
|
| - |
}
|
| + |
use radicle::prelude::Did;
|
| + |
|
| + |
use super::{Filter, Notification, NotificationKind, NotificationState, NotificationType};
|
| |
|
| - |
impl Default for NotificationFilter {
|
| - |
fn default() -> Self {
|
| - |
Self::Type(NotificationTypeFilter::Or(vec![
|
| - |
NotificationType::Issue,
|
| - |
NotificationType::Patch,
|
| - |
]))
|
| + |
#[derive(Debug, Clone, PartialEq)]
|
| + |
pub enum NotificationFilter {
|
| + |
State(NotificationState),
|
| + |
Type(NotificationTypeFilter),
|
| + |
Author(NotificationAuthorFilter),
|
| + |
Search(String),
|
| + |
And(Vec<NotificationFilter>),
|
| |
}
|
| - |
}
|
| |
|
| - |
#[derive(Debug, Clone, PartialEq)]
|
| - |
pub enum NotificationTypeFilter {
|
| - |
Single(NotificationType),
|
| - |
Or(Vec<NotificationType>),
|
| - |
}
|
| + |
impl Default for NotificationFilter {
|
| + |
fn default() -> Self {
|
| + |
Self::Type(NotificationTypeFilter::Or(vec![
|
| + |
NotificationType::Issue,
|
| + |
NotificationType::Patch,
|
| + |
]))
|
| + |
}
|
| + |
}
|
| |
|
| - |
#[derive(Debug, Clone, PartialEq)]
|
| - |
pub enum NotificationAuthorFilter {
|
| - |
Single(Did),
|
| - |
Or(Vec<Did>),
|
| - |
}
|
| + |
#[derive(Debug, Clone, PartialEq)]
|
| + |
pub enum NotificationTypeFilter {
|
| + |
Single(NotificationType),
|
| + |
Or(Vec<NotificationType>),
|
| + |
}
|
| |
|
| - |
impl Filter<Notification> for NotificationFilter {
|
| - |
fn matches(&self, notif: &Notification) -> bool {
|
| - |
use fuzzy_matcher::skim::SkimMatcherV2;
|
| - |
use fuzzy_matcher::FuzzyMatcher;
|
| - |
|
| - |
let matcher = SkimMatcherV2::default();
|
| - |
|
| - |
let match_type = |type_name: &NotificationType| match type_name {
|
| - |
NotificationType::Issue => matches!(¬if.kind, NotificationKind::Cob {
|
| - |
type_name,
|
| - |
summary: _,
|
| - |
status: _,
|
| - |
id: _,
|
| - |
} if type_name == "issue"),
|
| - |
NotificationType::Patch => matches!(¬if.kind, NotificationKind::Cob {
|
| - |
type_name,
|
| - |
summary: _,
|
| - |
status: _,
|
| - |
id: _,
|
| - |
} if type_name == "patch"),
|
| - |
NotificationType::Branch => {
|
| - |
matches!(notif.kind, NotificationKind::Branch { .. })
|
| - |
}
|
| - |
NotificationType::Unknown => {
|
| - |
matches!(notif.kind, NotificationKind::Unknown { .. })
|
| - |
}
|
| - |
};
|
| + |
#[derive(Debug, Clone, PartialEq)]
|
| + |
pub enum NotificationAuthorFilter {
|
| + |
Single(Did),
|
| + |
Or(Vec<Did>),
|
| + |
}
|
| |
|
| - |
match self {
|
| - |
NotificationFilter::State(state) => match state {
|
| - |
NotificationState::Seen => notif.seen,
|
| - |
NotificationState::Unseen => !notif.seen,
|
| - |
},
|
| - |
NotificationFilter::Type(type_filter) => match type_filter {
|
| - |
NotificationTypeFilter::Single(type_name) => match_type(type_name),
|
| - |
NotificationTypeFilter::Or(types) => types.iter().any(|other| match_type(other)),
|
| - |
},
|
| - |
NotificationFilter::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)),
|
| - |
},
|
| - |
NotificationFilter::Search(search) => {
|
| - |
let summary = match ¬if.kind {
|
| - |
NotificationKind::Cob {
|
| - |
type_name: _,
|
| - |
summary,
|
| - |
status: _,
|
| - |
id: _,
|
| - |
} => summary,
|
| - |
NotificationKind::Branch {
|
| - |
name: _,
|
| - |
summary,
|
| - |
status: _,
|
| - |
id: _,
|
| - |
} => summary,
|
| - |
NotificationKind::Unknown { refname: _ } => "",
|
| - |
};
|
| - |
match matcher.fuzzy_match(summary, search) {
|
| - |
Some(score) => score == 0 || score > 60,
|
| - |
_ => false,
|
| + |
impl Filter<Notification> for NotificationFilter {
|
| + |
fn matches(&self, notif: &Notification) -> bool {
|
| + |
use fuzzy_matcher::skim::SkimMatcherV2;
|
| + |
use fuzzy_matcher::FuzzyMatcher;
|
| + |
|
| + |
let matcher = SkimMatcherV2::default();
|
| + |
|
| + |
let match_type = |type_name: &NotificationType| match type_name {
|
| + |
NotificationType::Issue => matches!(¬if.kind, NotificationKind::Cob {
|
| + |
type_name,
|
| + |
summary: _,
|
| + |
status: _,
|
| + |
id: _,
|
| + |
} if type_name == "issue"),
|
| + |
NotificationType::Patch => matches!(¬if.kind, NotificationKind::Cob {
|
| + |
type_name,
|
| + |
summary: _,
|
| + |
status: _,
|
| + |
id: _,
|
| + |
} if type_name == "patch"),
|
| + |
NotificationType::Branch => {
|
| + |
matches!(notif.kind, NotificationKind::Branch { .. })
|
| + |
}
|
| + |
NotificationType::Unknown => {
|
| + |
matches!(notif.kind, NotificationKind::Unknown { .. })
|
| + |
}
|
| + |
};
|
| + |
|
| + |
match self {
|
| + |
NotificationFilter::State(state) => match state {
|
| + |
NotificationState::Seen => notif.seen,
|
| + |
NotificationState::Unseen => !notif.seen,
|
| + |
},
|
| + |
NotificationFilter::Type(type_filter) => match type_filter {
|
| + |
NotificationTypeFilter::Single(type_name) => match_type(type_name),
|
| + |
NotificationTypeFilter::Or(types) => {
|
| + |
types.iter().any(|other| match_type(other))
|
| + |
}
|
| + |
},
|
| + |
NotificationFilter::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)),
|
| + |
},
|
| + |
NotificationFilter::Search(search) => {
|
| + |
let summary = match ¬if.kind {
|
| + |
NotificationKind::Cob {
|
| + |
type_name: _,
|
| + |
summary,
|
| + |
status: _,
|
| + |
id: _,
|
| + |
} => summary,
|
| + |
NotificationKind::Branch {
|
| + |
name: _,
|
| + |
summary,
|
| + |
status: _,
|
| + |
id: _,
|
| + |
} => summary,
|
| + |
NotificationKind::Unknown { refname: _ } => "",
|
| + |
};
|
| + |
match matcher.fuzzy_match(summary, search) {
|
| + |
Some(score) => score == 0 || score > 60,
|
| + |
_ => false,
|
| + |
}
|
| |
}
|
| + |
NotificationFilter::And(filters) => filters.iter().all(|f| f.matches(notif)),
|
| |
}
|
| - |
NotificationFilter::And(filters) => filters.iter().all(|f| f.matches(notif)),
|
| |
}
|
| |
}
|
| - |
}
|
| |
|
| - |
impl FromStr for NotificationFilter {
|
| - |
type Err = anyhow::Error;
|
| - |
|
| - |
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,
|
| - |
))),
|
| + |
impl FromStr for NotificationFilter {
|
| + |
type Err = anyhow::Error;
|
| + |
|
| + |
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,
|
| + |
))),
|
| + |
}
|
| |
}
|
| - |
}
|
| |
|
| - |
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(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, NotificationFilter> {
|
| - |
map(
|
| - |
preceded(
|
| - |
tuple((
|
| - |
tag_no_case("state"),
|
| - |
multispace0,
|
| - |
tag_no_case("="),
|
| - |
multispace0,
|
| - |
)),
|
| - |
parse_state,
|
| - |
),
|
| - |
NotificationFilter::State,
|
| - |
)(input)
|
| - |
}
|
| + |
fn parse_state_filter(input: &str) -> IResult<&str, NotificationFilter> {
|
| + |
map(
|
| + |
preceded(
|
| + |
tuple((
|
| + |
tag_no_case("state"),
|
| + |
multispace0,
|
| + |
tag_no_case("="),
|
| + |
multispace0,
|
| + |
)),
|
| + |
parse_state,
|
| + |
),
|
| + |
NotificationFilter::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(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_single(input: &str) -> IResult<&str, NotificationTypeFilter> {
|
| + |
map(parse_type, NotificationTypeFilter::Single)(input)
|
| + |
}
|
| |
|
| - |
fn parse_type_or(input: &str) -> IResult<&str, NotificationTypeFilter> {
|
| - |
map(
|
| - |
delimited(
|
| - |
tuple((multispace0, char('('), multispace0)),
|
| - |
separated_list1(
|
| - |
delimited(multispace0, tag_no_case("or"), multispace0),
|
| - |
parse_type,
|
| + |
fn parse_type_or(input: &str) -> IResult<&str, NotificationTypeFilter> {
|
| + |
map(
|
| + |
delimited(
|
| + |
tuple((multispace0, char('('), multispace0)),
|
| + |
separated_list1(
|
| + |
delimited(multispace0, tag_no_case("or"), multispace0),
|
| + |
parse_type,
|
| + |
),
|
| + |
tuple((multispace0, char(')'), multispace0)),
|
| |
),
|
| - |
tuple((multispace0, char(')'), multispace0)),
|
| - |
),
|
| - |
NotificationTypeFilter::Or,
|
| - |
)(input)
|
| - |
}
|
| - |
|
| - |
fn parse_type_filter(input: &str) -> IResult<&str, NotificationFilter> {
|
| - |
map(
|
| - |
preceded(
|
| - |
tuple((
|
| - |
tag_no_case("type"),
|
| - |
multispace0,
|
| - |
tag_no_case("="),
|
| - |
multispace0,
|
| - |
)),
|
| - |
alt((parse_type_or, parse_type_single)),
|
| - |
),
|
| - |
NotificationFilter::Type,
|
| - |
)(input)
|
| - |
}
|
| - |
|
| - |
fn parse_author_single(input: &str) -> IResult<&str, NotificationAuthorFilter> {
|
| - |
map(parse_did, NotificationAuthorFilter::Single)(input)
|
| - |
}
|
| + |
NotificationTypeFilter::Or,
|
| + |
)(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),
|
| + |
fn parse_type_filter(input: &str) -> IResult<&str, NotificationFilter> {
|
| + |
map(
|
| + |
preceded(
|
| + |
tuple((
|
| + |
tag_no_case("type"),
|
| + |
multispace0,
|
| + |
tag_no_case("="),
|
| + |
multispace0,
|
| + |
)),
|
| + |
alt((parse_type_or, parse_type_single)),
|
| |
),
|
| - |
tuple((multispace0, char(')'), multispace0)),
|
| - |
),
|
| - |
|dids: Vec<&str>| {
|
| - |
NotificationAuthorFilter::Or(
|
| - |
dids.iter()
|
| - |
.filter_map(|did| Did::from_str(did).ok())
|
| - |
.collect::<Vec<_>>(),
|
| - |
)
|
| - |
},
|
| - |
)(input)
|
| - |
}
|
| + |
NotificationFilter::Type,
|
| + |
)(input)
|
| + |
}
|
| |
|
| - |
fn parse_author_filter(input: &str) -> IResult<&str, NotificationFilter> {
|
| - |
map(
|
| - |
preceded(
|
| - |
tuple((
|
| - |
tag_no_case("author"),
|
| - |
multispace0,
|
| - |
tag_no_case("="),
|
| - |
multispace0,
|
| - |
)),
|
| - |
alt((parse_author_single, parse_author_or)),
|
| - |
),
|
| - |
NotificationFilter::Author,
|
| - |
)(input)
|
| - |
}
|
| + |
fn parse_author_single(input: &str) -> IResult<&str, NotificationAuthorFilter> {
|
| + |
map(parse_did, NotificationAuthorFilter::Single)(input)
|
| + |
}
|
| |
|
| - |
fn parse_search_filter(input: &str) -> IResult<&str, NotificationFilter> {
|
| - |
map(
|
| - |
take_while1(|c: char| c.is_alphanumeric() || c == '_' || c == '-'),
|
| - |
|s: &str| NotificationFilter::Search(s.to_string()),
|
| - |
)(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_single_filter(input: &str) -> IResult<&str, NotificationFilter> {
|
| - |
alt((
|
| - |
parse_state_filter,
|
| - |
parse_type_filter,
|
| - |
parse_author_filter,
|
| - |
parse_search_filter,
|
| - |
))(input)
|
| - |
}
|
| + |
fn parse_author_filter(input: &str) -> IResult<&str, NotificationFilter> {
|
| + |
map(
|
| + |
preceded(
|
| + |
tuple((
|
| + |
tag_no_case("author"),
|
| + |
multispace0,
|
| + |
tag_no_case("="),
|
| + |
multispace0,
|
| + |
)),
|
| + |
alt((parse_author_single, parse_author_or)),
|
| + |
),
|
| + |
NotificationFilter::Author,
|
| + |
)(input)
|
| + |
}
|
| |
|
| - |
fn parse_filters(input: &str) -> IResult<&str, Vec<NotificationFilter>> {
|
| - |
many0(preceded(multispace0, parse_single_filter))(input)
|
| - |
}
|
| + |
fn parse_search_filter(input: &str) -> IResult<&str, NotificationFilter> {
|
| + |
map(
|
| + |
take_while1(|c: char| c.is_alphanumeric() || c == '_' || c == '-'),
|
| + |
|s: &str| NotificationFilter::Search(s.to_string()),
|
| + |
)(input)
|
| + |
}
|
| |
|
| - |
let parse_filter_expression = |input: &str| -> Result<NotificationFilter, String> {
|
| - |
match parse_filters(input) {
|
| - |
Ok((remaining, filters)) => {
|
| - |
let remaining = remaining.trim();
|
| - |
if !remaining.is_empty() {
|
| - |
return Err(format!("Unparsed input remaining: '{}'", remaining));
|
| - |
}
|
| + |
fn parse_single_filter(input: &str) -> IResult<&str, NotificationFilter> {
|
| + |
alt((
|
| + |
parse_state_filter,
|
| + |
parse_type_filter,
|
| + |
parse_author_filter,
|
| + |
parse_search_filter,
|
| + |
))(input)
|
| + |
}
|
| |
|
| - |
if filters.is_empty() {
|
| - |
return Err("No filters provided".to_string());
|
| - |
}
|
| + |
fn parse_filters(input: &str) -> IResult<&str, Vec<NotificationFilter>> {
|
| + |
many0(preceded(multispace0, parse_single_filter))(input)
|
| + |
}
|
| |
|
| - |
if filters.len() == 1 {
|
| - |
Ok(filters.into_iter().next().unwrap())
|
| - |
} else {
|
| - |
Ok(NotificationFilter::And(filters))
|
| + |
let parse_filter_expression = |input: &str| -> Result<NotificationFilter, 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 {
|
| + |
Ok(NotificationFilter::And(filters))
|
| + |
}
|
| |
}
|
| + |
Err(e) => Err(format!("Parse error: {}", e)),
|
| |
}
|
| - |
Err(e) => Err(format!("Parse error: {}", e)),
|
| - |
}
|
| - |
};
|
| + |
};
|
| |
|
| - |
parse_filter_expression(filter_exp).map_err(|err| anyhow::format_err!(err))
|
| + |
parse_filter_expression(filter_exp).map_err(|err| anyhow::format_err!(err))
|
| + |
}
|
| |
}
|
| |
}
|
| |
|
| |
#[cfg(test)]
|
| |
mod tests {
|
| + |
use std::str::FromStr;
|
| + |
|
| |
use anyhow::Result;
|
| + |
use radicle::prelude::Did;
|
| |
|
| + |
use super::filter::*;
|
| |
use super::*;
|
| |
|
| |
#[test]
|