#![doc = include_str!("../README.md")]
mod params;
#[cfg(test)]
mod tests;
use std::marker::PhantomData;
pub use params::Param;
pub use params::{Language, Since, SpokenLanguage};
use soupy::{Node, Queryable};
use thiserror::Error;
#[derive(Clone, Debug)]
pub struct Repository {
pub name: String,
pub owner: String,
pub description: String,
}
impl Repository {
pub fn url(&self) -> String {
format!("https://github.com/{}/{}", self.owner, self.name)
}
}
#[derive(Debug, Error)]
pub enum TrendingError<E>
where
E: std::error::Error,
{
#[error("Request Error: {0}")]
RequestError(#[from] E),
}
pub trait TrendExt<'c> {
type BackendError: std::error::Error;
fn github_trending(&'c self) -> TrendingBuilder<'c, Self> {
TrendingBuilder {
client: self,
parameters: TrendingParameters::default(),
}
}
#[doc(hidden)]
fn request_trending(
&self,
request: TrendingRequest,
) -> impl std::future::Future<Output = Result<String, TrendingError<Self::BackendError>>> + Send;
}
#[cfg(feature = "reqwest")]
impl<'c> TrendExt<'c> for reqwest::Client {
type BackendError = reqwest::Error;
async fn request_trending(
&self,
TrendingRequest { url, parameters }: TrendingRequest,
) -> Result<String, TrendingError<Self::BackendError>> {
Ok(self
.get(url)
.query(¶meters)
.send()
.await?
.text()
.await?)
}
}
#[derive(Debug, Clone, Default)]
pub struct TrendingRequest {
pub url: String,
pub parameters: Vec<(&'static str, &'static str)>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TrendingParameters {
lang: Option<Language>,
spoken: Option<SpokenLanguage>,
since: Option<Since>,
}
impl TrendingParameters {
pub fn with_language(&mut self, lang: Language) -> &mut Self {
self.lang.replace(lang);
self
}
pub fn since(&mut self, since: Since) -> &mut Self {
self.since.replace(since);
self
}
pub fn with_spoken_language(&mut self, spoken_language: SpokenLanguage) -> &mut Self {
self.spoken.replace(spoken_language);
self
}
}
pub struct TrendingBuilder<'c, C>
where
C: TrendExt<'c> + ?Sized,
{
client: &'c C,
parameters: TrendingParameters,
}
impl<'c, C> TrendingBuilder<'c, C>
where
C: TrendExt<'c> + ?Sized,
{
pub fn with_language(&mut self, lang: Language) -> &mut Self {
self.parameters.with_language(lang);
self
}
pub fn since(&mut self, since: Since) -> &mut Self {
self.parameters.since(since);
self
}
pub fn with_spoken_language(&mut self, spoken_language: SpokenLanguage) -> &mut Self {
self.parameters.with_spoken_language(spoken_language);
self
}
pub async fn repositories(
&self,
) -> Result<Trending<Repository>, TrendingError<C::BackendError>> {
let request = Repository::request(self.parameters);
let resp = self.client.request_trending(request).await?;
Ok(Trending {
raw: resp,
_t: PhantomData,
})
}
}
pub trait Trend: Sized {
fn extract(text: &str) -> Vec<Self>;
fn request(parameter: TrendingParameters) -> TrendingRequest;
}
impl Trend for Repository {
fn extract(text: &str) -> Vec<Self> {
let soup = soupy::Soup::html(text);
soup.tag("article")
.into_iter()
.filter_map(|article| {
let url = article.query().tag("h2").first().and_then(|item| {
item.query()
.tag("a")
.first()
.and_then(|item| item.get("href").cloned())
.and_then(|url| {
url.strip_prefix("/").and_then(|url| {
url.split_once("/")
.map(|(owner, name)| (owner.to_string(), name.to_string()))
})
})
});
let desc = article
.query()
.tag("p")
.first()
.map(|item| item.all_text().trim().to_string());
url.zip(desc)
.map(|((owner, name), description)| Repository {
name,
owner,
description,
})
})
.collect()
}
fn request(param: TrendingParameters) -> TrendingRequest {
let url = match param.lang {
Some(lang) => format!("https://github.com/trending/{}", lang.value()),
None => "https://github.com/trending".to_string(),
};
let parameters: Vec<_> = param
.since
.iter()
.map(Since::query)
.chain(param.spoken.iter().map(SpokenLanguage::query))
.collect();
TrendingRequest { url, parameters }
}
}
pub struct Trending<T: Trend> {
raw: String,
_t: PhantomData<T>,
}
impl<T: Trend> Trending<T> {
pub fn all(&self) -> Vec<T> {
T::extract(&self.raw)
}
pub fn raw(&self) -> &str {
&self.raw
}
}