Skip to content

Commit

Permalink
feat: api endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
vladkens committed Sep 8, 2024
1 parent 473fe47 commit 26f181d
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 20 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ thousands = "0.2.0"
tokio-cron-scheduler = "0.11.0"
tracing-subscriber = "0.3.18"
tracing = "0.1.40"
tower-http = { version = "0.5.2", features = ["trace"] }
tower-http = { version = "0.5.2", features = ["trace", "cors"] }
dotenvy = "0.15.7"
serde_variant = "0.1.3"

Expand Down
17 changes: 16 additions & 1 deletion src/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
use std::collections::HashMap;

use axum::extract::{Query, Request};

use crate::{
db_client::DbClient,
db_client::{DbClient, RepoFilter, RepoTotals},
gh_client::{GhClient, Repo},
types::Res,
};

pub fn get_header<'a>(req: &'a Request, name: &'a str) -> Option<&'a str> {
match req.headers().get(name) {
Some(x) => Some(x.to_str().unwrap_or_default()),
None => None,
}
}

async fn check_hidden_repos(db: &DbClient, repos: &Vec<Repo>) -> Res {
let now_ids = repos.iter().map(|r| r.id as i64).collect::<Vec<_>>();
let was_ids = db.get_repos_ids().await?;
Expand Down Expand Up @@ -172,6 +181,12 @@ fn is_included(repo: &str, rules: &str) -> bool {
return false;
}

pub async fn get_filtered_repos(db: &DbClient, qs: &Query<RepoFilter>) -> Res<Vec<RepoTotals>> {
let repos = db.get_repos(&qs).await?;
let repos = repos.into_iter().filter(|x| is_repo_included(&x.name)).collect::<Vec<_>>();
Ok(repos)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
10 changes: 4 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use axum::{http::StatusCode, response::IntoResponse, Router};
mod db_client;
mod gh_client;
mod helpers;
mod pages;
mod routes;
mod types;

use db_client::{DbClient, RepoFilter};
Expand Down Expand Up @@ -97,7 +97,6 @@ async fn health() -> impl IntoResponse {

#[tokio::main]
async fn main() -> Res {
use crate::pages;
use tower_http::trace::{self, TraceLayer};
use tracing::Level;

Expand All @@ -112,15 +111,14 @@ async fn main() -> Res {
tracing::info!("{}", brand);

let router = Router::new()
.route("/", get(pages::index))
.route("/:owner/:repo", get(pages::repo_page))
.nest("/api", routes::api_routes())
.merge(routes::html_routes())
.layer(
TraceLayer::new_for_http()
.make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
.on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
)
// do not show logs for this routes
.route("/health", get(health));
.route("/health", get(health)); // do not show logs for this route

let state = Arc::new(AppState::new().await?);
let service = router.with_state(state.clone()).into_make_service();
Expand Down
36 changes: 36 additions & 0 deletions src/routes/api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use std::sync::Arc;

use axum::extract::{Query, Request, State};
use axum::Json;

use crate::db_client::{RepoFilter, RepoTotals};
use crate::helpers::get_filtered_repos;
use crate::types::JsonRes;
use crate::AppState;

#[derive(Debug, serde::Serialize)]
pub struct ReposList {
total_count: i32,
total_stars: i32,
total_forks: i32,
total_views: i32,
total_clones: i32,
items: Vec<RepoTotals>,
}

pub async fn api_get_repos(State(state): State<Arc<AppState>>, req: Request) -> JsonRes<ReposList> {
let db = &state.db;
let qs: Query<RepoFilter> = Query::try_from_uri(req.uri())?;
let repos = get_filtered_repos(&db, &qs).await?;

let repos_list = ReposList {
total_count: repos.len() as i32,
total_stars: repos.iter().map(|r| r.stars).sum(),
total_forks: repos.iter().map(|r| r.forks).sum(),
total_views: repos.iter().map(|r| r.views_count).sum(),
total_clones: repos.iter().map(|r| r.clones_count).sum(),
items: repos,
};

Ok(Json(repos_list))
}
19 changes: 7 additions & 12 deletions src/pages.rs → src/routes/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use thousands::Separable;
use crate::db_client::{
DbClient, Direction, PopularFilter, PopularKind, PopularSort, RepoFilter, RepoSort, RepoTotals,
};
use crate::helpers::is_repo_included;
use crate::helpers::{get_filtered_repos, is_repo_included};
use crate::types::{AppError, HtmlRes};
use crate::AppState;

Expand All @@ -19,10 +19,7 @@ struct TablePopularItem {
}

fn get_hx_target(req: &Request) -> Option<&str> {
match req.headers().get("hx-target") {
Some(x) => Some(x.to_str().unwrap_or_default()),
None => None,
}
crate::helpers::get_header(req, "hx-target")
}

fn maybe_url(item: &(String, Option<String>)) -> Markup {
Expand Down Expand Up @@ -68,7 +65,7 @@ fn base(state: &Arc<AppState>, navs: Vec<(String, Option<String>)>, inner: Marku
_ => &format!("{} · {}", navs.last().unwrap().0, app_name),
};

let favicon = include_str!("../assets/favicon.svg")
let favicon = include_str!("../../assets/favicon.svg")
.replace("\n", "")
.replace("\"", "%22")
.replace("#", "%23");
Expand All @@ -87,7 +84,7 @@ fn base(state: &Arc<AppState>, navs: Vec<(String, Option<String>)>, inner: Marku
script src="https://unpkg.com/[email protected]" {}
script src="https://unpkg.com/[email protected]" {}
script src="https://unpkg.com/[email protected]" {}
style { (PreEscaped(include_str!("../assets/app.css"))) }
style { (PreEscaped(include_str!("../../assets/app.css"))) }
}
body {
main class="container-fluid pt-0 main-box" {
Expand Down Expand Up @@ -327,7 +324,7 @@ pub async fn repo_page(
}
}

script { (PreEscaped(include_str!("../assets/app.js"))) }
script { (PreEscaped(include_str!("../../assets/app.js"))) }
script {
"const Metrics = "(PreEscaped(serde_json::to_string(&metrics)?))";"
"const Stars = "(PreEscaped(serde_json::to_string(&stars)?))";"
Expand All @@ -351,11 +348,9 @@ pub async fn repo_page(
// https://docs.rs/axum/latest/axum/extract/index.html#common-extractors
pub async fn index(State(state): State<Arc<AppState>>, req: Request) -> HtmlRes {
// let qs: Query<HashMap<String, String>> = Query::try_from_uri(req.uri())?;
let qs: Query<RepoFilter> = Query::try_from_uri(req.uri())?;

let db = &state.db;
let repos = db.get_repos(&qs).await?;
let repos = repos.into_iter().filter(|x| is_repo_included(&x.name)).collect::<Vec<_>>();
let qs: Query<RepoFilter> = Query::try_from_uri(req.uri())?;
let repos = get_filtered_repos(&db, &qs).await?;

let cols: Vec<(&str, Box<dyn Fn(&RepoTotals) -> Markup>, RepoSort)> = vec![
("Name", Box::new(|x| html!(a href=(format!("/{}", x.name)) { (x.name) })), RepoSort::Name),
Expand Down
40 changes: 40 additions & 0 deletions src/routes/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
mod api;
mod html;

use std::sync::Arc;

use axum::http::StatusCode;
use axum::{extract::Request, middleware::Next, response::IntoResponse, routing::get, Router};
use reqwest::Method;
use tower_http::cors::{Any, CorsLayer};

use crate::AppState;

async fn check_api_token(
req: Request,
next: Next,
) -> Result<impl IntoResponse, (StatusCode, String)> {
let ghs_token = std::env::var("GHS_API_TOKEN").unwrap_or_default();
let req_token = crate::helpers::get_header(&req, "x-api-token").unwrap_or_default();
if ghs_token.is_empty() || req_token != ghs_token {
return Err((StatusCode::UNAUTHORIZED, "unauthorized".to_string()));
}

let res = next.run(req).await;
Ok(res)
}

pub fn api_routes() -> Router<Arc<AppState>> {
let cors = CorsLayer::new().allow_methods([Method::GET]).allow_origin(Any);

let router = Router::new()
.route("/repos", get(api::api_get_repos))
.layer(axum::middleware::from_fn(check_api_token))
.layer(cors);

router
}

pub fn html_routes() -> Router<Arc<AppState>> {
Router::new().route("/", get(html::index)).route("/:owner/:repo", get(html::repo_page))
}
1 change: 1 addition & 0 deletions src/types.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// https://github.com/tokio-rs/axum/blob/main/examples/anyhow-error-response/src/main.rs

pub type Res<T = ()> = anyhow::Result<T>;
pub type JsonRes<T> = Result<axum::Json<T>, AppError>;
pub type HtmlRes = Result<maud::Markup, AppError>;

pub struct AppError(anyhow::Error);
Expand Down

0 comments on commit 26f181d

Please sign in to comment.