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 slog::Logger;
|
||||
|
||||
use ::models::repo::RepoPath;
|
||||
use ::models::repo::{Repository, RepoPath};
|
||||
use ::models::crates::{CrateName, CrateRelease, CrateManifest, AnalyzedDependencies};
|
||||
|
||||
use ::parsers::manifest::{ManifestParseError, parse_manifest_toml};
|
||||
|
||||
use ::interactors::crates::{QueryCrateError, query_crate};
|
||||
use ::interactors::github::{RetrieveFileAtPathError, retrieve_file_at_path};
|
||||
use ::interactors::github::get_popular_repos;
|
||||
pub use ::interactors::github::GetPopularReposError;
|
||||
|
||||
use self::analyzer::DependencyAnalyzer;
|
||||
|
||||
|
@ -37,6 +39,12 @@ pub struct AnalyzeDependenciesOutcome {
|
|||
}
|
||||
|
||||
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) ->
|
||||
impl Future<Item=AnalyzeDependenciesOutcome, Error=AnalyzeDependenciesError>
|
||||
{
|
||||
|
|
|
@ -3,10 +3,13 @@ use std::string::FromUtf8Error;
|
|||
use futures::{Future, IntoFuture, Stream, future};
|
||||
use hyper::{Error as HyperError, Method, Request, Response, StatusCode};
|
||||
use hyper::error::UriError;
|
||||
use hyper::header::UserAgent;
|
||||
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";
|
||||
|
||||
#[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;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Repository {
|
||||
pub path: RepoPath,
|
||||
pub description: String
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RepoPath {
|
||||
pub site: RepoSite,
|
||||
|
|
|
@ -26,6 +26,7 @@ enum StaticFile {
|
|||
}
|
||||
|
||||
enum Route {
|
||||
Index,
|
||||
Static(StaticFile),
|
||||
Status(StatusFormat)
|
||||
}
|
||||
|
@ -40,6 +41,8 @@ impl Server {
|
|||
pub fn new(engine: Engine) -> Server {
|
||||
let mut router = Router::new();
|
||||
|
||||
router.add("/", Route::Index);
|
||||
|
||||
router.add("/static/style.css", Route::Static(StaticFile::StyleCss));
|
||||
|
||||
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 {
|
||||
if let Ok(route_match) = self.router.recognize(req.uri().path()) {
|
||||
match route_match.handler {
|
||||
&Route::Index => {
|
||||
if *req.method() == Method::Get {
|
||||
return Box::new(self.index(req, route_match.params));
|
||||
}
|
||||
},
|
||||
&Route::Status(format) => {
|
||||
if *req.method() == Method::Get {
|
||||
return Box::new(self.status(req, route_match.params, format));
|
||||
|
@ -80,6 +88,23 @@ impl Service for 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) ->
|
||||
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::header::ContentType;
|
||||
use maud::{Render, html};
|
||||
use maud::{Markup, Render, html};
|
||||
|
||||
pub mod index;
|
||||
pub mod status;
|
||||
|
||||
lazy_static! {
|
||||
|
@ -35,3 +36,17 @@ fn render_html<B: Render>(title: &str, body: B) -> Response {
|
|||
.with_header(ContentType::html())
|
||||
.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 {
|
||||
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 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() {
|
||||
("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! {
|
||||
section class=(format!("hero {}", hero_class)) {
|
||||
div class="hero-head" (super::render_navbar())
|
||||
div class="hero-body" {
|
||||
div class="container" {
|
||||
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