Skip to content

Commit

Permalink
feat(term): parse feed category and requirement
Browse files Browse the repository at this point in the history
  • Loading branch information
ymgyt committed Mar 31, 2024
1 parent 5f476cc commit 17b6288
Show file tree
Hide file tree
Showing 14 changed files with 362 additions and 54 deletions.
87 changes: 87 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ http = { version = "0.2" }
itertools = { version = "0.12", default-features = false, features = ["use_std"] }
kvsd = { version = "0.1.3", default-features = false }
moka = { version = "0.12.4", features = ["future"] }
nutype = { version = "0.4.0", default-features = false }
parse_duration = { version = "2.1.1" }
rand = { version = "0.8.5" }
reqwest = { version = "0.11.24", default-features = false, features = ["rustls-tls", "json"] }
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Options:
### Authentication

syndicationd maintains state (such as subscribed feeds) on the backend, and therefore requires authentication to make requests.
Currently, GitHub and Google are supported. The only scope syndicationd requires is [`user:email`](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps)(Github) or [`email`](https://developers.google.com/identity/gsi/web/guides/devices#obtain_a_user_code_and_verification_url)(Google) to read the user's email. the user's email is used only as an identifier after being hashed and never stored.
Currently, GitHub and Google are supported as authorize server/id provider. The only scope syndicationd requires is [`user:email`](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps)(Github) or [`email`](https://developers.google.com/identity/gsi/web/guides/devices#obtain_a_user_code_and_verification_url)(Google) to read the user's email. the user's email is used only as an identifier after being hashed and never stored.

### Keymap

Expand Down Expand Up @@ -184,7 +184,7 @@ The theme can be changed using the `--theme` flag. Please refer to the help for

### Backend api

By default, use `https://api.syndicationd.ymgyt.io` as the [backend api](./crates/synd_api)([hosted on my home Raspberry Pi](https://github.com/ymgyt/mynix/blob/main/homeserver/modules/syndicationd/default.nix)).
By default, `synd` use `https://api.syndicationd.ymgyt.io` as the [backend api](./crates/synd_api)([hosted on my home Raspberry Pi](https://github.com/ymgyt/mynix/blob/main/homeserver/modules/syndicationd/default.nix)).
To change the endpoint, specify the `--endpoint` flag

The hosted api is instrumented with OpenTelemetry. Basic signals(traces,metrics,logs) are published on the [Grafana dashboard](https://ymgyt.grafana.net/public-dashboards/863ebddd82c44ddd9a28a68eaac848ff?orgId=1&refresh=1h&from=now-1h&to=now)
Expand Down
2 changes: 2 additions & 0 deletions crates/synd_term/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ futures-util = "0.3.30"
graphql_client = { workspace = true }
html2text = { version = "0.12" }
itertools = { workspace = true }
nom = { version = "7.1.3", default-features = false, features = ["std"] }
nutype = { workspace = true }
open = "5.1.0"
parse_duration = { workspace = true }
ratatui = { version = "0.26.0" }
Expand Down
186 changes: 162 additions & 24 deletions crates/synd_term/src/application/input_parser.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
use thiserror::Error;
use url::Url;

use crate::types::{self, SubscribeFeedInput};

pub use feed::requirement as parse_requirement;

type NomError<'s> = nom::error::Error<&'s str>;

#[derive(Error, Debug, PartialEq, Eq)]
pub(super) enum ParseFeedUrlError {
#[error("invalid feed url: `{input}`: {err}")]
InvalidUrl { input: String, err: url::ParseError },
#[error("no input")]
NoInput,
pub(super) enum ParseFeedError {
#[error("parse feed error: {0}")]
Parse(String),
}

pub(super) struct InputParser<'a> {
Expand All @@ -20,33 +23,167 @@ impl<'a> InputParser<'a> {
# Example:
# https://this-week-in-rust.org/atom.xml
";

pub(super) fn new(input: &'a str) -> Self {
Self { input }
}

pub(super) fn parse_feed_url(&self) -> Result<&'a str, ParseFeedUrlError> {
match self
.input
.lines()
.map(str::trim)
.filter(|s| !s.starts_with('#') && !s.is_empty())
.map(|s| (Url::parse(s), s))
.next()
{
Some((Ok(_), input)) => Ok(input),
Some((Err(parse_err), input)) => Err(ParseFeedUrlError::InvalidUrl {
input: input.to_owned(),
err: parse_err,
}),
None => Err(ParseFeedUrlError::NoInput),
pub(super) fn parse_feed_subscription(&self) -> Result<SubscribeFeedInput, ParseFeedError> {
feed::parse(self.input).map_err(|e| ParseFeedError::Parse(e.to_string()))
}

pub(super) fn edit_feed_prompt(feed: &types::Feed) -> String {
format!(
"{}\n{feed_url}",
Self::SUSBSCRIBE_FEED_PROMPT,
feed_url = feed.url,
)
}
}

mod feed {
use nom::{
branch::alt,
bytes::complete::{tag_no_case, take_while, take_while_m_n},
character::complete::{multispace0, multispace1},
combinator::{map, value},
sequence::{delimited, Tuple},
Finish, IResult, Parser,
};

use super::NomError;
use crate::{
application::input_parser::comment,
types::{Category, Requirement, SubscribeFeedInput},
};

pub(super) fn parse(s: &str) -> Result<SubscribeFeedInput, NomError> {
delimited(comment::comments, feed_input, comment::comments)
.parse(s)
.finish()
.map(|(_, input)| input)
}

fn feed_input(s: &str) -> IResult<&str, SubscribeFeedInput> {
let (remain, (_, requirement, _, category, _, feed_url, _)) = (
multispace0,
requirement,
multispace1,
category,
multispace1,
url,
multispace0,
)
.parse(s)?;
Ok((
remain,
SubscribeFeedInput {
feed_url,
requirement: Some(requirement),
category: Some(category),
},
))
}

pub fn requirement(s: &str) -> IResult<&str, Requirement> {
alt((
value(Requirement::Must, tag_no_case("MUST")),
value(Requirement::Should, tag_no_case("SHOULD")),
value(Requirement::May, tag_no_case("MAY")),
))
.parse(s)
}

fn category(s: &str) -> IResult<&str, Category> {
let (remain, category) = take_while_m_n(1, 20, |c| c != ' ').parse(s)?;
Ok((remain, Category::new(category).expect("this is a bug")))
}

fn url(s: &str) -> IResult<&str, String> {
map(take_while(|c| c != ' '), |s: &str| s.to_owned()).parse(s)
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn parse_requirement() {
assert_eq!(requirement("must"), Ok(("", Requirement::Must)));
assert_eq!(requirement("Must"), Ok(("", Requirement::Must)));
assert_eq!(requirement("MUST"), Ok(("", Requirement::Must)));
assert_eq!(requirement("should"), Ok(("", Requirement::Should)));
assert_eq!(requirement("Should"), Ok(("", Requirement::Should)));
assert_eq!(requirement("SHOULD"), Ok(("", Requirement::Should)));
assert_eq!(requirement("may"), Ok(("", Requirement::May)));
assert_eq!(requirement("May"), Ok(("", Requirement::May)));
assert_eq!(requirement("MAY"), Ok(("", Requirement::May)));
}

#[test]
fn parse_category() {
assert_eq!(category("rust"), Ok(("", Category::new("rust").unwrap())));
assert_eq!(category("Rust"), Ok(("", Category::new("rust").unwrap())));
}

#[test]
fn parse_feed_input() {
assert_eq!(
feed_input("MUST rust https://example.ymgyt.io/atom.xml"),
Ok((
"",
SubscribeFeedInput {
feed_url: "https://example.ymgyt.io/atom.xml".into(),
requirement: Some(Requirement::Must),
category: Some(Category::new("rust").unwrap())
}
))
);
}
}
}

mod comment {
use nom::{
bytes::complete::{tag, take_until},
character::complete::line_ending,
combinator::value,
multi::fold_many0,
sequence::delimited,
IResult, Parser,
};

pub(super) fn comments(s: &str) -> IResult<&str, ()> {
fold_many0(comment, || (), |acc, ()| acc).parse(s)
}

pub(super) fn comment(s: &str) -> IResult<&str, ()> {
value((), delimited(tag("#"), take_until("\n"), line_ending)).parse(s)
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn parse_comment() {
assert_eq!(comment("# foo\n"), Ok(("", ())),);
assert_eq!(comment("# foo\r\n"), Ok(("", ())),);
}

#[test]
fn parse_comments() {
let s = "# comment1\n# comment2\n";
assert_eq!(comments(s), Ok(("", ())));
}
}
}

#[cfg(test)]
mod test {
use super::*;

// use super::*;
// TODO: update test
/*
#[test]
fn parse_feed_url() {
let prompt = InputParser::SUSBSCRIBE_FEED_PROMPT;
Expand All @@ -67,7 +204,8 @@ mod test {
for case in cases {
let p = InputParser::new(case.0.as_str());
assert_eq!(p.parse_feed_url(), case.1);
assert_eq!(p.parse_feed_subscription(), case.1);
}
}
*/
}

0 comments on commit 17b6288

Please sign in to comment.