From 037ac391f85d0345fabb9e9866f5fff4aed26a75 Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Tue, 10 Sep 2024 17:10:28 +0200 Subject: [PATCH] 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. --- package.json | 1 + src-tauri/bindings/Comment.ts | 14 ++ src-tauri/bindings/Edit.ts | 9 ++ src-tauri/bindings/Issue.ts | 3 + src-tauri/bindings/Never.ts | 8 ++ src-tauri/bindings/Reaction.ts | 9 ++ src-tauri/bindings/Review.ts | 13 ++ src-tauri/bindings/Revision.ts | 18 +++ src-tauri/src/commands/auth.rs | 1 + src-tauri/src/commands/cobs.rs | 81 +++++++++++ src-tauri/src/lib.rs | 4 + src-tauri/src/types/cobs.rs | 254 ++++++++++++++++++++++++++++++++- 12 files changed, 410 insertions(+), 5 deletions(-) create mode 100644 src-tauri/bindings/Comment.ts create mode 100644 src-tauri/bindings/Edit.ts create mode 100644 src-tauri/bindings/Never.ts create mode 100644 src-tauri/bindings/Reaction.ts create mode 100644 src-tauri/bindings/Review.ts create mode 100644 src-tauri/bindings/Revision.ts diff --git a/package.json b/package.json index c09555f5f..4ed339fcc 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src-tauri/bindings/Comment.ts b/src-tauri/bindings/Comment.ts new file mode 100644 index 000000000..1a0692822 --- /dev/null +++ b/src-tauri/bindings/Comment.ts @@ -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 = { + id: string; + author: Author; + edits: Array; + reactions: string; + replyTo?: string; + location?: T; + resolved: boolean; +}; diff --git a/src-tauri/bindings/Edit.ts b/src-tauri/bindings/Edit.ts new file mode 100644 index 000000000..438c263d7 --- /dev/null +++ b/src-tauri/bindings/Edit.ts @@ -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 }; +}; diff --git a/src-tauri/bindings/Issue.ts b/src-tauri/bindings/Issue.ts index 5a95bf398..8c4381937 100644 --- a/src-tauri/bindings/Issue.ts +++ b/src-tauri/bindings/Issue.ts @@ -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; + discussion: Array>; labels: Array; timestamp: number; }; diff --git a/src-tauri/bindings/Never.ts b/src-tauri/bindings/Never.ts new file mode 100644 index 000000000..ab811bbec --- /dev/null +++ b/src-tauri/bindings/Never.ts @@ -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; diff --git a/src-tauri/bindings/Reaction.ts b/src-tauri/bindings/Reaction.ts new file mode 100644 index 000000000..eb698dddf --- /dev/null +++ b/src-tauri/bindings/Reaction.ts @@ -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; + location?: CodeLocation; +}; diff --git a/src-tauri/bindings/Review.ts b/src-tauri/bindings/Review.ts new file mode 100644 index 000000000..4d62c5f78 --- /dev/null +++ b/src-tauri/bindings/Review.ts @@ -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>; + timestamp: bigint; +}; diff --git a/src-tauri/bindings/Revision.ts b/src-tauri/bindings/Revision.ts new file mode 100644 index 000000000..eb348d5c8 --- /dev/null +++ b/src-tauri/bindings/Revision.ts @@ -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; + base: string; + oid: string; + reviews: Array; + timestamp: bigint; + discussion: Array>; + reactions: Array; +}; diff --git a/src-tauri/src/commands/auth.rs b/src-tauri/src/commands/auth.rs index 9c54bacbb..6e1f2a950 100644 --- a/src-tauri/src/commands/auth.rs +++ b/src-tauri/src/commands/auth.rs @@ -1,4 +1,5 @@ use anyhow::anyhow; + use radicle::crypto::ssh; use crate::{error::Error, AppState}; diff --git a/src-tauri/src/commands/cobs.rs b/src-tauri/src/commands/cobs.rs index eb017d630..14616368e 100644 --- a/src-tauri/src/commands/cobs.rs +++ b/src-tauri/src/commands/cobs.rs @@ -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, + rid: RepoId, + id: IssueId, +) -> Result, 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, @@ -58,6 +79,66 @@ pub fn list_patches( Ok::<_, Error>(patches) } +#[tauri::command] +pub fn patches_by_id( + ctx: tauri::State, + rid: RepoId, + id: String, +) -> Result, 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, + rid: RepoId, + id: String, +) -> Result>, 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::>() + }); + + Ok::<_, Error>(revisions) +} + +#[tauri::command] +pub fn revisions_by_id( + ctx: tauri::State, + rid: RepoId, + id: String, + revision_id: String, +) -> Result, 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}; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f70343eb7..f7dd10ad3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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!()) diff --git a/src-tauri/src/types/cobs.rs b/src-tauri/src/types/cobs.rs index 1f8c56f8d..90ad29920 100644 --- a/src-tauri/src/types/cobs.rs +++ b/src-tauri/src/types/cobs.rs @@ -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, + discussion: Vec, #[ts(as = "Vec")] labels: Vec, #[ts(type = "number")] @@ -55,6 +60,10 @@ impl Issue { .assignees() .map(|did| Author::new(*did, aliases)) .collect::>(), + discussion: issue + .comments() + .map(|(id, c)| Comment::::new(*id, c.clone(), aliases)) + .collect::>(), labels: issue.labels().cloned().collect::>(), 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, + #[ts(as = "String")] + base: git::Oid, + #[ts(as = "String")] + oid: git::Oid, + reviews: Vec, + #[ts(as = "i64")] + timestamp: cob::common::Timestamp, + discussion: Vec>, + reactions: Vec, +} + +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::>(), + base: *value.base(), + oid: value.head(), + reviews: value + .reviews() + .map(|(id, r)| Review::new(*id, r.clone(), aliases)) + .collect::>(), + timestamp: value.timestamp(), + discussion: value + .discussion() + .comments() + .map(|(id, c)| Comment::::new(*id, c.clone(), aliases)) + .collect::>(), + 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::>() + }) + .collect::>(), + } + } +} + +#[derive(Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct Reaction { + #[ts(as = "String")] + emoji: cob::Reaction, + authors: Vec, + #[ts(optional)] + location: Option, +} + +impl Reaction { + pub fn new( + emoji: cob::Reaction, + authors: Vec<&crypto::PublicKey>, + location: Option, + aliases: &impl AliasStore, + ) -> Self { + Self { + emoji, + authors: authors + .into_iter() + .map(|a| Author::new(a.into(), aliases)) + .collect::>(), + 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, + summary: Option, + comments: Vec>, + #[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::::new(*id, c.clone(), aliases)) + .collect::>(), + 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>, +} + +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 { + #[ts(as = "String")] + id: cob::thread::CommentId, + author: Author, + edits: Vec, + #[ts(as = "String")] + reactions: BTreeMap>, + #[ts(as = "Option")] + #[ts(optional)] + reply_to: Option, + #[ts(optional)] + location: Option, + resolved: bool, +} + +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::>(), + reactions: comment + .reactions() + .into_iter() + .map(|(r, a)| (*r, a.into_iter().copied().collect::>())) + .collect::>>(), + 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::>(), + reactions: comment + .reactions() + .into_iter() + .map(|(r, a)| (*r, a.into_iter().copied().collect::>())) + .collect::>>(), + 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 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 for cob::CodeLocation { fn from(val: CodeLocation) -> Self { Self {