1use regex::Regex;
2
3use crate::{
4 api_traits::{ApiOperation, TrendingProjectURL},
5 cmds::trending::TrendingProject,
6 http::Headers,
7 io::{HttpResponse, HttpRunner},
8 remote::query,
9 Result,
10};
11
12use super::Github;
13
14impl<R: HttpRunner<Response = HttpResponse>> TrendingProjectURL for Github<R> {
15 fn list(&self, language: String) -> Result<Vec<TrendingProject>> {
16 let url = format!("https://{}/trending/{}", self.domain, language);
17 let mut headers = Headers::new();
18 headers.set("Accept".to_string(), "text/html".to_string());
19 let response = query::get_raw::<_, String>(
20 &self.runner,
21 &url,
22 None,
23 headers,
24 ApiOperation::SinglePage,
25 )?;
26 parse_response(response)
27 }
28}
29
30fn parse_response(response: HttpResponse) -> Result<Vec<TrendingProject>> {
31 let body = response.body;
32 let proj_re = Regex::new(r#"href="/[a-zA-Z0-9_-]*/[a-zA-Z0-9_-]*/stargazers""#).unwrap();
33 let description_re = Regex::new(r#"<p class="col-9 color-fg-muted my-1 pr-4">"#).unwrap();
34 let mut descr_header_matched = false;
35 let mut trending = Vec::new();
36 let mut description = String::new();
37 for line in body.lines() {
38 if descr_header_matched {
39 description = line.trim().to_string();
40 descr_header_matched = false;
41 continue;
42 }
43 if description_re.find(line).is_some() {
44 descr_header_matched = true;
45 continue;
46 }
47 if let Some(proj) = proj_re.find(line) {
48 let proj = proj.as_str().split('"').collect::<Vec<&str>>();
49 let proj_paths = proj[1].split('/').collect::<Vec<&str>>();
50 if proj_paths[1] == "features" || proj_paths[1] == "about" || proj_paths[1] == "site" {
51 continue;
52 }
53 let url = format!("https://github.com/{}/{}", proj_paths[1], proj_paths[2]);
54 trending.push(TrendingProject::new(url, description.to_string()));
55 }
56 }
57 Ok(trending)
58}
59
60#[cfg(test)]
61mod test {
62
63 use super::*;
64
65 use crate::{
66 setup_client,
67 test::utils::{default_github, ContractType, ResponseContracts},
68 };
69
70 #[test]
71 fn test_list_trending_projects() {
72 let contracts =
73 ResponseContracts::new(ContractType::Github).add_contract(200, "trending.html", None);
74 let (client, github) = setup_client!(contracts, default_github(), dyn TrendingProjectURL);
75
76 let trending = github.list("rust".to_string()).unwrap();
77 assert_eq!(2, trending.len());
78 assert_eq!("https://github.com/trending/rust", *client.url(),);
79 assert_eq!(
80 Some(ApiOperation::SinglePage),
81 *client.api_operation.borrow()
82 );
83 let proj = &trending[0];
84 assert_eq!("https://github.com/lencx/ChatGPT", proj.url);
85 assert_eq!(
86 "🔮 ChatGPT Desktop Application (Mac, Windows and Linux)",
87 proj.description
88 );
89 let proj = &trending[1];
90 assert_eq!("https://github.com/sxyazi/yazi", proj.url);
91 assert_eq!(
92 "💥 Blazing fast terminal file manager written in Rust, based on async I/O.",
93 proj.description
94 );
95 }
96}