//! The JSON API (hub spec ยง12.1) under `/api/v1`. Milestone 0.1 exposes //! repositories, bookmarks and tags; the rest follows the pages. use crate::repos::{view, RepoError, RepoName}; use crate::{db, Shared}; use axum::extract::{Path, State}; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use axum::routing::get; use axum::{Json, Router}; use hub_api::{Bookmark, Problem, Repository, Tag}; pub fn router() -> Router { Router::new() .route("/repos", get(list_repos)) .route("/repos/{owner}/{name}", get(get_repo)) .route("/repos/{owner}/{name}/bookmarks", get(bookmarks)) .route("/repos/{owner}/{name}/tags", get(tags)) } pub struct ApiError(StatusCode, String); impl IntoResponse for ApiError { fn into_response(self) -> Response { let body = Problem { status: self.0.as_u16(), title: self.0.canonical_reason().unwrap_or("error").into(), detail: self.1 }; (self.0, [("content-type", "application/problem+json")], Json(body)).into_response() } } impl From for ApiError { fn from(e: RepoError) -> ApiError { match e { RepoError::NotFound => ApiError(StatusCode::NOT_FOUND, "no such repository".into()), other => ApiError(StatusCode::INTERNAL_SERVER_ERROR, other.to_string()), } } } impl From for ApiError { fn from(e: sqlx::Error) -> ApiError { ApiError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()) } } fn repository(app: &Shared, r: &db::repos::RepoRow) -> Repository { Repository { owner: r.owner.clone(), name: r.name.clone(), description: r.description.clone(), default_bookmark: r.default_bookmark.clone(), visibility: r.visibility(), created_ms: r.created_ms, updated_ms: r.updated_ms, clone_url: format!("{}/{}/{}", app.config.base_url(), r.owner, r.name), } } async fn public_row(app: &Shared, owner: &str, name: &str) -> Result<(RepoName, db::repos::RepoRow), ApiError> { let repo = RepoName::new(owner, name).ok_or(ApiError(StatusCode::NOT_FOUND, "no such repository".into()))?; let row = db::repos::get(&app.db, owner, name).await?.filter(|r| r.visibility() == hub_api::Visibility::Public); Ok((repo, row.ok_or(ApiError(StatusCode::NOT_FOUND, "no such repository".into()))?)) } async fn list_repos(State(app): State) -> Result>, ApiError> { let rows = db::repos::list(&app.db).await?; Ok(Json(rows.iter().filter(|r| r.visibility() == hub_api::Visibility::Public).map(|r| repository(&app, r)).collect())) } async fn get_repo(State(app): State, Path((owner, name)): Path<(String, String)>) -> Result, ApiError> { let (_, row) = public_row(&app, &owner, &name).await?; Ok(Json(repository(&app, &row))) } async fn bookmarks(State(app): State, Path((owner, name)): Path<(String, String)>) -> Result>, ApiError> { let (repo, row) = public_row(&app, &owner, &name).await?; let default = row.default_bookmark.clone(); let list = app.repos.read(&repo, move |r| view::bookmarks(r, &default)).await?; Ok(Json(list.into_iter().map(|b| Bookmark { name: b.name, heads: b.heads, changes: b.changes, default: b.default }).collect())) } async fn tags(State(app): State, Path((owner, name)): Path<(String, String)>) -> Result>, ApiError> { let (repo, _) = public_row(&app, &owner, &name).await?; let list = app.repos.read(&repo, view::tags).await?; Ok(Json(list.into_iter().map(|t| Tag { name: t.name, heads: t.heads, message: t.message, signer: t.signer }).collect())) }