github_trending_rs/
lib.rs1#![doc = include_str!("../README.md")]
2
3mod params;
4#[cfg(test)]
5mod tests;
6
7use std::marker::PhantomData;
8
9use params::Param;
10pub use params::{Language, Since, SpokenLanguage};
11
12use soupy::{Node, Queryable};
13use thiserror::Error;
14
15#[derive(Clone, Debug)]
19pub struct Repository {
20 pub name: String,
21 pub owner: String,
22 pub description: String,
23 }
30
31impl Repository {
32 pub fn url(&self) -> String {
34 format!("https://github.com/{}/{}", self.owner, self.name)
35 }
36}
37
38#[derive(Debug, Error)]
39pub enum TrendingError<E>
40where
41 E: std::error::Error,
42{
43 #[error("Request Error: {0}")]
44 RequestError(#[from] E),
45}
46
47pub trait TrendExt<'c> {
54 type BackendError: std::error::Error;
55 fn github_trending(&'c self) -> TrendingBuilder<'c, Self>;
57 #[doc(hidden)]
58 fn request_trending(
59 &self,
60 request: TrendingRequest,
61 ) -> impl std::future::Future<Output = Result<String, TrendingError<Self::BackendError>>> + Send;
62}
63
64impl<'c> TrendExt<'c> for reqwest::Client {
65 type BackendError = reqwest::Error;
66
67 fn github_trending(&'c self) -> TrendingBuilder<'c, Self> {
68 TrendingBuilder {
69 client: self,
70 parameters: TrendingParameters::default(),
71 }
72 }
73
74 async fn request_trending(
75 &self,
76 TrendingRequest { url, parameters }: TrendingRequest,
77 ) -> Result<String, TrendingError<reqwest::Error>> {
78 Ok(self
79 .get(url)
80 .query(¶meters)
81 .send()
82 .await?
83 .text()
84 .await?)
85 }
86}
87
88#[derive(Debug, Clone, Default)]
89pub struct TrendingRequest {
90 url: String,
91 parameters: Vec<(&'static str, &'static str)>,
92}
93
94#[derive(Debug, Clone, Copy, Default)]
95pub struct TrendingParameters {
96 lang: Option<Language>,
97 spoken: Option<SpokenLanguage>,
98 since: Option<Since>,
99}
100
101impl TrendingParameters {
102 pub fn with_language(&mut self, lang: Language) -> &mut Self {
103 self.lang.replace(lang);
104 self
105 }
106
107 pub fn since(&mut self, since: Since) -> &mut Self {
108 self.since.replace(since);
109 self
110 }
111
112 pub fn with_spoken_language(&mut self, spoken_language: SpokenLanguage) -> &mut Self {
113 self.spoken.replace(spoken_language);
114 self
115 }
116}
117
118pub struct TrendingBuilder<'c, C>
120where
121 C: TrendExt<'c> + ?Sized,
122{
123 client: &'c C,
124 parameters: TrendingParameters,
125}
126
127impl<'c, C> TrendingBuilder<'c, C>
128where
129 C: TrendExt<'c> + ?Sized,
130{
131 pub fn with_language(&mut self, lang: Language) -> &mut Self {
133 self.parameters.with_language(lang);
134 self
135 }
136
137 pub fn since(&mut self, since: Since) -> &mut Self {
139 self.parameters.since(since);
140 self
141 }
142
143 pub fn with_spoken_language(&mut self, spoken_language: SpokenLanguage) -> &mut Self {
145 self.parameters.with_spoken_language(spoken_language);
146 self
147 }
148
149 pub async fn repositories(
151 &self,
152 ) -> Result<Trending<Repository>, TrendingError<C::BackendError>> {
153 let request = Repository::request(self.parameters);
154 let resp = self.client.request_trending(request).await?;
155 Ok(Trending {
156 raw: resp,
157 _t: PhantomData,
158 })
159 }
160}
161
162pub trait Trend: Sized {
166 fn extract(text: &str) -> Vec<Self>;
168 fn request(parameter: TrendingParameters) -> TrendingRequest;
170}
171
172impl Trend for Repository {
173 fn extract(text: &str) -> Vec<Self> {
174 let soup = soupy::Soup::html(text);
175 soup.tag("article")
176 .into_iter()
177 .filter_map(|article| {
178 let url = article.query().tag("h2").first().and_then(|item| {
180 item.query()
181 .tag("a")
182 .first()
183 .and_then(|item| item.get("href").cloned())
184 .and_then(|url| {
185 url.strip_prefix("/").and_then(|url| {
186 url.split_once("/")
187 .map(|(owner, name)| (owner.to_string(), name.to_string()))
188 })
189 })
190 });
191 let desc = article
192 .query()
193 .tag("p")
194 .first()
195 .map(|item| item.all_text().trim().to_string());
196 url.zip(desc)
197 .map(|((owner, name), description)| Repository {
198 name,
199 owner,
200 description,
201 })
202 })
203 .collect()
204 }
205
206 fn request(param: TrendingParameters) -> TrendingRequest {
207 let url = match param.lang {
208 Some(lang) => format!("https://github.com/trending/{}", lang.value()),
209 None => "https://github.com/trending".to_string(),
210 };
211 let parameters: Vec<_> = param
212 .since
213 .iter()
214 .map(Since::query)
215 .chain(param.spoken.iter().map(SpokenLanguage::query))
216 .collect();
217
218 TrendingRequest { url, parameters }
219 }
220}
221
222pub struct Trending<T: Trend> {
223 raw: String,
224 _t: PhantomData<T>,
225}
226
227impl<T: Trend> Trending<T> {
228 pub fn all(&self) -> Vec<T> {
230 T::extract(&self.raw)
231 }
232
233 pub fn raw(&self) -> &str {
235 &self.raw
236 }
237}