| d23464c0 Andy McCall ↶ |
1 |
|
| 2 |
|
| 3 |
|
| 4 |
use crate::repos::{view, RepoError, RepoName}; |
| 5 |
use crate::{db, Shared}; |
| 6 |
use axum::extract::{Path, State}; |
| 7 |
use axum::http::StatusCode; |
| 8 |
use axum::response::{IntoResponse, Response}; |
| 9 |
use axum::routing::get; |
| 10 |
use axum::{Json, Router}; |
| 11 |
use hub_api::{Bookmark, Problem, Repository, Tag}; |
| 12 |
|
| 13 |
pub fn router() -> Router<Shared> { |
| 14 |
Router::new() |
| 15 |
.route("/repos", get(list_repos)) |
| 16 |
.route("/repos/{owner}/{name}", get(get_repo)) |
| 17 |
.route("/repos/{owner}/{name}/bookmarks", get(bookmarks)) |
| 18 |
.route("/repos/{owner}/{name}/tags", get(tags)) |
| 19 |
} |
| 20 |
|
| 21 |
pub struct ApiError(StatusCode, String); |
| 22 |
|
| 23 |
impl IntoResponse for ApiError { |
| 24 |
fn into_response(self) -> Response { |
| 25 |
let body = Problem { status: self.0.as_u16(), title: self.0.canonical_reason().unwrap_or("error").into(), detail: self.1 }; |
| 26 |
(self.0, [("content-type", "application/problem+json")], Json(body)).into_response() |
| 27 |
} |
| 28 |
} |
| 29 |
|
| 30 |
impl From<RepoError> for ApiError { |
| 31 |
fn from(e: RepoError) -> ApiError { |
| 32 |
match e { |
| 33 |
RepoError::NotFound => ApiError(StatusCode::NOT_FOUND, "no such repository".into()), |
| 34 |
other => ApiError(StatusCode::INTERNAL_SERVER_ERROR, other.to_string()), |
| 35 |
} |
| 36 |
} |
| 37 |
} |
| 38 |
|
| 39 |
impl From<sqlx::Error> for ApiError { |
| 40 |
fn from(e: sqlx::Error) -> ApiError { |
| 41 |
ApiError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()) |
| 42 |
} |
| 43 |
} |
| 44 |
|
| 45 |
fn repository(app: &Shared, r: &db::repos::RepoRow) -> Repository { |
| 46 |
Repository { |
| 47 |
owner: r.owner.clone(), |
| 48 |
name: r.name.clone(), |
| 49 |
description: r.description.clone(), |
| 50 |
default_bookmark: r.default_bookmark.clone(), |
| 51 |
visibility: r.visibility(), |
| 52 |
created_ms: r.created_ms, |
| 53 |
updated_ms: r.updated_ms, |
| 54 |
clone_url: format!("{}/{}/{}", app.config.base_url(), r.owner, r.name), |
| 55 |
} |
| 56 |
} |
| 57 |
|
| 58 |
async fn public_row(app: &Shared, owner: &str, name: &str) -> Result<(RepoName, db::repos::RepoRow), ApiError> { |
| 59 |
let repo = RepoName::new(owner, name).ok_or(ApiError(StatusCode::NOT_FOUND, "no such repository".into()))?; |
| 60 |
let row = db::repos::get(&app.db, owner, name).await?.filter(|r| r.visibility() == hub_api::Visibility::Public); |
| 61 |
Ok((repo, row.ok_or(ApiError(StatusCode::NOT_FOUND, "no such repository".into()))?)) |
| 62 |
} |
| 63 |
|
| 64 |
async fn list_repos(State(app): State<Shared>) -> Result<Json<Vec<Repository>>, ApiError> { |
| 65 |
let rows = db::repos::list(&app.db).await?; |
| 66 |
Ok(Json(rows.iter().filter(|r| r.visibility() == hub_api::Visibility::Public).map(|r| repository(&app, r)).collect())) |
| 67 |
} |
| 68 |
|
| 69 |
async fn get_repo(State(app): State<Shared>, Path((owner, name)): Path<(String, String)>) -> Result<Json<Repository>, ApiError> { |
| 70 |
let (_, row) = public_row(&app, &owner, &name).await?; |
| 71 |
Ok(Json(repository(&app, &row))) |
| 72 |
} |
| 73 |
|
| 74 |
async fn bookmarks(State(app): State<Shared>, Path((owner, name)): Path<(String, String)>) -> Result<Json<Vec<Bookmark>>, ApiError> { |
| 75 |
let (repo, row) = public_row(&app, &owner, &name).await?; |
| 76 |
let default = row.default_bookmark.clone(); |
| 77 |
let list = app.repos.read(&repo, move |r| view::bookmarks(r, &default)).await?; |
| 78 |
Ok(Json(list.into_iter().map(|b| Bookmark { name: b.name, heads: b.heads, changes: b.changes, default: b.default }).collect())) |
| 79 |
} |
| 80 |
|
| 81 |
async fn tags(State(app): State<Shared>, Path((owner, name)): Path<(String, String)>) -> Result<Json<Vec<Tag>>, ApiError> { |
| 82 |
let (repo, _) = public_row(&app, &owner, &name).await?; |
| 83 |
let list = app.repos.read(&repo, view::tags).await?; |
| 84 |
Ok(Json(list.into_iter().map(|t| Tag { name: t.name, heads: t.heads, message: t.message, signer: t.signer }).collect())) |
| 85 |
} |