Add issues_by_id, patches_by_id, revisions_by_patch, revisions_by_id

Also provides all the types related to it and a new npm script that
allows generating those types quickly.
This commit is contained in:
Sebastian Martinez
2024-09-24 06:40:50 +02:00
parent 9334ffb3c5
commit 037ac391f8
12 changed files with 410 additions and 5 deletions
+1
View File
@@ -12,6 +12,7 @@
"check-js": "scripts/check-js",
"check-rs": "scripts/check-rs",
"format": "npx prettier '**/*.@(ts|js|svelte|json|css|html|yml)' --write",
"generate-types": "cargo test --manifest-path ./src-tauri/Cargo.toml && npx prettier ./src-tauri/bindings --write",
"tauri": "npx tauri"
},
"engines": {
+14
View File
@@ -0,0 +1,14 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Author } from "./Author";
import type { Edit } from "./Edit";
import type { Never } from "./Never";
export type Comment<T = Never> = {
id: string;
author: Author;
edits: Array<Edit>;
reactions: string;
replyTo?: string;
location?: T;
resolved: boolean;
};
+9
View File
@@ -0,0 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Author } from "./Author";
export type Edit = {
author: Author;
timestamp: bigint;
body: string;
embeds: { name: string; content: string };
};
+3
View File
@@ -1,5 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Author } from "./Author";
import type { Comment } from "./Comment";
import type { Never } from "./Never";
export type Issue = {
id: string;
@@ -7,6 +9,7 @@ export type Issue = {
title: string;
state: { status: "closed"; reason: "other" | "solved" } | { status: "open" };
assignees: Array<Author>;
discussion: Array<Comment<Never>>;
labels: Array<string>;
timestamp: number;
};
+8
View File
@@ -0,0 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* The `Infallible` type does not have a `Serialize`/`Deserialize`
* implementation. The `Never` type imitates `Infallible` and
* provides the derived implementations.
*/
export type Never = never;
+9
View File
@@ -0,0 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Author } from "./Author";
import type { CodeLocation } from "./CodeLocation";
export type Reaction = {
emoji: string;
authors: Array<Author>;
location?: CodeLocation;
};
+13
View File
@@ -0,0 +1,13 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Author } from "./Author";
import type { CodeLocation } from "./CodeLocation";
import type { Comment } from "./Comment";
export type Review = {
id: string;
author: Author;
verdict?: "accept" | "reject";
summary: string | null;
comments: Array<Comment<CodeLocation>>;
timestamp: bigint;
};
+18
View File
@@ -0,0 +1,18 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Author } from "./Author";
import type { CodeLocation } from "./CodeLocation";
import type { Comment } from "./Comment";
import type { Edit } from "./Edit";
import type { Reaction } from "./Reaction";
import type { Review } from "./Review";
export type Revision = {
author: Author;
description: Array<Edit>;
base: string;
oid: string;
reviews: Array<Review>;
timestamp: bigint;
discussion: Array<Comment<CodeLocation>>;
reactions: Array<Reaction>;
};
+1
View File
@@ -1,4 +1,5 @@
use anyhow::anyhow;
use radicle::crypto::ssh;
use crate::{error::Error, AppState};
+81
View File
@@ -1,5 +1,10 @@
use std::str::FromStr;
use radicle::cob::ObjectId;
use radicle::git::Oid;
use radicle::identity::RepoId;
use radicle::issue::cache::Issues;
use radicle::issue::IssueId;
use radicle::patch::cache::Patches;
use crate::error::Error;
@@ -32,6 +37,22 @@ pub fn list_issues(
Ok::<_, Error>(issues)
}
#[tauri::command]
pub fn issues_by_id(
ctx: tauri::State<AppState>,
rid: RepoId,
id: IssueId,
) -> Result<Option<cobs::Issue>, Error> {
let (repo, _) = ctx.repo(rid)?;
let issues = ctx.profile.issues(&repo)?;
let issue = issues.get(&id)?;
let aliases = &ctx.profile.aliases();
let issue = issue.map(|issue| cobs::Issue::new(id, issue, aliases));
Ok::<_, Error>(issue)
}
#[tauri::command]
pub fn list_patches(
ctx: tauri::State<AppState>,
@@ -58,6 +79,66 @@ pub fn list_patches(
Ok::<_, Error>(patches)
}
#[tauri::command]
pub fn patches_by_id(
ctx: tauri::State<AppState>,
rid: RepoId,
id: String,
) -> Result<Option<cobs::Patch>, Error> {
let id = ObjectId::from_str(&id)?;
let (repo, _) = ctx.repo(rid)?;
let patches = ctx.profile.patches(&repo)?;
let patch = patches.get(&id)?;
let aliases = &ctx.profile.aliases();
let patches = patch.map(|patch| cobs::Patch::new(id, patch, aliases));
Ok::<_, Error>(patches)
}
#[tauri::command]
pub fn revisions_by_patch(
ctx: tauri::State<AppState>,
rid: RepoId,
id: String,
) -> Result<Option<Vec<cobs::Revision>>, Error> {
let id = ObjectId::from_str(&id)?;
let (repo, _) = ctx.repo(rid)?;
let patches = ctx.profile.patches(&repo)?;
let revisions = patches.get(&id)?.map(|patch| {
let aliases = &ctx.profile.aliases();
patch
.revisions()
.map(|(_, r)| cobs::Revision::new(r.clone(), aliases))
.collect::<Vec<_>>()
});
Ok::<_, Error>(revisions)
}
#[tauri::command]
pub fn revisions_by_id(
ctx: tauri::State<AppState>,
rid: RepoId,
id: String,
revision_id: String,
) -> Result<Option<cobs::Revision>, Error> {
let id = ObjectId::from_str(&id)?;
let (repo, _) = ctx.repo(rid)?;
let patches = ctx.profile.patches(&repo)?;
let revision = patches.get(&id)?.and_then(|patch| {
let revision_id = Oid::from_str(&revision_id).ok()?;
let aliases = &ctx.profile.aliases();
patch
.revision(&revision_id.into())
.map(|r| cobs::Revision::new(r.clone(), aliases))
});
Ok::<_, Error>(revision)
}
mod query {
use serde::{Deserialize, Serialize};
+4
View File
@@ -141,9 +141,13 @@ pub fn run() {
repos::list_repos,
repos::repo_by_id,
cobs::list_issues,
cobs::issues_by_id,
cobs::list_patches,
thread::create_issue_comment,
thread::create_patch_comment,
cobs::patches_by_id,
cobs::revisions_by_patch,
cobs::revisions_by_id,
profile::config,
])
.run(tauri::generate_context!())
+249 -5
View File
@@ -1,15 +1,19 @@
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use radicle::cob;
use radicle::git;
use radicle::crypto;
use radicle::identity;
use radicle::issue;
use radicle::node::{Alias, AliasStore};
use radicle::patch;
use serde::Deserialize;
use serde::Serialize;
use ts_rs::TS;
use radicle::storage::git;
#[derive(Serialize, TS)]
pub(crate) struct Author {
#[serde(rename_all = "camelCase")]
pub struct Author {
#[ts(as = "String")]
did: identity::Did,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -38,6 +42,7 @@ pub struct Issue {
#[ts(type = "{ status: 'closed', reason: 'other' | 'solved' } | { status: 'open' } ")]
state: issue::State,
assignees: Vec<Author>,
discussion: Vec<Comment>,
#[ts(as = "Vec<String>")]
labels: Vec<cob::Label>,
#[ts(type = "number")]
@@ -55,6 +60,10 @@ impl Issue {
.assignees()
.map(|did| Author::new(*did, aliases))
.collect::<Vec<_>>(),
discussion: issue
.comments()
.map(|(id, c)| Comment::<Never>::new(*id, c.clone(), aliases))
.collect::<Vec<_>>(),
labels: issue.labels().cloned().collect::<Vec<_>>(),
timestamp: issue.timestamp(),
}
@@ -106,6 +115,230 @@ impl Patch {
}
}
#[derive(Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct Revision {
author: Author,
description: Vec<Edit>,
#[ts(as = "String")]
base: git::Oid,
#[ts(as = "String")]
oid: git::Oid,
reviews: Vec<Review>,
#[ts(as = "i64")]
timestamp: cob::common::Timestamp,
discussion: Vec<Comment<CodeLocation>>,
reactions: Vec<Reaction>,
}
impl Revision {
pub fn new(value: cob::patch::Revision, aliases: &impl AliasStore) -> Self {
Self {
author: Author::new(*value.author().id(), aliases),
description: value
.edits()
.map(|e| Edit::new(e, aliases))
.collect::<Vec<_>>(),
base: *value.base(),
oid: value.head(),
reviews: value
.reviews()
.map(|(id, r)| Review::new(*id, r.clone(), aliases))
.collect::<Vec<_>>(),
timestamp: value.timestamp(),
discussion: value
.discussion()
.comments()
.map(|(id, c)| Comment::<CodeLocation>::new(*id, c.clone(), aliases))
.collect::<Vec<_>>(),
reactions: value
.reactions()
.iter()
.flat_map(|(location, reactions)| {
let reaction_by_author = reactions.iter().fold(
BTreeMap::new(),
|mut acc: BTreeMap<&cob::Reaction, Vec<_>>, (author, emoji)| {
acc.entry(emoji).or_default().push(author);
acc
},
);
reaction_by_author
.into_iter()
.map(|(emoji, authors)| {
Reaction::new(
*emoji,
authors,
location.as_ref().map(|l| CodeLocation::new(l.clone())),
aliases,
)
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>(),
}
}
}
#[derive(Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct Reaction {
#[ts(as = "String")]
emoji: cob::Reaction,
authors: Vec<Author>,
#[ts(optional)]
location: Option<CodeLocation>,
}
impl Reaction {
pub fn new(
emoji: cob::Reaction,
authors: Vec<&crypto::PublicKey>,
location: Option<CodeLocation>,
aliases: &impl AliasStore,
) -> Self {
Self {
emoji,
authors: authors
.into_iter()
.map(|a| Author::new(a.into(), aliases))
.collect::<Vec<_>>(),
location,
}
}
}
#[derive(Serialize, TS)]
#[serde(rename_all = "camelCase")]
pub struct Review {
#[ts(as = "String")]
id: identity::PublicKey,
author: Author,
#[ts(type = "'accept' | 'reject'")]
#[ts(optional)]
verdict: Option<cob::patch::Verdict>,
summary: Option<String>,
comments: Vec<Comment<CodeLocation>>,
#[ts(as = "i64")]
timestamp: cob::common::Timestamp,
}
impl Review {
pub fn new(
id: identity::PublicKey,
review: cob::patch::Review,
aliases: &impl AliasStore,
) -> Self {
Self {
id,
author: Author::new(review.author().id, aliases),
verdict: review.verdict(),
summary: review.summary().map(|s| s.to_string()),
comments: review
.comments()
.map(|(id, c)| Comment::<CodeLocation>::new(*id, c.clone(), aliases))
.collect::<Vec<_>>(),
timestamp: review.timestamp(),
}
}
}
#[derive(TS, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Edit {
pub author: Author,
#[ts(as = "i64")]
pub timestamp: cob::common::Timestamp,
pub body: String,
#[ts(type = "{ name: string, content: string }")]
pub embeds: Vec<cob::change::store::Embed<cob::common::Uri>>,
}
impl Edit {
pub fn new(edit: &cob::thread::Edit, aliases: &impl AliasStore) -> Self {
Self {
author: Author::new(edit.author.into(), aliases),
timestamp: edit.timestamp,
body: edit.body.clone(),
embeds: edit.embeds.clone(),
}
}
}
/// The `Infallible` type does not have a `Serialize`/`Deserialize`
/// implementation. The `Never` type imitates `Infallible` and
/// provides the derived implementations.
#[derive(TS, Serialize)]
pub enum Never {}
#[derive(Serialize, TS)]
#[serde(rename_all = "camelCase")]
pub struct Comment<T = Never> {
#[ts(as = "String")]
id: cob::thread::CommentId,
author: Author,
edits: Vec<Edit>,
#[ts(as = "String")]
reactions: BTreeMap<cob::common::Reaction, Vec<cob::op::ActorId>>,
#[ts(as = "Option<String>")]
#[ts(optional)]
reply_to: Option<cob::thread::CommentId>,
#[ts(optional)]
location: Option<T>,
resolved: bool,
}
impl Comment<CodeLocation> {
pub fn new(
id: cob::thread::CommentId,
comment: cob::thread::Comment<cob::common::CodeLocation>,
aliases: &impl AliasStore,
) -> Self {
Self {
id,
author: Author::new(comment.author().into(), aliases),
edits: comment
.edits()
.map(|e| Edit::new(e, aliases))
.collect::<Vec<_>>(),
reactions: comment
.reactions()
.into_iter()
.map(|(r, a)| (*r, a.into_iter().copied().collect::<Vec<_>>()))
.collect::<BTreeMap<cob::common::Reaction, Vec<cob::op::ActorId>>>(),
reply_to: comment.reply_to(),
location: comment.location().map(|l| CodeLocation::new(l.clone())),
resolved: comment.is_resolved(),
}
}
}
impl Comment {
pub fn new(
id: cob::thread::CommentId,
comment: cob::thread::Comment,
aliases: &impl AliasStore,
) -> Self {
Self {
id,
author: Author::new(comment.author().into(), aliases),
edits: comment
.edits()
.map(|e| Edit::new(e, aliases))
.collect::<Vec<_>>(),
reactions: comment
.reactions()
.into_iter()
.map(|(r, a)| (*r, a.into_iter().copied().collect::<Vec<_>>()))
.collect::<BTreeMap<cob::common::Reaction, Vec<cob::op::ActorId>>>(),
reply_to: comment.reply_to(),
location: None,
resolved: comment.is_resolved(),
}
}
}
#[derive(TS, Serialize, Deserialize)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
@@ -156,6 +389,17 @@ impl From<cob::CodeLocation> for CodeLocation {
}
}
impl CodeLocation {
pub fn new(location: cob::common::CodeLocation) -> Self {
Self {
commit: location.commit,
path: location.path,
old: location.old.map(|l| l.into()),
new: location.new.map(|l| l.into()),
}
}
}
impl From<CodeLocation> for cob::CodeLocation {
fn from(val: CodeLocation) -> Self {
Self {