first simple version of crate dependency reports

This commit is contained in:
Sam Rijs 2018-02-17 00:25:34 +11:00
parent 12e4d7df51
commit 7fff95203e
6 changed files with 182 additions and 41 deletions

View file

@ -3,7 +3,7 @@ use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use failure::Error; use failure::Error;
use futures::Future; use futures::{Future, future};
use futures::future::join_all; use futures::future::join_all;
use hyper::Client; use hyper::Client;
use hyper::client::HttpConnector; use hyper::client::HttpConnector;
@ -18,7 +18,7 @@ mod futures;
use ::utils::cache::Cache; use ::utils::cache::Cache;
use ::models::repo::{Repository, RepoPath}; use ::models::repo::{Repository, RepoPath};
use ::models::crates::{CrateName, CrateRelease, AnalyzedDependencies}; use ::models::crates::{CrateName, CratePath, CrateRelease, AnalyzedDependencies};
use ::interactors::crates::QueryCrate; use ::interactors::crates::QueryCrate;
use ::interactors::RetrieveFileAtPath; 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<Item=AnalyzeDependenciesOutcome, Error=Error> impl Future<Item=AnalyzeDependenciesOutcome, Error=Error>
{ {
let start = Instant::now(); let start = Instant::now();
@ -109,6 +109,33 @@ impl Engine {
}) })
} }
pub fn analyze_crate_dependencies(&self, crate_path: CratePath) ->
impl Future<Item=AnalyzeDependenciesOutcome, Error=Error>
{
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<I: IntoIterator<Item=CrateName>>(&self, names: I) -> fn fetch_releases<I: IntoIterator<Item=CrateName>>(&self, names: I) ->
impl Iterator<Item=impl Future<Item=Vec<CrateRelease>, Error=Error>> impl Iterator<Item=impl Future<Item=Vec<CrateRelease>, Error=Error>>
{ {

View file

@ -4,28 +4,49 @@ use failure::Error;
use futures::{Future, Stream, IntoFuture, future}; use futures::{Future, Stream, IntoFuture, future};
use hyper::{Error as HyperError, Method, Request, Response, Uri}; use hyper::{Error as HyperError, Method, Request, Response, Uri};
use tokio_service::Service; use tokio_service::Service;
use semver::Version; use semver::{Version, VersionReq};
use serde_json; 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"; 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<String>
}
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct RegistryPackage { struct RegistryPackage {
vers: Version, vers: Version,
#[serde(default)] #[serde(default)]
deps: Vec<RegistryPackageDep>,
#[serde(default)]
yanked: bool yanked: bool
} }
fn convert_pkgs(name: &CrateName, packages: Vec<RegistryPackage>) -> Result<QueryCrateResponse, Error> { fn convert_pkgs(name: &CrateName, packages: Vec<RegistryPackage>) -> Result<QueryCrateResponse, Error> {
let releases = packages.into_iter().map(|package| { 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(), name: name.clone(),
version: package.vers, version: package.vers,
deps: deps,
yanked: package.yanked yanked: package.yanked
} })
}).collect(); }).collect::<Result<_, Error>>()?;
Ok(QueryCrateResponse { Ok(QueryCrateResponse {
releases: releases releases: releases
@ -72,7 +93,7 @@ impl<S> Service for QueryCrate<S>
future::Either::A(future::err(format_err!("Status code {} for URI {}", status, uri))) future::Either::A(future::err(format_err!("Status code {} for URI {}", status, uri)))
} else { } else {
let body_future = response.body().concat2().from_err(); 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 string_body = str::from_utf8(body.as_ref())?;
let packages = string_body.lines() let packages = string_body.lines()
.map(|s| s.trim()) .map(|s| s.trim())

View file

@ -6,6 +6,21 @@ use ordermap::OrderMap;
use relative_path::RelativePathBuf; use relative_path::RelativePathBuf;
use semver::{Version, VersionReq}; 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<CratePath, Error> {
Ok(CratePath {
name: name.parse()?,
version: version.parse()?
})
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CrateName(String); pub struct CrateName(String);
@ -47,6 +62,7 @@ impl FromStr for CrateName {
pub struct CrateRelease { pub struct CrateRelease {
pub name: CrateName, pub name: CrateName,
pub version: Version, pub version: Version,
pub deps: CrateDeps,
pub yanked: bool pub yanked: bool
} }

View file

@ -1,2 +1,7 @@
pub mod crates; pub mod crates;
pub mod repo; pub mod repo;
pub enum SubjectPath {
Repo(self::repo::RepoPath),
Crate(self::crates::CratePath)
}

View file

@ -11,7 +11,9 @@ mod assets;
mod views; mod views;
use ::engine::{Engine, AnalyzeDependenciesOutcome}; use ::engine::{Engine, AnalyzeDependenciesOutcome};
use ::models::crates::CratePath;
use ::models::repo::RepoPath; use ::models::repo::RepoPath;
use ::models::SubjectPath;
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq)]
enum StatusFormat { enum StatusFormat {
@ -28,7 +30,8 @@ enum StaticFile {
enum Route { enum Route {
Index, Index,
Static(StaticFile), Static(StaticFile),
Status(StatusFormat) RepoStatus(StatusFormat),
CrateStatus(StatusFormat)
} }
#[derive(Clone)] #[derive(Clone)]
@ -47,8 +50,11 @@ impl Server {
router.add("/static/style.css", Route::Static(StaticFile::StyleCss)); router.add("/static/style.css", Route::Static(StaticFile::StyleCss));
router.add("/static/favicon.png", Route::Static(StaticFile::FaviconPng)); 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", Route::RepoStatus(StatusFormat::Html));
router.add("/repo/:site/:qual/:name/status.svg", Route::Status(StatusFormat::Svg)); 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) } 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)); return Box::new(self.index(req, route_match.params, logger));
} }
}, },
&Route::Status(format) => { &Route::RepoStatus(format) => {
if *req.method() == Method::Get { 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) => { &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<Item=Response, Error=HyperError> impl Future<Item=Response, Error=HyperError>
{ {
let server = self.clone(); let server = self.clone();
@ -127,15 +138,15 @@ impl Server {
future::Either::A(future::ok(response)) future::Either::A(future::ok(response))
}, },
Ok(repo_path) => { 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 { match analyze_result {
Err(err) => { Err(err) => {
error!(logger, "error: {}", 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) future::ok(response)
}, },
Ok(analysis_outcome) => { 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) future::ok(response)
} }
} }
@ -145,12 +156,48 @@ impl Server {
}) })
} }
fn status_format_analysis(analysis_outcome: Option<AnalyzeDependenciesOutcome>, format: StatusFormat, repo_path: RepoPath) -> Response { fn crate_status(&self, _req: Request, params: Params, logger: Logger, format: StatusFormat) ->
impl Future<Item=Response, Error=HyperError>
{
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<AnalyzeDependenciesOutcome>, format: StatusFormat, subject_path: SubjectPath) -> Response {
match format { match format {
StatusFormat::Svg => StatusFormat::Svg =>
views::badge::response(analysis_outcome.as_ref()), views::badge::response(analysis_outcome.as_ref()),
StatusFormat::Html => StatusFormat::Html =>
views::html::status::render(analysis_outcome, repo_path) views::html::status::render(analysis_outcome, subject_path)
} }
} }

View file

@ -4,7 +4,8 @@ use ordermap::OrderMap;
use ::engine::AnalyzeDependenciesOutcome; use ::engine::AnalyzeDependenciesOutcome;
use ::models::crates::{CrateName, AnalyzedDependency, AnalyzedDependencies}; use ::models::crates::{CrateName, AnalyzedDependency, AnalyzedDependencies};
use ::models::repo::{RepoSite, RepoPath}; use ::models::SubjectPath;
use ::models::repo::RepoSite;
use super::super::badge; use super::super::badge;
@ -84,26 +85,44 @@ fn dependency_table(title: &str, deps: OrderMap<CrateName, AnalyzedDependency>)
} }
} }
fn get_site_icon(repo_site: &RepoSite) -> &'static str { fn get_site_icon(site: &RepoSite) -> &'static str {
match *repo_site { match *site {
RepoSite::Github => "fa-github", RepoSite::Github => "fa-github",
RepoSite::Gitlab => "fa-gitlab", RepoSite::Gitlab => "fa-gitlab",
RepoSite::Bitbucket => "fa-bitbucket", RepoSite::Bitbucket => "fa-bitbucket"
} }
} }
fn render_failure(repo_path: RepoPath) -> Markup { fn render_title(subject_path: &SubjectPath) -> Markup {
let site_icon = get_site_icon(&repo_path.site); 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! { html! {
section class="hero is-light" { section class="hero is-light" {
div class="hero-head" (super::render_navbar()) div class="hero-head" (super::render_navbar())
div class="hero-body" { div class="hero-body" {
div class="container" { div class="container" {
h1 class="title is-1" { h1 class="title is-1" {
a href=(format!("{}/{}/{}", repo_path.site.to_base_uri(), repo_path.qual.as_ref(), repo_path.name.as_ref())) { (render_title(&subject_path))
i class=(format!("fa {}", site_icon)) ""
(format!(" {} / {}", repo_path.qual.as_ref(), repo_path.name.as_ref()))
}
} }
} }
} }
@ -120,10 +139,14 @@ fn render_failure(repo_path: RepoPath) -> Markup {
} }
} }
fn render_success(analysis_outcome: AnalyzeDependenciesOutcome, repo_path: RepoPath) -> Markup { fn render_success(analysis_outcome: AnalyzeDependenciesOutcome, subject_path: SubjectPath) -> Markup {
let self_path = format!("repo/{}/{}/{}", repo_path.site.as_ref(), repo_path.qual.as_ref(), repo_path.name.as_ref()); 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 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(); 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="hero-body" {
div class="container" { div class="container" {
h1 class="title is-1" { h1 class="title is-1" {
a href=(format!("{}/{}/{}", repo_path.site.to_base_uri(), repo_path.qual.as_ref(), repo_path.name.as_ref())) { (render_title(&subject_path))
i class=(format!("fa {}", site_icon)) ""
(format!(" {} / {}", repo_path.qual.as_ref(), repo_path.name.as_ref()))
}
} }
img src=(status_data_uri); img src=(status_data_uri);
@ -167,12 +187,17 @@ fn render_success(analysis_outcome: AnalyzeDependenciesOutcome, repo_path: RepoP
} }
} }
pub fn render(analysis_outcome: Option<AnalyzeDependenciesOutcome>, repo_path: RepoPath) -> Response { pub fn render(analysis_outcome: Option<AnalyzeDependenciesOutcome>, subject_path: SubjectPath) -> Response {
let title = format!("{} / {}", repo_path.qual.as_ref(), repo_path.name.as_ref()); 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 { 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 { } else {
super::render_html(&title, render_failure(repo_path)) super::render_html(&title, render_failure(subject_path))
} }
} }