From 7fff95203ee4920f566ec9adf6c03a891c7da9b2 Mon Sep 17 00:00:00 2001 From: Sam Rijs Date: Sat, 17 Feb 2018 00:25:34 +1100 Subject: [PATCH] first simple version of crate dependency reports --- src/engine/mod.rs | 33 ++++++++++++++-- src/interactors/crates.rs | 33 +++++++++++++--- src/models/crates.rs | 16 ++++++++ src/models/mod.rs | 5 +++ src/server/mod.rs | 69 +++++++++++++++++++++++++++------ src/server/views/html/status.rs | 67 ++++++++++++++++++++++---------- 6 files changed, 182 insertions(+), 41 deletions(-) diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 0e78039..c3010c6 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use failure::Error; -use futures::Future; +use futures::{Future, future}; use futures::future::join_all; use hyper::Client; use hyper::client::HttpConnector; @@ -18,7 +18,7 @@ mod futures; use ::utils::cache::Cache; use ::models::repo::{Repository, RepoPath}; -use ::models::crates::{CrateName, CrateRelease, AnalyzedDependencies}; +use ::models::crates::{CrateName, CratePath, CrateRelease, AnalyzedDependencies}; use ::interactors::crates::QueryCrate; use ::interactors::RetrieveFileAtPath; @@ -83,7 +83,7 @@ impl Engine { }) } - pub fn analyze_dependencies(&self, repo_path: RepoPath) -> + pub fn analyze_repo_dependencies(&self, repo_path: RepoPath) -> impl Future { let start = Instant::now(); @@ -109,6 +109,33 @@ impl Engine { }) } + pub fn analyze_crate_dependencies(&self, crate_path: CratePath) -> + impl Future + { + let start = Instant::now(); + + let query_future = self.query_crate.call(crate_path.name.clone()).from_err(); + + let engine = self.clone(); + query_future.and_then(move |query_response| { + match query_response.releases.iter().find(|release| release.version == crate_path.version) { + None => future::Either::A(future::err(format_err!("could not find crate release with version {}", crate_path.version))), + Some(release) => { + let analyzed_deps_future = AnalyzeDependenciesFuture::new(&engine, release.deps.clone()); + + future::Either::B(analyzed_deps_future.map(move |analyzed_deps| { + let crates = vec![(crate_path.name, analyzed_deps)].into_iter().collect(); + let duration = start.elapsed(); + + AnalyzeDependenciesOutcome { + crates, duration + } + })) + } + } + }) + } + fn fetch_releases>(&self, names: I) -> impl Iterator, Error=Error>> { diff --git a/src/interactors/crates.rs b/src/interactors/crates.rs index 774ac73..098cd36 100644 --- a/src/interactors/crates.rs +++ b/src/interactors/crates.rs @@ -4,28 +4,49 @@ use failure::Error; use futures::{Future, Stream, IntoFuture, future}; use hyper::{Error as HyperError, Method, Request, Response, Uri}; use tokio_service::Service; -use semver::Version; +use semver::{Version, VersionReq}; use serde_json; -use ::models::crates::{CrateName, CrateRelease}; +use ::models::crates::{CrateName, CrateRelease, CrateDeps, CrateDep}; const CRATES_INDEX_BASE_URI: &str = "https://raw.githubusercontent.com/rust-lang/crates.io-index"; +#[derive(Deserialize, Debug)] +struct RegistryPackageDep { + name: String, + req: VersionReq, + #[serde(default)] + kind: Option +} + #[derive(Deserialize, Debug)] struct RegistryPackage { vers: Version, #[serde(default)] + deps: Vec, + #[serde(default)] yanked: bool } fn convert_pkgs(name: &CrateName, packages: Vec) -> Result { let releases = packages.into_iter().map(|package| { - CrateRelease { + let mut deps = CrateDeps::default(); + for dep in package.deps { + match dep.kind.map(|k| k.clone()).unwrap_or_else(|| "normal".into()).as_ref() { + "normal" => + deps.main.insert(dep.name.parse()?, CrateDep::External(dep.req)), + "dev" => + deps.dev.insert(dep.name.parse()?, CrateDep::External(dep.req)), + _ => None + }; + } + Ok(CrateRelease { name: name.clone(), version: package.vers, + deps: deps, yanked: package.yanked - } - }).collect(); + }) + }).collect::>()?; Ok(QueryCrateResponse { releases: releases @@ -72,7 +93,7 @@ impl Service for QueryCrate 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 decode_future = body_future.and_then(move |body| { let string_body = str::from_utf8(body.as_ref())?; let packages = string_body.lines() .map(|s| s.trim()) diff --git a/src/models/crates.rs b/src/models/crates.rs index d6320af..c72c7fd 100644 --- a/src/models/crates.rs +++ b/src/models/crates.rs @@ -6,6 +6,21 @@ use ordermap::OrderMap; use relative_path::RelativePathBuf; use semver::{Version, VersionReq}; +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct CratePath { + pub name: CrateName, + pub version: Version +} + +impl CratePath { + pub fn from_parts(name: &str, version: &str) -> Result { + Ok(CratePath { + name: name.parse()?, + version: version.parse()? + }) + } +} + #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct CrateName(String); @@ -47,6 +62,7 @@ impl FromStr for CrateName { pub struct CrateRelease { pub name: CrateName, pub version: Version, + pub deps: CrateDeps, pub yanked: bool } diff --git a/src/models/mod.rs b/src/models/mod.rs index 290c408..5f3bd04 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,2 +1,7 @@ pub mod crates; pub mod repo; + +pub enum SubjectPath { + Repo(self::repo::RepoPath), + Crate(self::crates::CratePath) +} diff --git a/src/server/mod.rs b/src/server/mod.rs index 7e93f36..f7b4752 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -11,7 +11,9 @@ mod assets; mod views; use ::engine::{Engine, AnalyzeDependenciesOutcome}; +use ::models::crates::CratePath; use ::models::repo::RepoPath; +use ::models::SubjectPath; #[derive(Clone, Copy, PartialEq)] enum StatusFormat { @@ -28,7 +30,8 @@ enum StaticFile { enum Route { Index, Static(StaticFile), - Status(StatusFormat) + RepoStatus(StatusFormat), + CrateStatus(StatusFormat) } #[derive(Clone)] @@ -47,8 +50,11 @@ impl Server { router.add("/static/style.css", Route::Static(StaticFile::StyleCss)); router.add("/static/favicon.png", Route::Static(StaticFile::FaviconPng)); - router.add("/repo/:site/:qual/:name", Route::Status(StatusFormat::Html)); - router.add("/repo/:site/:qual/:name/status.svg", Route::Status(StatusFormat::Svg)); + router.add("/repo/:site/:qual/:name", Route::RepoStatus(StatusFormat::Html)); + router.add("/repo/:site/:qual/:name/status.svg", Route::RepoStatus(StatusFormat::Svg)); + + router.add("/crate/:name/:version", Route::CrateStatus(StatusFormat::Html)); + router.add("/crate/:name/:version/status.svg", Route::CrateStatus(StatusFormat::Svg)); Server { logger, engine, router: Arc::new(router) } } @@ -70,9 +76,14 @@ impl Service for Server { return Box::new(self.index(req, route_match.params, logger)); } }, - &Route::Status(format) => { + &Route::RepoStatus(format) => { if *req.method() == Method::Get { - return Box::new(self.status(req, route_match.params, logger, format)); + return Box::new(self.repo_status(req, route_match.params, logger, format)); + } + }, + &Route::CrateStatus(format) => { + if *req.method() == Method::Get { + return Box::new(self.crate_status(req, route_match.params, logger, format)); } }, &Route::Static(file) => { @@ -108,7 +119,7 @@ impl Server { }) } - fn status(&self, _req: Request, params: Params, logger: Logger, format: StatusFormat) -> + fn repo_status(&self, _req: Request, params: Params, logger: Logger, format: StatusFormat) -> impl Future { let server = self.clone(); @@ -127,15 +138,15 @@ impl Server { future::Either::A(future::ok(response)) }, Ok(repo_path) => { - future::Either::B(server.engine.analyze_dependencies(repo_path.clone()).then(move |analyze_result| { + future::Either::B(server.engine.analyze_repo_dependencies(repo_path.clone()).then(move |analyze_result| { match analyze_result { Err(err) => { error!(logger, "error: {}", err); - let response = Server::status_format_analysis(None, format, repo_path); + let response = Server::status_format_analysis(None, format, SubjectPath::Repo(repo_path)); future::ok(response) }, Ok(analysis_outcome) => { - let response = Server::status_format_analysis(Some(analysis_outcome), format, repo_path); + let response = Server::status_format_analysis(Some(analysis_outcome), format, SubjectPath::Repo(repo_path)); future::ok(response) } } @@ -145,12 +156,48 @@ impl Server { }) } - fn status_format_analysis(analysis_outcome: Option, format: StatusFormat, repo_path: RepoPath) -> Response { + fn crate_status(&self, _req: Request, params: Params, logger: Logger, format: StatusFormat) -> + impl Future + { + let server = self.clone(); + + let name = params.find("name").expect("route param 'name' not found"); + let version = params.find("version").expect("route param 'version' not found"); + + CratePath::from_parts(name, version).into_future().then(move |crate_path_result| { + match crate_path_result { + Err(err) => { + error!(logger, "error: {}", err); + let mut response = views::html::error::render("Could not parse crate path", + "Please make sure to provide a valid crate name and version."); + response.set_status(StatusCode::BadRequest); + future::Either::A(future::ok(response)) + }, + Ok(crate_path) => { + future::Either::B(server.engine.analyze_crate_dependencies(crate_path.clone()).then(move |analyze_result| { + match analyze_result { + Err(err) => { + error!(logger, "error: {}", err); + let response = Server::status_format_analysis(None, format, SubjectPath::Crate(crate_path)); + future::ok(response) + }, + Ok(analysis_outcome) => { + let response = Server::status_format_analysis(Some(analysis_outcome), format, SubjectPath::Crate(crate_path)); + future::ok(response) + } + } + })) + } + } + }) + } + + fn status_format_analysis(analysis_outcome: Option, format: StatusFormat, subject_path: SubjectPath) -> Response { match format { StatusFormat::Svg => views::badge::response(analysis_outcome.as_ref()), StatusFormat::Html => - views::html::status::render(analysis_outcome, repo_path) + views::html::status::render(analysis_outcome, subject_path) } } diff --git a/src/server/views/html/status.rs b/src/server/views/html/status.rs index 116b98a..fc4aa6a 100644 --- a/src/server/views/html/status.rs +++ b/src/server/views/html/status.rs @@ -4,7 +4,8 @@ use ordermap::OrderMap; use ::engine::AnalyzeDependenciesOutcome; use ::models::crates::{CrateName, AnalyzedDependency, AnalyzedDependencies}; -use ::models::repo::{RepoSite, RepoPath}; +use ::models::SubjectPath; +use ::models::repo::RepoSite; use super::super::badge; @@ -84,26 +85,44 @@ fn dependency_table(title: &str, deps: OrderMap) } } -fn get_site_icon(repo_site: &RepoSite) -> &'static str { - match *repo_site { +fn get_site_icon(site: &RepoSite) -> &'static str { + match *site { RepoSite::Github => "fa-github", RepoSite::Gitlab => "fa-gitlab", - RepoSite::Bitbucket => "fa-bitbucket", + RepoSite::Bitbucket => "fa-bitbucket" } } -fn render_failure(repo_path: RepoPath) -> Markup { - let site_icon = get_site_icon(&repo_path.site); +fn render_title(subject_path: &SubjectPath) -> Markup { + match *subject_path { + SubjectPath::Repo(ref repo_path) => { + let site_icon = get_site_icon(&repo_path.site); + html! { + a href=(format!("{}/{}/{}", repo_path.site.to_base_uri(), repo_path.qual.as_ref(), repo_path.name.as_ref())) { + i class=(format!("fa {}", site_icon)) "" + (format!(" {} / {}", repo_path.qual.as_ref(), repo_path.name.as_ref())) + } + } + }, + SubjectPath::Crate(ref crate_path) => { + html! { + a href=(format!("https://crates.io/crates/{}/{}", crate_path.name.as_ref(), crate_path.version)) { + i class="fa fa-cube" "" + (format!(" {} {}", crate_path.name.as_ref(), crate_path.version)) + } + } + } + } +} + +fn render_failure(subject_path: SubjectPath) -> Markup { html! { section class="hero is-light" { div class="hero-head" (super::render_navbar()) div class="hero-body" { div class="container" { h1 class="title is-1" { - a href=(format!("{}/{}/{}", repo_path.site.to_base_uri(), repo_path.qual.as_ref(), repo_path.name.as_ref())) { - i class=(format!("fa {}", site_icon)) "" - (format!(" {} / {}", repo_path.qual.as_ref(), repo_path.name.as_ref())) - } + (render_title(&subject_path)) } } } @@ -120,10 +139,14 @@ fn render_failure(repo_path: RepoPath) -> Markup { } } -fn render_success(analysis_outcome: AnalyzeDependenciesOutcome, repo_path: RepoPath) -> Markup { - let self_path = format!("repo/{}/{}/{}", repo_path.site.as_ref(), repo_path.qual.as_ref(), repo_path.name.as_ref()); +fn render_success(analysis_outcome: AnalyzeDependenciesOutcome, subject_path: SubjectPath) -> Markup { + let self_path = match subject_path { + SubjectPath::Repo(ref repo_path) => + format!("repo/{}/{}/{}", repo_path.site.as_ref(), repo_path.qual.as_ref(), repo_path.name.as_ref()), + SubjectPath::Crate(ref crate_path) => + format!("crate/{}/{}", crate_path.name.as_ref(), crate_path.version) + }; let status_base_url = format!("{}/{}", &super::SELF_BASE_URL as &str, self_path); - let site_icon = get_site_icon(&repo_path.site); let status_data_uri = badge::badge(Some(&analysis_outcome)).to_svg_data_uri(); @@ -139,10 +162,7 @@ fn render_success(analysis_outcome: AnalyzeDependenciesOutcome, repo_path: RepoP div class="hero-body" { div class="container" { h1 class="title is-1" { - a href=(format!("{}/{}/{}", repo_path.site.to_base_uri(), repo_path.qual.as_ref(), repo_path.name.as_ref())) { - i class=(format!("fa {}", site_icon)) "" - (format!(" {} / {}", repo_path.qual.as_ref(), repo_path.name.as_ref())) - } + (render_title(&subject_path)) } img src=(status_data_uri); @@ -167,12 +187,17 @@ fn render_success(analysis_outcome: AnalyzeDependenciesOutcome, repo_path: RepoP } } -pub fn render(analysis_outcome: Option, repo_path: RepoPath) -> Response { - let title = format!("{} / {}", repo_path.qual.as_ref(), repo_path.name.as_ref()); +pub fn render(analysis_outcome: Option, subject_path: SubjectPath) -> Response { + let title = match subject_path { + SubjectPath::Repo(ref repo_path) => + format!("{} / {}", repo_path.qual.as_ref(), repo_path.name.as_ref()), + SubjectPath::Crate(ref crate_path) => + format!("{} {}", crate_path.name.as_ref(), crate_path.version) + }; if let Some(outcome) = analysis_outcome { - super::render_html(&title, render_success(outcome, repo_path)) + super::render_html(&title, render_success(outcome, subject_path)) } else { - super::render_html(&title, render_failure(repo_path)) + super::render_html(&title, render_failure(subject_path)) } }