mirror of
https://github.com/deps-rs/deps.rs.git
synced 2024-11-24 19:16:31 +00:00
add first prototype of landing page
This commit is contained in:
parent
3d6ea4be6f
commit
dff0f8d6e3
8 changed files with 173 additions and 149 deletions
|
@ -6,13 +6,15 @@ use hyper::client::HttpConnector;
|
||||||
use hyper_tls::HttpsConnector;
|
use hyper_tls::HttpsConnector;
|
||||||
use slog::Logger;
|
use slog::Logger;
|
||||||
|
|
||||||
use ::models::repo::RepoPath;
|
use ::models::repo::{Repository, RepoPath};
|
||||||
use ::models::crates::{CrateName, CrateRelease, CrateManifest, AnalyzedDependencies};
|
use ::models::crates::{CrateName, CrateRelease, CrateManifest, AnalyzedDependencies};
|
||||||
|
|
||||||
use ::parsers::manifest::{ManifestParseError, parse_manifest_toml};
|
use ::parsers::manifest::{ManifestParseError, parse_manifest_toml};
|
||||||
|
|
||||||
use ::interactors::crates::{QueryCrateError, query_crate};
|
use ::interactors::crates::{QueryCrateError, query_crate};
|
||||||
use ::interactors::github::{RetrieveFileAtPathError, retrieve_file_at_path};
|
use ::interactors::github::{RetrieveFileAtPathError, retrieve_file_at_path};
|
||||||
|
use ::interactors::github::get_popular_repos;
|
||||||
|
pub use ::interactors::github::GetPopularReposError;
|
||||||
|
|
||||||
use self::analyzer::DependencyAnalyzer;
|
use self::analyzer::DependencyAnalyzer;
|
||||||
|
|
||||||
|
@ -37,6 +39,12 @@ pub struct AnalyzeDependenciesOutcome {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Engine {
|
impl Engine {
|
||||||
|
pub fn get_popular_repos(&self) ->
|
||||||
|
impl Future<Item=Vec<Repository>, Error=GetPopularReposError>
|
||||||
|
{
|
||||||
|
get_popular_repos(self.client.clone())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn analyze_dependencies(&self, repo_path: RepoPath) ->
|
pub fn analyze_dependencies(&self, repo_path: RepoPath) ->
|
||||||
impl Future<Item=AnalyzeDependenciesOutcome, Error=AnalyzeDependenciesError>
|
impl Future<Item=AnalyzeDependenciesOutcome, Error=AnalyzeDependenciesError>
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,10 +3,13 @@ use std::string::FromUtf8Error;
|
||||||
use futures::{Future, IntoFuture, Stream, future};
|
use futures::{Future, IntoFuture, Stream, future};
|
||||||
use hyper::{Error as HyperError, Method, Request, Response, StatusCode};
|
use hyper::{Error as HyperError, Method, Request, Response, StatusCode};
|
||||||
use hyper::error::UriError;
|
use hyper::error::UriError;
|
||||||
|
use hyper::header::UserAgent;
|
||||||
use tokio_service::Service;
|
use tokio_service::Service;
|
||||||
|
use serde_json;
|
||||||
|
|
||||||
use ::models::repo::RepoPath;
|
use ::models::repo::{Repository, RepoPath, RepoValidationError};
|
||||||
|
|
||||||
|
const GITHUB_API_BASE_URI: &'static str = "https://api.github.com";
|
||||||
const GITHUB_USER_CONTENT_BASE_URI: &'static str = "https://raw.githubusercontent.com";
|
const GITHUB_USER_CONTENT_BASE_URI: &'static str = "https://raw.githubusercontent.com";
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -44,3 +47,60 @@ pub fn retrieve_file_at_path<S>(service: S, repo_path: &RepoPath, file_path: &st
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum GetPopularReposError {
|
||||||
|
Uri(UriError),
|
||||||
|
Transport(HyperError),
|
||||||
|
Status(StatusCode),
|
||||||
|
Decode(serde_json::Error),
|
||||||
|
Validate(RepoValidationError)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct GithubSearchResponse {
|
||||||
|
items: Vec<GithubRepo>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct GithubRepo {
|
||||||
|
name: String,
|
||||||
|
owner: GithubOwner,
|
||||||
|
description: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct GithubOwner {
|
||||||
|
login: String
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_popular_repos<S>(service: S) ->
|
||||||
|
impl Future<Item=Vec<Repository>, Error=GetPopularReposError>
|
||||||
|
where S: Service<Request=Request, Response=Response, Error=HyperError>
|
||||||
|
{
|
||||||
|
let uri_future = format!("{}/search/repositories?q=language:rust&sort=stars", GITHUB_API_BASE_URI)
|
||||||
|
.parse().into_future().map_err(GetPopularReposError::Uri);
|
||||||
|
|
||||||
|
uri_future.and_then(move |uri| {
|
||||||
|
let mut request = Request::new(Method::Get, uri);
|
||||||
|
request.headers_mut().set(UserAgent::new("deps.rs"));
|
||||||
|
|
||||||
|
service.call(request).map_err(GetPopularReposError::Transport).and_then(|response| {
|
||||||
|
let status = response.status();
|
||||||
|
if !status.is_success() {
|
||||||
|
future::Either::A(future::err(GetPopularReposError::Status(status)))
|
||||||
|
} else {
|
||||||
|
let body_future = response.body().concat2().map_err(GetPopularReposError::Transport);
|
||||||
|
let decode_future = body_future
|
||||||
|
.and_then(|body| serde_json::from_slice(body.as_ref()).map_err(GetPopularReposError::Decode));
|
||||||
|
future::Either::B(decode_future.and_then(|search_response: GithubSearchResponse| {
|
||||||
|
search_response.items.into_iter().map(|item| {
|
||||||
|
let path = RepoPath::from_parts("github", &item.owner.login, &item.name)
|
||||||
|
.map_err(GetPopularReposError::Validate)?;
|
||||||
|
Ok(Repository { path, description: item.description })
|
||||||
|
}).collect::<Result<Vec<_>, _>>()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Repository {
|
||||||
|
pub path: RepoPath,
|
||||||
|
pub description: String
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RepoPath {
|
pub struct RepoPath {
|
||||||
pub site: RepoSite,
|
pub site: RepoSite,
|
||||||
|
|
|
@ -26,6 +26,7 @@ enum StaticFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Route {
|
enum Route {
|
||||||
|
Index,
|
||||||
Static(StaticFile),
|
Static(StaticFile),
|
||||||
Status(StatusFormat)
|
Status(StatusFormat)
|
||||||
}
|
}
|
||||||
|
@ -40,6 +41,8 @@ impl Server {
|
||||||
pub fn new(engine: Engine) -> Server {
|
pub fn new(engine: Engine) -> Server {
|
||||||
let mut router = Router::new();
|
let mut router = Router::new();
|
||||||
|
|
||||||
|
router.add("/", Route::Index);
|
||||||
|
|
||||||
router.add("/static/style.css", Route::Static(StaticFile::StyleCss));
|
router.add("/static/style.css", Route::Static(StaticFile::StyleCss));
|
||||||
|
|
||||||
router.add("/repo/:site/:qual/:name", Route::Status(StatusFormat::Html));
|
router.add("/repo/:site/:qual/:name", Route::Status(StatusFormat::Html));
|
||||||
|
@ -59,6 +62,11 @@ impl Service for Server {
|
||||||
fn call(&self, req: Request) -> Self::Future {
|
fn call(&self, req: Request) -> Self::Future {
|
||||||
if let Ok(route_match) = self.router.recognize(req.uri().path()) {
|
if let Ok(route_match) = self.router.recognize(req.uri().path()) {
|
||||||
match route_match.handler {
|
match route_match.handler {
|
||||||
|
&Route::Index => {
|
||||||
|
if *req.method() == Method::Get {
|
||||||
|
return Box::new(self.index(req, route_match.params));
|
||||||
|
}
|
||||||
|
},
|
||||||
&Route::Status(format) => {
|
&Route::Status(format) => {
|
||||||
if *req.method() == Method::Get {
|
if *req.method() == Method::Get {
|
||||||
return Box::new(self.status(req, route_match.params, format));
|
return Box::new(self.status(req, route_match.params, format));
|
||||||
|
@ -80,6 +88,23 @@ impl Service for Server {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
|
fn index(&self, _req: Request, _params: Params) ->
|
||||||
|
impl Future<Item=Response, Error=HyperError>
|
||||||
|
{
|
||||||
|
self.engine.get_popular_repos().then(|popular_result| {
|
||||||
|
match popular_result {
|
||||||
|
Err(err) => {
|
||||||
|
let mut response = Response::new();
|
||||||
|
response.set_status(StatusCode::BadRequest);
|
||||||
|
response.set_body(format!("{:?}", err));
|
||||||
|
future::ok(response)
|
||||||
|
},
|
||||||
|
Ok(popular) =>
|
||||||
|
future::ok(views::html::index::render(popular))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn status(&self, _req: Request, params: Params, format: StatusFormat) ->
|
fn status(&self, _req: Request, params: Params, format: StatusFormat) ->
|
||||||
impl Future<Item=Response, Error=HyperError>
|
impl Future<Item=Response, Error=HyperError>
|
||||||
{
|
{
|
||||||
|
|
54
src/server/views/html/index.rs
Normal file
54
src/server/views/html/index.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
use hyper::Response;
|
||||||
|
use maud::{Markup, html};
|
||||||
|
|
||||||
|
use ::models::repo::Repository;
|
||||||
|
|
||||||
|
fn popular_table(popular: Vec<Repository>) -> Markup {
|
||||||
|
html! {
|
||||||
|
h2 class="title is-3" "Popular"
|
||||||
|
|
||||||
|
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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(popular: Vec<Repository>) -> Response {
|
||||||
|
super::render_html("Keep your dependencies up-to-date - Deps.rs", html! {
|
||||||
|
section class="hero is-light" {
|
||||||
|
div class="hero-head" (super::render_navbar())
|
||||||
|
div class="hero-body" {
|
||||||
|
div class="container" {
|
||||||
|
p class="title is-1" "Keep your dependencies up-to-date"
|
||||||
|
p {
|
||||||
|
"Docs.rs uses semantic versioning to detect outdated or insecure dependencies in your project's"
|
||||||
|
code "Cargo.toml"
|
||||||
|
"."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
section class="section" {
|
||||||
|
div class="container" (popular_table(popular))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -2,8 +2,9 @@ use std::env;
|
||||||
|
|
||||||
use hyper::Response;
|
use hyper::Response;
|
||||||
use hyper::header::ContentType;
|
use hyper::header::ContentType;
|
||||||
use maud::{Render, html};
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
pub mod index;
|
||||||
pub mod status;
|
pub mod status;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
|
@ -35,3 +36,17 @@ fn render_html<B: Render>(title: &str, body: B) -> Response {
|
||||||
.with_header(ContentType::html())
|
.with_header(ContentType::html())
|
||||||
.with_body(rendered.0)
|
.with_body(rendered.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_navbar() -> Markup {
|
||||||
|
html! {
|
||||||
|
header class="navbar" {
|
||||||
|
div class="container" {
|
||||||
|
div class="navbar-brand" {
|
||||||
|
a class="navbar-item is-dark" href=(SELF_BASE_URL) {
|
||||||
|
h1 class="title is-3" "Deps.rs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ fn dependency_table(title: &str, deps: BTreeMap<CrateName, AnalyzedDependency>)
|
||||||
pub fn render(analysis_outcome: AnalyzeDependenciesOutcome, repo_path: RepoPath) -> Response {
|
pub fn render(analysis_outcome: AnalyzeDependenciesOutcome, repo_path: RepoPath) -> Response {
|
||||||
let self_path = format!("repo/{}/{}/{}", repo_path.site.as_ref(), repo_path.qual.as_ref(), repo_path.name.as_ref());
|
let self_path = format!("repo/{}/{}/{}", repo_path.site.as_ref(), repo_path.qual.as_ref(), repo_path.name.as_ref());
|
||||||
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 title = format!("{} / {} - Dependency Status", repo_path.qual.as_ref(), repo_path.name.as_ref());
|
let title = format!("{} / {} - Deps.rs", repo_path.qual.as_ref(), repo_path.name.as_ref());
|
||||||
|
|
||||||
let (hero_class, status_asset) = if analysis_outcome.deps.any_outdated() {
|
let (hero_class, status_asset) = if analysis_outcome.deps.any_outdated() {
|
||||||
("is-warning", assets::BADGE_OUTDATED_SVG.as_ref())
|
("is-warning", assets::BADGE_OUTDATED_SVG.as_ref())
|
||||||
|
@ -75,6 +75,7 @@ pub fn render(analysis_outcome: AnalyzeDependenciesOutcome, repo_path: RepoPath)
|
||||||
|
|
||||||
super::render_html(&title, html! {
|
super::render_html(&title, html! {
|
||||||
section class=(format!("hero {}", hero_class)) {
|
section class=(format!("hero {}", hero_class)) {
|
||||||
|
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" {
|
||||||
|
|
|
@ -1,145 +0,0 @@
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::env;
|
|
||||||
|
|
||||||
use base64::display::Base64Display;
|
|
||||||
use hyper::Response;
|
|
||||||
use hyper::header::ContentType;
|
|
||||||
use maud::{Markup, html};
|
|
||||||
|
|
||||||
use ::engine::AnalyzeDependenciesOutcome;
|
|
||||||
use ::models::crates::{CrateName, AnalyzedDependency};
|
|
||||||
use ::models::repo::RepoPath;
|
|
||||||
use ::server::assets;
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref SELF_BASE_URL: String = {
|
|
||||||
env::var("BASE_URL")
|
|
||||||
.unwrap_or_else(|_| "http://localhost:8080".to_string())
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dependency_table(title: &str, deps: BTreeMap<CrateName, AnalyzedDependency>) -> Markup {
|
|
||||||
let count_total = deps.len();
|
|
||||||
let count_outdated = deps.iter().filter(|&(_, dep)| dep.is_outdated()).count();
|
|
||||||
|
|
||||||
html! {
|
|
||||||
h3 class="title is-4" (title)
|
|
||||||
p class="subtitle is-5" {
|
|
||||||
@if count_outdated > 0 {
|
|
||||||
(format!(" ({} total, {} up-to-date, {} outdated)", count_total, count_total - count_outdated, count_outdated))
|
|
||||||
} @else {
|
|
||||||
(format!(" ({} total, all up-to-date)", count_total))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table class="table is-fullwidth is-striped is-hoverable" {
|
|
||||||
thead {
|
|
||||||
tr {
|
|
||||||
th "Crate"
|
|
||||||
th class="has-text-right" "Required"
|
|
||||||
th class="has-text-right" "Latest"
|
|
||||||
th class="has-text-right" "Status"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tbody {
|
|
||||||
@for (name, dep) in deps {
|
|
||||||
tr {
|
|
||||||
td {
|
|
||||||
a href=(format!("https://crates.io/crates/{}", name.as_ref())) (name.as_ref())
|
|
||||||
}
|
|
||||||
td class="has-text-right" code (dep.required.to_string())
|
|
||||||
td class="has-text-right" {
|
|
||||||
@if let Some(ref latest) = dep.latest {
|
|
||||||
code (latest.to_string())
|
|
||||||
} @else {
|
|
||||||
"N/A"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
td class="has-text-right" {
|
|
||||||
@if dep.is_outdated() {
|
|
||||||
span class="tag is-warning" "out of date"
|
|
||||||
} @else {
|
|
||||||
span class="tag is-success" "up to date"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn status_html(analysis_outcome: AnalyzeDependenciesOutcome, repo_path: RepoPath) -> Response {
|
|
||||||
let self_path = format!("repo/{}/{}/{}", repo_path.site.as_ref(), repo_path.qual.as_ref(), repo_path.name.as_ref());
|
|
||||||
let status_base_url = format!("{}/{}", &SELF_BASE_URL as &str, self_path);
|
|
||||||
let title = format!("{} / {} - Dependency Status", repo_path.qual.as_ref(), repo_path.name.as_ref());
|
|
||||||
|
|
||||||
let (hero_class, status_asset) = if analysis_outcome.deps.any_outdated() {
|
|
||||||
("is-warning", assets::BADGE_OUTDATED_SVG.as_ref())
|
|
||||||
} else {
|
|
||||||
("is-success", assets::BADGE_UPTODATE_SVG.as_ref())
|
|
||||||
};
|
|
||||||
|
|
||||||
let status_data_url = format!("data:image/svg+xml;base64,{}", Base64Display::standard(status_asset));
|
|
||||||
|
|
||||||
let rendered = html! {
|
|
||||||
html {
|
|
||||||
head {
|
|
||||||
meta charset="utf-8";
|
|
||||||
meta name="viewport" content="width=device-width, initial-scale=1";
|
|
||||||
title (title)
|
|
||||||
link rel="stylesheet" type="text/css" href="/static/style.css";
|
|
||||||
link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Fira+Sans:400,500,600";
|
|
||||||
link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Source+Code+Pro";
|
|
||||||
link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css";
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
section class=(format!("hero {}", hero_class)) {
|
|
||||||
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="fa fa-github" ""
|
|
||||||
(format!(" {} / {}", repo_path.qual.as_ref(), repo_path.name.as_ref()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
img src=(status_data_url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div class="hero-footer" {
|
|
||||||
div class="container" {
|
|
||||||
pre class="is-size-7" {
|
|
||||||
(format!("[![dependency status]({}/status.svg)]({})", status_base_url, status_base_url))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
section class="section" {
|
|
||||||
div class="container" {
|
|
||||||
h2 class="title is-3" {
|
|
||||||
"Crate "
|
|
||||||
code (analysis_outcome.name.as_ref())
|
|
||||||
}
|
|
||||||
|
|
||||||
@if !analysis_outcome.deps.main.is_empty() {
|
|
||||||
(dependency_table("Dependencies", analysis_outcome.deps.main))
|
|
||||||
}
|
|
||||||
|
|
||||||
@if !analysis_outcome.deps.dev.is_empty() {
|
|
||||||
(dependency_table("Dev dependencies", analysis_outcome.deps.dev))
|
|
||||||
}
|
|
||||||
|
|
||||||
@if !analysis_outcome.deps.build.is_empty() {
|
|
||||||
(dependency_table("Build dependencies", analysis_outcome.deps.build))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Response::new()
|
|
||||||
.with_header(ContentType::html())
|
|
||||||
.with_body(rendered.0)
|
|
||||||
}
|
|
Loading…
Reference in a new issue