Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
radicle-desktop crates test-http-api src api.rs
use std::ops::Deref;
use std::path::PathBuf;
use std::sync::Arc;

use axum::extract::State;
use axum::response::{IntoResponse, Json};
use axum::routing::post;
use axum::Router;
use hyper::header::CONTENT_TYPE;
use hyper::Method;
use serde::{Deserialize, Serialize};
use tower_http::cors::{self, CorsLayer};

use radicle::{git, identity};
use radicle_types as types;
use radicle_types::cobs::issue;
use radicle_types::cobs::issue::NewIssue;
use radicle_types::cobs::CobOptions;
use radicle_types::cobs::{self, FromRadicleAction};
use radicle_types::config::Version;
use radicle_types::domain::patch::models;
use radicle_types::domain::patch::service::Service;
use radicle_types::domain::patch::traits::PatchService;
use radicle_types::error::Error;
use radicle_types::outbound::sqlite::Sqlite;
use radicle_types::traits::cobs::Cobs;
use radicle_types::traits::issue::{Issues, IssuesMut};
use radicle_types::traits::job::Jobs;
use radicle_types::traits::patch::{Patches, PatchesMut};
use radicle_types::traits::repo::{Repo, Show};
use radicle_types::traits::thread::Thread;
use radicle_types::traits::Profile;

#[derive(Clone)]
pub struct Context {
    profile: Arc<radicle::Profile>,
    patches: Arc<Service<Sqlite>>,
}

impl Repo for Context {}
impl Cobs for Context {}
impl Thread for Context {}
impl Issues for Context {}
impl IssuesMut for Context {}
impl Jobs for Context {}
impl Patches for Context {}
impl PatchesMut for Context {}
impl Profile for Context {
    fn profile(&self) -> radicle::Profile {
        self.profile.deref().clone()
    }
}

impl Context {
    pub fn new(profile: Arc<radicle::Profile>, patches: Arc<Service<Sqlite>>) -> Self {
        Self { profile, patches }
    }
}

pub fn router(ctx: Context) -> Router {
    Router::new()
        .route("/config", post(config_handler))
        .route("/authenticate", post(auth_handler))
        .route("/repo_count", post(repo_count_handler))
        .route("/list_repos", post(repo_root_handler))
        .route("/list_repos_summary", post(list_repos_summary_handler))
        .route(
            "/seeded_not_replicated",
            post(seeded_not_replicated_handler),
        )
        .route("/repo_by_id", post(repo_handler))
        .route("/version", post(version_handler))
        .route("/diff_stats", post(diff_stats_handler))
        .route(
            "/activity_by_issue",
            post(activity_issue_handler::<radicle::issue::Action, issue::Action>),
        )
        .route(
            "/activity_by_patch",
            post(activity_patch_handler::<radicle::patch::Action, models::patch::Action>),
        )
        .route("/repo_readme", post(readme_handler))
        .route("/repo_tree", post(tree_handler))
        .route("/repo_blob", post(blob_handler))
        .route("/get_diff", post(diff_handler))
        .route("/get_commit_diff", post(commit_diff_handler))
        .route("/list_repo_commits", post(list_repo_commits_handler))
        .route("/repo_commit_count", post(repo_commit_count_handler))
        .route("/repo_commit", post(repo_commit_handler))
        .route("/list_issues", post(issues_handler))
        .route("/create_issue", post(create_issue_handler))
        .route("/create_issue_comment", post(create_issue_comment_handler))
        .route("/edit_issue", post(edit_issue_handler))
        .route("/issue_by_id", post(issue_handler))
        .route("/comment_threads_by_issue_id", post(issue_threads_handler))
        .route("/list_patches", post(patches_handler))
        .route("/patch_by_id", post(patch_handler))
        .route("/revisions_by_patch", post(revision_handler))
        .route("/get_embed", post(get_embeds_handler))
        .route("/save_embed_by_path", post(save_embed_handler))
        .route("/save_embed_by_clipboard", post(save_embed_handler))
        .route("/save_embed_by_bytes", post(save_embed_handler))
        .route("/save_embed_to_disk", post(save_embed_handler))
        .route("/list_jobs", post(jobs_handler))
        .route("/list_notifications", post(list_notifications_handler))
        .route("/notification_count", post(notification_count_handler))
        .route("/clear_notifications", post(clear_notifications_handler))
        .layer(
            CorsLayer::new()
                .allow_origin(cors::Any)
                .allow_methods([Method::POST, Method::GET])
                .allow_headers([CONTENT_TYPE]),
        )
        .with_state(ctx)
}

async fn config_handler(State(ctx): State<Context>) -> impl IntoResponse {
    let config = ctx.config();

    Ok::<_, Error>(Json(config))
}

async fn auth_handler() -> impl IntoResponse {
    Ok::<_, Error>(Json(()))
}

#[derive(Serialize, Deserialize)]
pub struct Options {
    show: Show,
}

async fn repo_root_handler(
    State(ctx): State<Context>,
    Json(Options { show }): Json<Options>,
) -> impl IntoResponse {
    let repos = ctx.list_repos(show)?;

    Ok::<_, Error>(Json(repos))
}

async fn repo_count_handler(State(ctx): State<Context>) -> impl IntoResponse {
    let repos = ctx.repo_count()?;
    Ok::<_, Error>(Json(repos))
}

async fn list_repos_summary_handler(State(ctx): State<Context>) -> impl IntoResponse {
    let repos = ctx.list_repos_summary()?;
    Ok::<_, Error>(Json(repos))
}

async fn seeded_not_replicated_handler(State(ctx): State<Context>) -> impl IntoResponse {
    let rids = ctx.seeded_not_replicated()?;
    Ok::<_, Error>(Json(rids))
}

async fn list_notifications_handler() -> impl IntoResponse {
    Ok::<_, Error>(Json(Vec::<
        radicle_types::domain::inbox::models::notification::NotificationsByRepo,
    >::new()))
}

async fn notification_count_handler() -> impl IntoResponse {
    Ok::<_, Error>(Json(0_usize))
}

async fn clear_notifications_handler() -> impl IntoResponse {
    Ok::<_, Error>(Json(()))
}

#[derive(Serialize, Deserialize)]
struct RepoBody {
    pub rid: identity::RepoId,
}

async fn repo_handler(
    State(ctx): State<Context>,
    Json(RepoBody { rid }): Json<RepoBody>,
) -> impl IntoResponse {
    let info = ctx.repo_by_id(rid)?;

    Ok::<_, Error>(Json(info))
}

async fn version_handler() -> impl IntoResponse {
    let version = Version {
        version: String::from("0.6.1"),
        head: String::from("51cf6cfbfe0be992ee709c49e6da589aa0f148c5"),
    };

    Ok::<_, Error>(Json(version))
}

#[derive(Serialize, Deserialize)]
struct DiffStatsBody {
    pub rid: identity::RepoId,
    pub base: git::Oid,
    pub head: git::Oid,
}

async fn diff_stats_handler(
    State(ctx): State<Context>,
    Json(DiffStatsBody { rid, base, head }): Json<DiffStatsBody>,
) -> impl IntoResponse {
    let info = ctx.diff_stats(rid, base, head)?;

    Ok::<_, Error>(Json(info))
}

#[derive(Serialize, Deserialize)]
struct DiffBody {
    pub rid: identity::RepoId,
    pub options: types::cobs::diff::DiffOptions,
}

#[derive(Serialize, Deserialize)]
struct ReadmeBody {
    pub rid: identity::RepoId,
}

async fn readme_handler(
    State(ctx): State<Context>,
    Json(ReadmeBody { rid }): Json<ReadmeBody>,
) -> impl IntoResponse {
    let readme = ctx.repo_readme(rid, None)?;

    Ok::<_, Error>(Json(readme))
}

#[derive(Serialize, Deserialize)]
struct TreeBody {
    pub rid: identity::RepoId,
    pub path: PathBuf,
}

async fn tree_handler(
    State(ctx): State<Context>,
    Json(TreeBody { rid, path }): Json<TreeBody>,
) -> impl IntoResponse {
    let info = ctx.repo_tree(rid, path)?;

    Ok::<_, Error>(Json(info))
}

#[derive(Serialize, Deserialize)]
struct BlobBody {
    pub rid: identity::RepoId,
    pub path: PathBuf,
}

async fn blob_handler(
    State(ctx): State<Context>,
    Json(BlobBody { rid, path }): Json<BlobBody>,
) -> impl IntoResponse {
    let info = ctx.repo_blob(rid, path)?;

    Ok::<_, Error>(Json(info))
}

async fn diff_handler(
    State(ctx): State<Context>,
    Json(DiffBody { rid, options }): Json<DiffBody>,
) -> impl IntoResponse {
    let info = ctx.get_diff(rid, options)?;

    Ok::<_, Error>(Json(info))
}

#[derive(Serialize, Deserialize)]
struct CommitDiffBody {
    pub rid: identity::RepoId,
    pub sha: git::Oid,
    pub unified: Option<u32>,
    pub highlight: Option<bool>,
}

async fn commit_diff_handler(
    State(ctx): State<Context>,
    Json(CommitDiffBody {
        rid,
        sha,
        unified,
        highlight,
    }): Json<CommitDiffBody>,
) -> impl IntoResponse {
    let diff = ctx.get_commit_diff(rid, sha, unified, highlight)?;

    Ok::<_, Error>(Json(diff))
}

#[derive(Serialize, Deserialize)]
struct ListRepoCommitsBody {
    pub rid: identity::RepoId,
    pub head: Option<git::Oid>,
    pub skip: Option<usize>,
    pub take: Option<usize>,
}

async fn list_repo_commits_handler(
    State(ctx): State<Context>,
    Json(ListRepoCommitsBody {
        rid,
        head,
        skip,
        take,
    }): Json<ListRepoCommitsBody>,
) -> impl IntoResponse {
    let commits = ctx.list_repo_commits(rid, head, skip, take)?;

    Ok::<_, Error>(Json(commits))
}

#[derive(Serialize, Deserialize)]
struct RepoCommitCountBody {
    pub rid: identity::RepoId,
    pub head: git::Oid,
}

async fn repo_commit_count_handler(
    State(ctx): State<Context>,
    Json(RepoCommitCountBody { rid, head }): Json<RepoCommitCountBody>,
) -> impl IntoResponse {
    let count = ctx.repo_commit_count(rid, head)?;

    Ok::<_, Error>(Json(count))
}

#[derive(Serialize, Deserialize)]
struct RepoCommitBody {
    pub rid: identity::RepoId,
    pub sha: git::Oid,
}

async fn repo_commit_handler(
    State(ctx): State<Context>,
    Json(RepoCommitBody { rid, sha }): Json<RepoCommitBody>,
) -> impl IntoResponse {
    let commit = ctx.repo_commit(rid, sha)?;

    Ok::<_, Error>(Json(commit))
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct IssuesBody {
    pub rid: identity::RepoId,
    pub status: Option<types::cobs::query::IssueStatus>,
}

async fn issues_handler(
    State(ctx): State<Context>,
    Json(IssuesBody { rid, status }): Json<IssuesBody>,
) -> impl IntoResponse {
    let issues = ctx.list_issues(rid, status)?;

    Ok::<_, Error>(Json(issues))
}

#[derive(Serialize, Deserialize)]
struct CreateIssuesBody {
    pub rid: identity::RepoId,
    pub opts: CobOptions,
    pub new: NewIssue,
}

async fn create_issue_handler(
    State(ctx): State<Context>,
    Json(CreateIssuesBody { rid, opts, new }): Json<CreateIssuesBody>,
) -> impl IntoResponse {
    let issues = ctx.create_issue(rid, new, opts)?;

    Ok::<_, Error>(Json(issues))
}

#[derive(Serialize, Deserialize)]
struct CreateIssueCommentBody {
    pub rid: identity::RepoId,
    pub new: types::cobs::thread::NewIssueComment,
    pub opts: types::cobs::CobOptions,
}

async fn create_issue_comment_handler(
    State(ctx): State<Context>,
    Json(CreateIssueCommentBody { rid, opts, new }): Json<CreateIssueCommentBody>,
) -> impl IntoResponse {
    let comment = ctx.create_issue_comment(rid, new, opts)?;

    Ok::<_, Error>(Json(comment))
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct EditIssuesBody {
    pub rid: identity::RepoId,
    pub cob_id: git::Oid,
    pub action: issue::Action,
    pub opts: CobOptions,
}

async fn edit_issue_handler(
    State(ctx): State<Context>,
    Json(EditIssuesBody {
        rid,
        cob_id,
        action,
        opts,
    }): Json<EditIssuesBody>,
) -> impl IntoResponse {
    let issues = ctx.edit_issue(rid, cob_id, action, opts)?;

    Ok::<_, Error>(Json(issues))
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ActivityBody {
    pub rid: identity::RepoId,
    pub id: git::Oid,
}

async fn activity_issue_handler<
    A: serde::Serialize + serde::de::DeserializeOwned,
    B: FromRadicleAction<A> + serde::Serialize,
>(
    State(ctx): State<Context>,
    Json(ActivityBody { rid, id }): Json<ActivityBody>,
) -> impl IntoResponse {
    let activity = ctx.activity_by_id::<A, B>(rid, &radicle::cob::issue::TYPENAME, id)?;

    Ok::<_, Error>(Json(activity))
}

async fn activity_patch_handler<
    A: serde::Serialize + serde::de::DeserializeOwned,
    B: FromRadicleAction<A> + serde::Serialize,
>(
    State(ctx): State<Context>,
    Json(ActivityBody { rid, id }): Json<ActivityBody>,
) -> impl IntoResponse {
    let activity = ctx.activity_by_id::<A, B>(rid, &radicle::cob::patch::TYPENAME, id)?;

    Ok::<_, Error>(Json(activity))
}

#[derive(Serialize, Deserialize)]
struct EmbedBody {
    pub rid: identity::RepoId,
    pub name: Option<String>,
    pub oid: git::Oid,
}

async fn get_embeds_handler(
    State(ctx): State<Context>,
    Json(EmbedBody { rid, name, oid }): Json<EmbedBody>,
) -> impl IntoResponse {
    let embed = ctx.get_embed(rid, name, oid)?;

    Ok::<_, Error>(Json(embed))
}

#[derive(Serialize, Deserialize)]
struct CreateEmbedBody {
    pub rid: identity::RepoId,
    pub path: PathBuf,
}

async fn save_embed_handler(
    State(ctx): State<Context>,
    Json(CreateEmbedBody { rid, path }): Json<CreateEmbedBody>,
) -> impl IntoResponse {
    let embed = ctx.save_embed_by_path(rid, path)?;

    Ok::<_, Error>(Json(embed))
}

#[derive(Serialize, Deserialize)]
struct IssueBody {
    pub rid: identity::RepoId,
    pub id: git::Oid,
}

async fn issue_handler(
    State(ctx): State<Context>,
    Json(IssueBody { rid, id }): Json<IssueBody>,
) -> impl IntoResponse {
    let issue = ctx.issue_by_id(rid, id)?;

    Ok::<_, Error>(Json(issue))
}

async fn issue_threads_handler(
    State(ctx): State<Context>,
    Json(IssueBody { rid, id }): Json<IssueBody>,
) -> impl IntoResponse {
    let issue_threads = ctx.comment_threads_by_issue_id(rid, id)?;

    Ok::<_, Error>(Json(issue_threads))
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PatchesBody {
    pub rid: identity::RepoId,
    pub skip: Option<usize>,
    pub take: Option<isize>,
    pub status: Option<types::cobs::query::PatchStatus>,
}

async fn patches_handler(
    State(ctx): State<Context>,
    Json(PatchesBody {
        rid,
        skip,
        take,
        status,
    }): Json<PatchesBody>,
) -> impl IntoResponse {
    let profile = ctx.profile;
    let cursor = skip.unwrap_or(0);
    let aliases = profile.aliases();

    let patches = match status {
        None => ctx.patches.list(rid)?.collect::<Vec<_>>(),
        Some(s) => ctx
            .patches
            .list_by_status(rid, s.into())?
            .collect::<Vec<_>>(),
    };

    if let Some(t) = take {
        if t < 0 {
            // Return all patches
            let content = patches
                .into_iter()
                .map(|(id, patch)| models::patch::Patch::new(id, &patch, &aliases))
                .collect::<Vec<_>>();

            return Ok::<_, Error>(Json(cobs::PaginatedQuery {
                cursor: 0,
                more: false,
                content,
            }));
        }
    }

    let take = take.unwrap_or(20) as usize;

    let more = cursor + take < patches.len();

    let patches = patches
        .into_iter()
        .map(|(id, patch)| models::patch::Patch::new(id, &patch, &aliases))
        .skip(cursor)
        .take(take)
        .collect::<Vec<_>>();

    Ok::<_, Error>(Json(cobs::PaginatedQuery {
        cursor,
        more,
        content: patches,
    }))
}

#[derive(Serialize, Deserialize)]
struct PatchBody {
    pub rid: identity::RepoId,
    pub id: git::Oid,
}

async fn patch_handler(
    State(ctx): State<Context>,
    Json(PatchBody { rid, id }): Json<PatchBody>,
) -> impl IntoResponse {
    let patch = ctx.get_patch(rid, id)?;

    Ok::<_, Error>(Json(patch))
}

async fn revision_handler(
    State(ctx): State<Context>,
    Json(PatchBody { rid, id }): Json<PatchBody>,
) -> impl IntoResponse {
    let revisions = ctx.revisions_by_patch(rid, id)?;

    Ok::<_, Error>(Json(revisions))
}

#[derive(Serialize, Deserialize)]
struct JobsBody {
    pub rid: identity::RepoId,
    pub sha: git::Oid,
}

async fn jobs_handler(
    State(ctx): State<Context>,
    Json(JobsBody { rid, sha }): Json<JobsBody>,
) -> impl IntoResponse {
    let jobs = ctx.list_jobs(rid, sha)?;

    Ok::<_, Error>(Json(jobs))
}