From 37a11c17b53b222040ba9ea3ccad3db5c3b4a06c Mon Sep 17 00:00:00 2001 From: Sam Rijs Date: Sat, 17 Feb 2018 14:41:09 +1100 Subject: [PATCH] add popular crates to index --- src/engine/mod.rs | 15 +++++++- src/interactors/crates.rs | 59 ++++++++++++++++++++++++++++- src/server/mod.rs | 26 +++++++------ src/server/views/html/index.rs | 69 ++++++++++++++++++++++++---------- 4 files changed, 135 insertions(+), 34 deletions(-) diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 5166222..7490204 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -21,7 +21,7 @@ use ::utils::cache::Cache; use ::models::repo::{Repository, RepoPath}; use ::models::crates::{CrateName, CratePath, CrateRelease, AnalyzedDependencies}; -use ::interactors::crates::QueryCrate; +use ::interactors::crates::{QueryCrate, GetPopularCrates}; use ::interactors::RetrieveFileAtPath; use ::interactors::github::{GetPopularRepos}; @@ -36,6 +36,7 @@ pub struct Engine { logger: Logger, query_crate: Arc>>, + get_popular_crates: Arc>>, get_popular_repos: Arc>>, retrieve_file_at_path: Arc> } @@ -43,12 +44,14 @@ pub struct Engine { impl Engine { pub fn new(client: Client>, logger: Logger) -> Engine { let query_crate = Cache::new(QueryCrate(client.clone()), Duration::from_secs(300), 500); + let get_popular_crates = Cache::new(GetPopularCrates(client.clone()), Duration::from_secs(10), 1); let get_popular_repos = Cache::new(GetPopularRepos(client.clone()), Duration::from_secs(10), 1); Engine { client: client.clone(), logger, query_crate: Arc::new(query_crate), + get_popular_crates: Arc::new(get_popular_crates), get_popular_repos: Arc::new(get_popular_repos), retrieve_file_at_path: Arc::new(RetrieveFileAtPath(client)) } @@ -84,6 +87,13 @@ impl Engine { }) } + pub fn get_popular_crates(&self) -> + impl Future, Error=Error> + { + self.get_popular_crates.call(()) + .from_err().map(|crates| crates.clone()) + } + pub fn analyze_repo_dependencies(&self, repo_path: RepoPath) -> impl Future { @@ -180,7 +190,8 @@ lazy_static! { RepoPath::from_parts("github", "google", "xi-editor"), RepoPath::from_parts("github", "lk-geimfari", "awesomo"), RepoPath::from_parts("github", "redox-os", "tfs"), - RepoPath::from_parts("github", "carols10cents", "rustlings") + RepoPath::from_parts("github", "carols10cents", "rustlings"), + RepoPath::from_parts("github", "rust-unofficial", "awesome-rust") ].into_iter().collect::, _>>().unwrap() }; } diff --git a/src/interactors/crates.rs b/src/interactors/crates.rs index 098cd36..060b01d 100644 --- a/src/interactors/crates.rs +++ b/src/interactors/crates.rs @@ -7,9 +7,10 @@ use tokio_service::Service; use semver::{Version, VersionReq}; use serde_json; -use ::models::crates::{CrateName, CrateRelease, CrateDeps, CrateDep}; +use ::models::crates::{CrateName, CrateRelease, CrateDeps, CrateDep, CratePath}; const CRATES_INDEX_BASE_URI: &str = "https://raw.githubusercontent.com/rust-lang/crates.io-index"; +const CRATES_API_BASE_URI: &str = "https://crates.io/api/v1"; #[derive(Deserialize, Debug)] struct RegistryPackageDep { @@ -109,3 +110,59 @@ impl Service for QueryCrate })) } } + +#[derive(Deserialize)] +struct SummaryResponseDetail { + name: String, + max_version: Version +} + +#[derive(Deserialize)] +struct SummaryResponse { + most_downloaded: Vec +} + +fn convert_summary(response: SummaryResponse) -> Result, Error> { + response.most_downloaded.into_iter().map(|detail| { + let name = detail.name.parse()?; + Ok(CratePath { name, version: detail.max_version }) + }).collect() +} + +#[derive(Debug, Clone)] +pub struct GetPopularCrates(pub S); + +impl Service for GetPopularCrates + where S: Service + Clone + 'static, + S::Future: 'static +{ + type Request = (); + type Response = Vec; + type Error = Error; + type Future = Box>; + + fn call(&self, _req: ()) -> Self::Future { + let service = self.0.clone(); + + let uri_future = format!("{}/summary", CRATES_API_BASE_URI) + .parse::().into_future().from_err(); + + Box::new(uri_future.and_then(move |uri| { + let request = Request::new(Method::Get, uri.clone()); + + service.call(request).from_err().and_then(move |response| { + let status = response.status(); + if !status.is_success() { + future::Either::A(future::err(format_err!("Status code {} for URI {}", status, uri))) + } else { + let body_future = response.body().concat2().from_err(); + let decode_future = body_future.and_then(|body| { + let summary = serde_json::from_slice::(&body)?; + convert_summary(summary) + }); + future::Either::B(decode_future) + } + }) + })) + } +} diff --git a/src/server/mod.rs b/src/server/mod.rs index f0900b2..a4ddd82 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -113,18 +113,20 @@ impl Server { fn index(&self, _req: Request, _params: Params, logger: Logger) -> impl Future { - self.engine.get_popular_repos().then(move |popular_result| { - match popular_result { - Err(err) => { - error!(logger, "error: {}", err); - let mut response = views::html::error::render("Could not retrieve popular repositories", ""); - response.set_status(StatusCode::InternalServerError); - future::ok(response) - }, - Ok(popular) => - future::ok(views::html::index::render(popular)) - } - }) + self.engine.get_popular_repos() + .join(self.engine.get_popular_crates()) + .then(move |popular_result| { + match popular_result { + Err(err) => { + error!(logger, "error: {}", err); + let mut response = views::html::error::render("Could not retrieve popular items", ""); + response.set_status(StatusCode::InternalServerError); + future::ok(response) + }, + Ok((popular_repos, popular_crates)) => + future::ok(views::html::index::render(popular_repos, popular_crates)) + } + }) } fn repo_status(&self, _req: Request, params: Params, logger: Logger, format: StatusFormat) -> diff --git a/src/server/views/html/index.rs b/src/server/views/html/index.rs index 26dc424..41e1605 100644 --- a/src/server/views/html/index.rs +++ b/src/server/views/html/index.rs @@ -2,28 +2,59 @@ use hyper::Response; use maud::{Markup, html}; use ::models::repo::Repository; +use ::models::crates::CratePath; -fn popular_table(popular: Vec) -> Markup { +fn popular_table(popular_repos: Vec, popular_crates: Vec) -> Markup { html! { - h2 class="title is-3" "Popular Repositories" + div class="columns" { + div class="column" { + h2 class="title is-3" "Popular Repositories" - table class="table is-fullwidth is-striped is-hoverable" { - thead { - tr { - th "Repository" - th class="has-text-right" "Status" - } - } - tbody { - @for repo in popular { - tr { - td { - a href=(format!("{}/repo/{}/{}/{}", &super::SELF_BASE_URL as &str, repo.path.site.as_ref(), repo.path.qual.as_ref(), repo.path.name.as_ref())) { - (format!("{} / {}", repo.path.qual.as_ref(), repo.path.name.as_ref())) + table class="table is-fullwidth is-striped is-hoverable" { + thead { + tr { + th "Repository" + th class="has-text-right" "Status" + } + } + tbody { + @for repo in popular_repos.into_iter().take(10) { + tr { + td { + a href=(format!("{}/repo/{}/{}/{}", &super::SELF_BASE_URL as &str, repo.path.site.as_ref(), repo.path.qual.as_ref(), repo.path.name.as_ref())) { + (format!("{} / {}", repo.path.qual.as_ref(), repo.path.name.as_ref())) + } + } + td class="has-text-right" { + img src=(format!("{}/repo/{}/{}/{}/status.svg", &super::SELF_BASE_URL as &str, repo.path.site.as_ref(), repo.path.qual.as_ref(), repo.path.name.as_ref())); + } } } - td class="has-text-right" { - img src=(format!("{}/repo/{}/{}/{}/status.svg", &super::SELF_BASE_URL as &str, repo.path.site.as_ref(), repo.path.qual.as_ref(), repo.path.name.as_ref())); + } + } + } + div class="column" { + h2 class="title is-3" "Popular Crates" + + table class="table is-fullwidth is-striped is-hoverable" { + thead { + tr { + th "Crate" + th class="has-text-right" "Status" + } + } + tbody { + @for crate_path in popular_crates { + tr { + td { + a href=(format!("{}/crate/{}/{}", &super::SELF_BASE_URL as &str, crate_path.name.as_ref(), crate_path.version)) { + (format!("{}", crate_path.name.as_ref())) + } + } + td class="has-text-right" { + img src=(format!("{}/crate/{}/{}/status.svg", &super::SELF_BASE_URL as &str, crate_path.name.as_ref(), crate_path.version)); + } + } } } } @@ -32,7 +63,7 @@ fn popular_table(popular: Vec) -> Markup { } } -pub fn render(popular: Vec) -> Response { +pub fn render(popular_repos: Vec, popular_crates: Vec) -> Response { super::render_html("Keep your dependencies up-to-date", html! { section class="hero is-light" { div class="hero-head" (super::render_navbar()) @@ -48,7 +79,7 @@ pub fn render(popular: Vec) -> Response { } } section class="section" { - div class="container" (popular_table(popular)) + div class="container" (popular_table(popular_repos, popular_crates)) } (super::render_footer(None)) })