OXYGENHUB
# 73beb84b202a oxygenhub / crates / hub / src / api / mod.rs
3.6 KiB · Rust · last changed Milestone 0.1: browse 1 month ago source blame history raw
d23464c0 Andy McCall ↶ 1
//! The JSON API (hub spec §12.1) under `/api/v1`. Milestone 0.1 exposes
2
//! repositories, bookmarks and tags; the rest follows the pages.
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
}