Skip to content

Commit a6aca41

Browse files
committed
feat: impl subscribe feed usecase
1 parent dbf4f03 commit a6aca41

36 files changed

+723
-174
lines changed

Cargo.lock

+75-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ resolver = "2"
33
members = ["synd", "syndapi", "syndterm"]
44

55
[workspace.dependencies]
6+
async-trait = { version = "0.1.77" }
67
anyhow = { version = "1" }
78
clap = { version = "4.4" }
9+
feed-rs = { version = "1.4" }
10+
futures = { version = "0.3" }
811
graphql_client = { version = "0.13.0", default-features = false }
912
reqwest = { version = "0.11.23", default-features = false, features = [
1013
"rustls-tls",

justfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ fmt-toml:
1313
taplo fmt **.toml
1414

1515
update-gql-schema:
16-
graphql-client introspect-schema http://localhost:5959/gql \
16+
graphql-client introspect-schema http://localhost:5959/graphql \
1717
--header 'authorization: me' out> syndterm/gql/schema.json
1818

1919

synd/Cargo.toml

+6
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,9 @@ edition = "2021"
66
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
77

88
[dependencies]
9+
anyhow = { workspace = true }
10+
async-trait = { workspace = true }
11+
feed-rs = { workspace = true }
12+
futures = { workspace = true }
13+
reqwest = { workspace = true, features = ["stream"] }
14+
thiserror = "1.0.56"

synd/src/feed/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod parser;

synd/src/feed/parser.rs

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
use std::time::Duration;
2+
3+
use async_trait::async_trait;
4+
use feed_rs::parser::Parser;
5+
6+
use crate::types::Feed;
7+
8+
pub type ParseResult<T> = std::result::Result<T, ParserError>;
9+
10+
#[derive(Debug, thiserror::Error)]
11+
pub enum ParserError {
12+
#[error("fetch failed")]
13+
Fetch(#[from] reqwest::Error),
14+
#[error("response size limit exceeded")]
15+
ResponseLimitExceed,
16+
17+
#[error(transparent)]
18+
Other(#[from] anyhow::Error),
19+
}
20+
21+
#[async_trait]
22+
pub trait FetchFeed: Send + Sync {
23+
async fn fetch(&self, url: String) -> ParseResult<Feed>;
24+
}
25+
26+
/// Feed Process entry point
27+
pub struct FeedService {
28+
http: reqwest::Client,
29+
buff_limit: usize,
30+
}
31+
32+
#[async_trait]
33+
impl FetchFeed for FeedService {
34+
async fn fetch(&self, url: String) -> ParseResult<Feed> {
35+
use futures::StreamExt;
36+
let mut stream = self
37+
.http
38+
.get(&url)
39+
.send()
40+
.await
41+
.map_err(ParserError::Fetch)?
42+
.error_for_status()
43+
.map_err(ParserError::Fetch)?
44+
.bytes_stream();
45+
46+
let mut buff = Vec::new();
47+
48+
while let Some(chunk) = stream.next().await {
49+
let chunk = chunk.map_err(ParserError::Fetch)?;
50+
if buff.len() + chunk.len() > self.buff_limit {
51+
return Err(ParserError::ResponseLimitExceed);
52+
}
53+
buff.extend(chunk);
54+
}
55+
56+
self.parse(url, buff.as_slice())
57+
}
58+
}
59+
60+
impl FeedService {
61+
pub fn new(user_agent: &str, buff_limit: usize) -> Self {
62+
let http = reqwest::ClientBuilder::new()
63+
.user_agent(user_agent)
64+
.timeout(Duration::from_secs(10))
65+
.connect_timeout(Duration::from_secs(10))
66+
.build()
67+
.unwrap();
68+
69+
Self { http, buff_limit }
70+
}
71+
72+
pub fn parse<S>(&self, url: impl Into<String>, source: S) -> ParseResult<Feed>
73+
where
74+
S: std::io::Read,
75+
{
76+
let url = url.into();
77+
let parser = self.build_parser(&url);
78+
79+
match parser.parse(source) {
80+
Ok(feed) => Ok(Feed::from((url, feed))),
81+
// TODO: handle error
82+
Err(err) => Err(ParserError::Other(err.into())),
83+
}
84+
}
85+
86+
fn build_parser(&self, base_uri: impl AsRef<str>) -> Parser {
87+
feed_rs::parser::Builder::new()
88+
.base_uri(Some(base_uri))
89+
.build()
90+
}
91+
}

synd/src/lib.rs

+2-10
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,2 @@
1-
#[derive(Debug, Clone)]
2-
pub struct Feed {
3-
pub url: String,
4-
}
5-
6-
impl Feed {
7-
pub fn new(url: String) -> Self {
8-
Self { url }
9-
}
10-
}
1+
pub mod feed;
2+
pub mod types;

synd/src/types.rs

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#[derive(Debug, Clone)]
2+
pub struct Feed {
3+
url: String,
4+
#[allow(dead_code)]
5+
feed: feed_rs::model::Feed,
6+
}
7+
8+
impl Feed {
9+
pub fn meta(&self) -> FeedMeta {
10+
FeedMeta::new(self.title().into(), self.url.clone())
11+
}
12+
13+
pub fn title(&self) -> &str {
14+
self.feed
15+
.title
16+
.as_ref()
17+
.map(|text| text.content.as_str())
18+
.unwrap_or("???")
19+
}
20+
}
21+
22+
impl From<(String, feed_rs::model::Feed)> for Feed {
23+
fn from(feed: (String, feed_rs::model::Feed)) -> Self {
24+
Feed {
25+
url: feed.0,
26+
feed: feed.1,
27+
}
28+
}
29+
}
30+
31+
#[derive(Debug, Clone)]
32+
pub struct FeedMeta {
33+
pub title: String,
34+
pub url: String,
35+
}
36+
37+
impl FeedMeta {
38+
pub fn new(title: String, url: String) -> Self {
39+
Self { title, url }
40+
}
41+
}

syndapi/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ async-graphql = { git = "https://github.com/async-graphql/async-graphql.git", br
1010
"tracing",
1111
] }
1212
async-graphql-axum = { git = "https://github.com/async-graphql/async-graphql.git", branch = "master" }
13+
async-trait = { workspace = true }
1314
# TODO configure features
1415
axum = { version = "0.7.3" }
1516
clap = { workspace = true, features = ["derive"] }
17+
either = "1.9.0"
1618
graphql_client = { workspace = true }
1719
kvsd = { version = "0.1.2" }
1820
reqwest = { workspace = true }

syndapi/src/config.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
pub const USER_AGENT: &'static str =
22
concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
3+
4+
pub const PORT: u16 = 5959;

0 commit comments

Comments
 (0)