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 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<Item=AnalyzeDependenciesOutcome, Error=Error>
{
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) ->
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 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<String>
}
#[derive(Deserialize, Debug)]
struct RegistryPackage {
vers: Version,
#[serde(default)]
deps: Vec<RegistryPackageDep>,
#[serde(default)]
yanked: bool
}
fn convert_pkgs(name: &CrateName, packages: Vec<RegistryPackage>) -> Result<QueryCrateResponse, Error> {
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::<Result<_, Error>>()?;
Ok(QueryCrateResponse {
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)))
} 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())

View file

@ -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<CratePath, Error> {
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
}

View file

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

View file

@ -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<Item=Response, Error=HyperError>
{
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<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 {
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)
}
}

View file

@ -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<CrateName, AnalyzedDependency>)
}
}
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<AnalyzeDependenciesOutcome>, repo_path: RepoPath) -> Response {
let title = format!("{} / {}", repo_path.qual.as_ref(), repo_path.name.as_ref());
pub fn render(analysis_outcome: Option<AnalyzeDependenciesOutcome>, 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))
}
}