Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Extract rust types into its own crate
Merged did:key:z6MkkfM3...sVz5 opened 1 year ago
21 files changed +758 -732 caebd0a3 0bdf0e71
modified Cargo.lock
@@ -3996,6 +3996,7 @@ dependencies = [
 "log",
 "radicle",
 "radicle-surf",
+
 "radicle-types",
 "serde",
 "serde_json",
 "tauri",
@@ -4009,6 +4010,17 @@ dependencies = [
]

[[package]]
+
name = "radicle-types"
+
version = "0.1.0"
+
dependencies = [
+
 "radicle",
+
 "radicle-surf",
+
 "serde",
+
 "serde_json",
+
 "ts-rs",
+
]
+

+
[[package]]
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
modified Cargo.toml
@@ -1,6 +1,7 @@
[workspace]
resolver = "1"

-
members = [
+
members = [ 
    "crates/radicle-tauri",
-
]

\ No newline at end of file
+
    "crates/radicle-types"
+
]
modified crates/radicle-tauri/Cargo.toml
@@ -20,6 +20,7 @@ base64 = { version = "0.22.1" }
log = { version = "0.4.22" }
localtime = { version = "1.3.1" }
radicle = { git = "https://seed.radicle.xyz/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git" }
+
radicle-types = { version = "0.1.0", path = "../radicle-types" }
radicle-surf = { version = "0.22.1", features = ["serde"] }
serde = { version = "1.0.210", features = ["derive"] }
serde_json = { version = "1.0.132" }
modified crates/radicle-tauri/src/commands/cob.rs
@@ -4,9 +4,10 @@ use radicle::cob;
use radicle::git;
use radicle::identity;
use radicle::storage::{ReadRepository, ReadStorage};
+
use radicle_types as types;
+
use radicle_types::cobs::IssueAction;

-
use crate::types::cobs::IssueAction;
-
use crate::{error, types, AppState};
+
use crate::{error, AppState};

pub mod draft;
pub mod issue;
modified crates/radicle-tauri/src/commands/cob/issue.rs
@@ -4,10 +4,10 @@ use radicle::issue::cache::Issues;
use radicle::node::Handle;
use radicle::node::Node;
use radicle::storage::ReadStorage;
+
use radicle_types::cobs;

use crate::cob::query;
use crate::error::Error;
-
use crate::types::cobs;
use crate::AppState;

#[tauri::command]
modified crates/radicle-tauri/src/commands/cob/patch.rs
@@ -1,3 +1,6 @@
+
use serde::{Deserialize, Serialize};
+
use ts_rs::TS;
+

use radicle::cob;
use radicle::git;
use radicle::identity;
@@ -5,16 +8,13 @@ use radicle::patch;
use radicle::patch::cache::Patches;
use radicle::storage;
use radicle::storage::ReadStorage;
-
use serde::{Deserialize, Serialize};
-
use ts_rs::TS;
+
use radicle_types::cobs;
+
use radicle_types::thread;

+
use crate::cob::query;
use crate::error::Error;
-
use crate::types::cobs;
-
use crate::types::thread;
use crate::AppState;

-
use crate::cob::query;
-

#[derive(Serialize, Deserialize, TS)]
#[ts(export)]
pub struct PaginatedQuery<T> {
modified crates/radicle-tauri/src/commands/profile.rs
@@ -1,5 +1,6 @@
+
use radicle_types::config::Config;
+

use crate::error::Error;
-
use crate::types::config::Config;
use crate::AppState;

/// Get active config.
modified crates/radicle-tauri/src/commands/repo.rs
@@ -1,18 +1,18 @@
-
use radicle::crypto::Verified;
-
use radicle::prelude::Doc;
-
use radicle::storage::git::Repository;
use serde_json::json;

+
use radicle::crypto::Verified;
use radicle::identity::doc::PayloadId;
use radicle::identity::{DocAt, RepoId};
use radicle::issue::cache::Issues;
use radicle::node::routing::Store;
use radicle::patch::cache::Patches;
+
use radicle::prelude::Doc;
+
use radicle::storage::git::Repository;
use radicle::storage::ReadStorage;
use radicle::storage::{self, ReadRepository};
+
use radicle_types as types;

use crate::error::Error;
-
use crate::types;
use crate::AppState;

/// List all repos.
modified crates/radicle-tauri/src/commands/thread.rs
@@ -5,10 +5,10 @@ use radicle::identity;
use radicle::node::Handle;
use radicle::storage::ReadStorage;
use radicle::Node;
+
use radicle_types::cobs;
+
use radicle_types::thread;

use crate::error::Error;
-
use crate::types::cobs;
-
use crate::types::thread;
use crate::AppState;

#[tauri::command]
modified crates/radicle-tauri/src/lib.rs
@@ -1,6 +1,5 @@
mod commands;
mod error;
-
mod types;

use tauri::Emitter;
use tauri::Manager;
deleted crates/radicle-tauri/src/types.rs
@@ -1,4 +0,0 @@
-
pub mod cobs;
-
pub mod config;
-
pub mod repo;
-
pub mod thread;
deleted crates/radicle-tauri/src/types/cobs.rs
@@ -1,434 +0,0 @@
-
use std::collections::BTreeMap;
-
use std::collections::BTreeSet;
-

-
use serde::{Deserialize, Serialize};
-
use ts_rs::TS;
-

-
use radicle::cob;
-
use radicle::crypto;
-
use radicle::identity;
-
use radicle::issue;
-
use radicle::node::{Alias, AliasStore};
-
use radicle::patch;
-
use radicle::storage::git;
-

-
use crate::types::cobs;
-
use crate::types::thread;
-

-
#[derive(Serialize, TS)]
-
#[serde(rename_all = "camelCase")]
-
pub struct Author {
-
    #[ts(as = "String")]
-
    did: identity::Did,
-
    #[serde(skip_serializing_if = "Option::is_none")]
-
    #[ts(as = "Option<String>")]
-
    #[ts(optional)]
-
    alias: Option<Alias>,
-
}
-

-
impl Author {
-
    pub fn new(did: identity::Did, aliases: &impl AliasStore) -> Self {
-
        Self {
-
            did,
-
            alias: aliases.alias(&did),
-
        }
-
    }
-
}
-

-
#[derive(TS, Serialize)]
-
#[ts(export)]
-
#[serde(rename_all = "camelCase")]
-
pub struct Issue {
-
    #[ts(as = "String")]
-
    id: String,
-
    author: Author,
-
    title: String,
-
    #[ts(type = "{ status: 'closed', reason: 'other' | 'solved' } | { status: 'open' } ")]
-
    state: issue::State,
-
    assignees: Vec<Author>,
-
    discussion: Vec<thread::Comment<cobs::Never>>,
-
    #[ts(as = "Vec<String>")]
-
    labels: Vec<cob::Label>,
-
    #[ts(type = "number")]
-
    timestamp: cob::Timestamp,
-
}
-

-
impl Issue {
-
    pub fn new(id: &issue::IssueId, issue: &issue::Issue, aliases: &impl AliasStore) -> Self {
-
        Self {
-
            id: id.to_string(),
-
            author: Author::new(*issue.author().id(), aliases),
-
            title: issue.title().to_string(),
-
            state: *issue.state(),
-
            assignees: issue
-
                .assignees()
-
                .map(|did| Author::new(*did, aliases))
-
                .collect::<Vec<_>>(),
-
            discussion: issue
-
                .comments()
-
                .map(|(id, c)| thread::Comment::<Never>::new(*id, c.clone(), aliases))
-
                .collect::<Vec<_>>(),
-
            labels: issue.labels().cloned().collect::<Vec<_>>(),
-
            timestamp: issue.timestamp(),
-
        }
-
    }
-
}
-

-
#[derive(TS, Serialize)]
-
#[ts(export)]
-
#[serde(rename_all = "camelCase")]
-
pub struct Patch {
-
    #[ts(as = "String")]
-
    id: String,
-
    author: Author,
-
    title: String,
-
    #[ts(as = "String")]
-
    base: git::Oid,
-
    #[ts(as = "String")]
-
    head: git::Oid,
-
    #[ts(type = r#"{
-
  status: 'draft'
-
} | {
-
  status: 'open',
-
  conflicts: [string, string][]
-
} | {
-
  status: 'archived'
-
} | {
-
  status: 'merged', revision: string, commit: string
-
} "#)]
-
    state: patch::State,
-
    assignees: Vec<Author>,
-
    #[ts(as = "Vec<String>")]
-
    labels: Vec<cob::Label>,
-
    #[ts(type = "number")]
-
    timestamp: cob::Timestamp,
-
    revision_count: usize,
-
}
-

-
impl Patch {
-
    pub fn new(id: patch::PatchId, patch: patch::Patch, aliases: &impl AliasStore) -> Self {
-
        Self {
-
            id: id.to_string(),
-
            author: Author::new(*patch.author().id(), aliases),
-
            title: patch.title().to_string(),
-
            state: patch.state().clone(),
-
            base: *patch.base(),
-
            head: *patch.head(),
-
            assignees: patch
-
                .assignees()
-
                .map(|did| Author::new(did, aliases))
-
                .collect::<Vec<_>>(),
-
            labels: patch.labels().cloned().collect::<Vec<_>>(),
-
            timestamp: patch.timestamp(),
-
            revision_count: patch.revisions().count(),
-
        }
-
    }
-

-
    pub fn timestamp(&self) -> u64 {
-
        self.timestamp.as_millis()
-
    }
-
}
-

-
#[derive(Serialize, Deserialize, TS)]
-
#[serde(rename_all = "camelCase")]
-
#[ts(export)]
-
pub struct ReviewEdit {
-
    #[ts(as = "String")]
-
    pub review_id: cob::patch::ReviewId,
-
    #[ts(as = "Option<String>")]
-
    pub verdict: Option<cob::patch::Verdict>,
-
    pub summary: Option<String>,
-
    #[ts(as = "Vec<String>")]
-
    pub labels: Vec<cob::Label>,
-
}
-

-
#[derive(Serialize, TS)]
-
#[serde(rename_all = "camelCase")]
-
#[ts(export)]
-
pub struct Revision {
-
    #[ts(as = "String")]
-
    id: patch::RevisionId,
-
    author: Author,
-
    description: Vec<Edit>,
-
    #[ts(as = "String")]
-
    base: git::Oid,
-
    #[ts(as = "String")]
-
    head: git::Oid,
-
    reviews: Vec<Review>,
-
    #[ts(type = "number")]
-
    timestamp: cob::common::Timestamp,
-
    discussion: Vec<thread::Comment<thread::CodeLocation>>,
-
    reactions: Vec<Reaction>,
-
}
-

-
impl Revision {
-
    pub fn new(value: cob::patch::Revision, aliases: &impl AliasStore) -> Self {
-
        Self {
-
            id: value.id(),
-
            author: Author::new(*value.author().id(), aliases),
-
            description: value
-
                .edits()
-
                .map(|e| Edit::new(e, aliases))
-
                .collect::<Vec<_>>(),
-
            base: *value.base(),
-
            head: 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)| {
-
                    thread::Comment::<thread::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| thread::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<thread::CodeLocation>,
-
}
-

-
impl Reaction {
-
    pub fn new(
-
        emoji: cob::Reaction,
-
        authors: Vec<&crypto::PublicKey>,
-
        location: Option<thread::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<thread::Comment<thread::CodeLocation>>,
-
    #[ts(type = "number")]
-
    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)| {
-
                    thread::Comment::<thread::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(type = "number")]
-
    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(TS, Serialize, Deserialize)]
-
#[ts(export)]
-
#[serde(rename_all = "camelCase")]
-
pub struct NewIssue {
-
    pub title: String,
-
    pub description: String,
-
    #[ts(as = "Vec<String>")]
-
    pub labels: Vec<cob::Label>,
-
    #[ts(as = "Vec<String>")]
-
    pub assignees: Vec<identity::Did>,
-
    #[ts(type = "{ name: string, content: string }[]")]
-
    pub embeds: Vec<cob::Embed<cob::Uri>>,
-
}
-

-
#[derive(TS, Serialize, Deserialize)]
-
#[serde(rename_all = "camelCase")]
-
#[ts(export)]
-
pub struct CobOptions {
-
    #[ts(as = "Option<bool>")]
-
    #[ts(optional)]
-
    announce: Option<bool>,
-
}
-

-
impl CobOptions {
-
    pub fn announce(&self) -> bool {
-
        self.announce.unwrap_or(true)
-
    }
-
}
-

-
#[derive(TS, Serialize)]
-
#[ts(export)]
-
pub struct Stats {
-
    pub files_changed: usize,
-
    pub insertions: usize,
-
    pub deletions: usize,
-
}
-

-
impl Stats {
-
    pub fn new(stats: &radicle_surf::diff::Stats) -> Self {
-
        Self {
-
            files_changed: stats.files_changed,
-
            insertions: stats.insertions,
-
            deletions: stats.deletions,
-
        }
-
    }
-
}
-

-
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, TS)]
-
#[serde(tag = "type", rename_all = "camelCase")]
-
#[ts(export)]
-
pub enum IssueAction {
-
    /// Assign issue to an actor.
-
    #[serde(rename = "assign")]
-
    Assign {
-
        #[ts(as = "Vec<String>")]
-
        assignees: BTreeSet<identity::Did>,
-
    },
-

-
    /// Edit issue title.
-
    #[serde(rename = "edit")]
-
    Edit { title: String },
-

-
    /// Transition to a different state.
-
    #[serde(rename = "lifecycle")]
-
    Lifecycle {
-
        #[ts(type = "{ status: 'closed', reason: 'other' | 'solved' } | { status: 'open' } ")]
-
        state: issue::State,
-
    },
-

-
    /// Modify issue labels.
-
    #[serde(rename = "label")]
-
    Label {
-
        #[ts(as = "Vec<String>")]
-
        labels: BTreeSet<cob::Label>,
-
    },
-

-
    /// Comment on a thread.
-
    #[serde(rename_all = "camelCase")]
-
    #[serde(rename = "comment")]
-
    Comment {
-
        /// Comment body.
-
        body: String,
-
        /// Comment this is a reply to.
-
        /// Should be [`None` | `null`] if it's the top-level comment.
-
        /// Should be the root [`CommentId`] if it's a top-level comment.
-
        #[serde(default, skip_serializing_if = "Option::is_none")]
-
        #[ts(as = "Option<String>")]
-
        reply_to: Option<cob::thread::CommentId>,
-
        /// Embeded content.
-
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
-
        #[ts(as = "Vec<String>")]
-
        embeds: Vec<cob::Embed<cob::Uri>>,
-
    },
-

-
    /// Edit a comment.
-
    #[serde(rename = "comment.edit")]
-
    CommentEdit {
-
        /// Comment being edited.
-
        #[ts(as = "String")]
-
        id: cob::thread::CommentId,
-
        /// New value for the comment body.
-
        body: String,
-
        /// New value for the embeds list.
-
        #[ts(as = "Vec<String>")]
-
        embeds: Vec<cob::Embed<cob::Uri>>,
-
    },
-

-
    /// Redact a change. Not all changes can be redacted.
-
    #[serde(rename = "comment.redact")]
-
    CommentRedact {
-
        #[ts(as = "String")]
-
        id: cob::thread::CommentId,
-
    },
-

-
    /// React to a comment.
-
    #[serde(rename = "comment.react")]
-
    CommentReact {
-
        #[ts(as = "String")]
-
        id: cob::thread::CommentId,
-
        #[ts(as = "String")]
-
        reaction: cob::Reaction,
-
        active: bool,
-
    },
-
}
deleted crates/radicle-tauri/src/types/config.rs
@@ -1,23 +0,0 @@
-
use radicle::crypto::PublicKey;
-
use serde::Serialize;
-
use ts_rs::TS;
-

-
use radicle::node::config::DefaultSeedingPolicy;
-
use radicle::node::Alias;
-

-
/// Service configuration.
-
#[derive(TS, Serialize)]
-
#[serde(rename_all = "camelCase")]
-
#[ts(export)]
-
pub struct Config {
-
    /// Node Public Key in NID format.
-
    #[ts(as = "String")]
-
    pub public_key: PublicKey,
-
    /// Node alias.
-
    #[ts(as = "String")]
-
    pub alias: Alias,
-
    /// Default seeding policy.
-
    #[serde(default)]
-
    #[ts(type = "{ default: 'allow', scope: 'followed' | 'all' } | { default: 'block' }")]
-
    pub seeding_policy: DefaultSeedingPolicy,
-
}
deleted crates/radicle-tauri/src/types/repo.rs
@@ -1,52 +0,0 @@
-
use serde::Serialize;
-
use serde_json::Value;
-
use ts_rs::TS;
-

-
use radicle::identity::RepoId;
-

-
use super::cobs::Author;
-

-
/// Repos info.
-
#[derive(Serialize, TS)]
-
#[ts(export)]
-
pub struct RepoInfo {
-
    pub payloads: SupportedPayloads,
-
    #[ts(as = "Vec<Author>")]
-
    pub delegates: Vec<Author>,
-
    pub threshold: usize,
-
    #[ts(type = "{ type: 'public' } | { type: 'private', allow?: string[] }")]
-
    pub visibility: radicle::identity::Visibility,
-
    #[ts(as = "String")]
-
    pub rid: RepoId,
-
    pub seeding: usize,
-
}
-

-
#[derive(Serialize, TS)]
-
#[ts(export)]
-
pub struct SupportedPayloads {
-
    #[serde(rename = "xyz.radicle.project")]
-
    #[serde(default, skip_serializing_if = "Option::is_none")]
-
    #[ts(optional)]
-
    #[ts(type = r#"{
-
  data: {
-
    defaultBranch: string,
-
    description: string,
-
    name: string,
-
  },
-
  meta: {
-
    head: string,
-
    issues: {
-
      open: number,
-
      closed: number,
-
    },
-
    patches: {
-
      open: number,
-
      draft: number,
-
      archived: number,
-
      merged: number,
-
    }
-
    lastCommit: number,
-
  }
-
}"#)]
-
    pub project: Option<Value>,
-
}
deleted crates/radicle-tauri/src/types/thread.rs
@@ -1,200 +0,0 @@
-
use serde::{Deserialize, Serialize};
-
use ts_rs::TS;
-

-
use radicle::node::AliasStore;
-
use radicle::{cob, git};
-

-
use crate::types::cobs;
-
use crate::types::thread;
-

-
#[derive(TS, Serialize, Deserialize)]
-
#[ts(export)]
-
#[serde(rename_all = "camelCase")]
-
pub struct CreateReviewComment {
-
    #[ts(as = "String")]
-
    pub review_id: cob::patch::ReviewId,
-
    pub body: String,
-
    #[ts(as = "Option<String>")]
-
    pub reply_to: Option<cob::thread::CommentId>,
-
    pub location: Option<thread::CodeLocation>,
-
    #[ts(type = "{ name: string, content: string }[]")]
-
    pub embeds: Vec<cob::Embed<cob::Uri>>,
-
}
-

-
#[derive(Serialize, TS)]
-
#[serde(rename_all = "camelCase")]
-
pub struct Comment<T> {
-
    #[ts(as = "String")]
-
    id: cob::thread::CommentId,
-
    author: cobs::Author,
-
    edits: Vec<cobs::Edit>,
-
    reactions: Vec<cobs::Reaction>,
-
    #[ts(as = "Option<String>")]
-
    #[ts(optional)]
-
    reply_to: Option<cob::thread::CommentId>,
-
    #[ts(optional)]
-
    location: Option<T>,
-
    #[ts(type = "{ name: string, content: string }[]")]
-
    embeds: Vec<cob::Embed<cob::Uri>>,
-
    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: cobs::Author::new(comment.author().into(), aliases),
-
            edits: comment
-
                .edits()
-
                .map(|e| cobs::Edit::new(e, aliases))
-
                .collect::<Vec<_>>(),
-
            reactions: comment
-
                .reactions()
-
                .into_iter()
-
                .map(|(reaction, authors)| cobs::Reaction::new(*reaction, authors, None, aliases))
-
                .collect::<Vec<_>>(),
-
            reply_to: comment.reply_to(),
-
            location: comment.location().map(|l| CodeLocation::new(l.clone())),
-
            embeds: comment.embeds().to_vec(),
-
            resolved: comment.is_resolved(),
-
        }
-
    }
-
}
-

-
impl Comment<cobs::Never> {
-
    pub fn new(
-
        id: cob::thread::CommentId,
-
        comment: cob::thread::Comment,
-
        aliases: &impl AliasStore,
-
    ) -> Self {
-
        Self {
-
            id,
-
            author: cobs::Author::new(comment.author().into(), aliases),
-
            edits: comment
-
                .edits()
-
                .map(|e| cobs::Edit::new(e, aliases))
-
                .collect::<Vec<_>>(),
-
            reactions: comment
-
                .reactions()
-
                .into_iter()
-
                .map(|(reaction, authors)| cobs::Reaction::new(*reaction, authors, None, aliases))
-
                .collect::<Vec<_>>(),
-
            reply_to: comment.reply_to(),
-
            location: None,
-
            embeds: comment.embeds().to_vec(),
-
            resolved: comment.is_resolved(),
-
        }
-
    }
-
}
-

-
#[derive(Clone, TS, Serialize, Deserialize)]
-
#[ts(export)]
-
#[serde(rename_all = "camelCase")]
-
pub struct NewIssueComment {
-
    #[ts(as = "String")]
-
    pub id: git::Oid,
-
    pub body: String,
-
    #[ts(as = "Option<String>")]
-
    #[ts(optional)]
-
    pub reply_to: Option<cob::thread::CommentId>,
-
    #[ts(type = "{ name: string, content: string }[]")]
-
    pub embeds: Vec<cob::Embed<cob::Uri>>,
-
}
-

-
#[derive(Clone, TS, Serialize, Deserialize)]
-
#[ts(export)]
-
#[serde(rename_all = "camelCase")]
-
pub struct NewPatchComment {
-
    #[ts(as = "String")]
-
    pub id: git::Oid,
-
    #[ts(as = "String")]
-
    pub revision: git::Oid,
-
    pub body: String,
-
    #[ts(as = "Option<String>")]
-
    #[ts(optional)]
-
    pub reply_to: Option<cob::thread::CommentId>,
-
    pub location: Option<CodeLocation>,
-
    #[ts(type = "{ name: string, content: string }[]")]
-
    pub embeds: Vec<cob::Embed<cob::Uri>>,
-
}
-

-
#[derive(Clone, TS, Serialize, Deserialize)]
-
#[serde(rename_all = "camelCase")]
-
#[ts(export)]
-
pub struct CodeLocation {
-
    #[ts(as = "String")]
-
    commit: git::Oid,
-
    path: std::path::PathBuf,
-
    old: Option<CodeRange>,
-
    new: Option<CodeRange>,
-
}
-

-
impl From<cob::CodeLocation> for CodeLocation {
-
    fn from(val: cob::CodeLocation) -> Self {
-
        Self {
-
            commit: val.commit,
-
            path: val.path,
-
            old: val.old.map(|o| o.into()),
-
            new: val.new.map(|o| o.into()),
-
        }
-
    }
-
}
-

-
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 {
-
            commit: val.commit,
-
            path: val.path,
-
            old: val.old.map(|o| o.into()),
-
            new: val.new.map(|o| o.into()),
-
        }
-
    }
-
}
-

-
#[derive(Clone, TS, Serialize, Deserialize)]
-
#[serde(rename_all = "camelCase", tag = "type")]
-
#[ts(export)]
-
pub enum CodeRange {
-
    Lines {
-
        #[ts(type = "{ start: number, end: number }")]
-
        range: std::ops::Range<usize>,
-
    },
-
    Chars {
-
        line: usize,
-
        #[ts(type = "{ start: number, end: number }")]
-
        range: std::ops::Range<usize>,
-
    },
-
}
-

-
impl From<cob::CodeRange> for CodeRange {
-
    fn from(val: cob::CodeRange) -> Self {
-
        match val {
-
            cob::CodeRange::Chars { line, range } => Self::Chars { line, range },
-
            cob::CodeRange::Lines { range } => Self::Lines { range },
-
        }
-
    }
-
}
-

-
impl From<CodeRange> for cob::CodeRange {
-
    fn from(val: CodeRange) -> Self {
-
        match val {
-
            CodeRange::Chars { line, range } => Self::Chars { line, range },
-
            CodeRange::Lines { range } => Self::Lines { range },
-
        }
-
    }
-
}
added crates/radicle-types/Cargo.toml
@@ -0,0 +1,11 @@
+
[package]
+
name = "radicle-types"
+
version = "0.1.0"
+
edition = "2021"
+

+
[dependencies]
+
radicle = { git = "https://seed.radicle.xyz/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git" }
+
radicle-surf = { version = "0.22.1", features = ["serde"] }
+
serde = { version = "1.0.210", features = ["derive"] }
+
serde_json = { version = "1.0.132" }
+
ts-rs = { version = "10.0.0", features = ["serde-json-impl", "no-serde-warnings"] }

\ No newline at end of file
added crates/radicle-types/src/cobs.rs
@@ -0,0 +1,434 @@
+
use std::collections::BTreeMap;
+
use std::collections::BTreeSet;
+

+
use serde::{Deserialize, Serialize};
+
use ts_rs::TS;
+

+
use radicle::cob;
+
use radicle::crypto;
+
use radicle::identity;
+
use radicle::issue;
+
use radicle::node::{Alias, AliasStore};
+
use radicle::patch;
+
use radicle::storage::git;
+

+
use crate::cobs;
+
use crate::thread;
+

+
#[derive(Serialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
pub struct Author {
+
    #[ts(as = "String")]
+
    did: identity::Did,
+
    #[serde(skip_serializing_if = "Option::is_none")]
+
    #[ts(as = "Option<String>")]
+
    #[ts(optional)]
+
    alias: Option<Alias>,
+
}
+

+
impl Author {
+
    pub fn new(did: identity::Did, aliases: &impl AliasStore) -> Self {
+
        Self {
+
            did,
+
            alias: aliases.alias(&did),
+
        }
+
    }
+
}
+

+
#[derive(TS, Serialize)]
+
#[ts(export)]
+
#[serde(rename_all = "camelCase")]
+
pub struct Issue {
+
    #[ts(as = "String")]
+
    id: String,
+
    author: Author,
+
    title: String,
+
    #[ts(type = "{ status: 'closed', reason: 'other' | 'solved' } | { status: 'open' } ")]
+
    state: issue::State,
+
    assignees: Vec<Author>,
+
    discussion: Vec<thread::Comment<cobs::Never>>,
+
    #[ts(as = "Vec<String>")]
+
    labels: Vec<cob::Label>,
+
    #[ts(type = "number")]
+
    timestamp: cob::Timestamp,
+
}
+

+
impl Issue {
+
    pub fn new(id: &issue::IssueId, issue: &issue::Issue, aliases: &impl AliasStore) -> Self {
+
        Self {
+
            id: id.to_string(),
+
            author: Author::new(*issue.author().id(), aliases),
+
            title: issue.title().to_string(),
+
            state: *issue.state(),
+
            assignees: issue
+
                .assignees()
+
                .map(|did| Author::new(*did, aliases))
+
                .collect::<Vec<_>>(),
+
            discussion: issue
+
                .comments()
+
                .map(|(id, c)| thread::Comment::<Never>::new(*id, c.clone(), aliases))
+
                .collect::<Vec<_>>(),
+
            labels: issue.labels().cloned().collect::<Vec<_>>(),
+
            timestamp: issue.timestamp(),
+
        }
+
    }
+
}
+

+
#[derive(TS, Serialize)]
+
#[ts(export)]
+
#[serde(rename_all = "camelCase")]
+
pub struct Patch {
+
    #[ts(as = "String")]
+
    id: String,
+
    author: Author,
+
    title: String,
+
    #[ts(as = "String")]
+
    base: git::Oid,
+
    #[ts(as = "String")]
+
    head: git::Oid,
+
    #[ts(type = r#"{
+
  status: 'draft'
+
} | {
+
  status: 'open',
+
  conflicts: [string, string][]
+
} | {
+
  status: 'archived'
+
} | {
+
  status: 'merged', revision: string, commit: string
+
} "#)]
+
    state: patch::State,
+
    assignees: Vec<Author>,
+
    #[ts(as = "Vec<String>")]
+
    labels: Vec<cob::Label>,
+
    #[ts(type = "number")]
+
    timestamp: cob::Timestamp,
+
    revision_count: usize,
+
}
+

+
impl Patch {
+
    pub fn new(id: patch::PatchId, patch: patch::Patch, aliases: &impl AliasStore) -> Self {
+
        Self {
+
            id: id.to_string(),
+
            author: Author::new(*patch.author().id(), aliases),
+
            title: patch.title().to_string(),
+
            state: patch.state().clone(),
+
            base: *patch.base(),
+
            head: *patch.head(),
+
            assignees: patch
+
                .assignees()
+
                .map(|did| Author::new(did, aliases))
+
                .collect::<Vec<_>>(),
+
            labels: patch.labels().cloned().collect::<Vec<_>>(),
+
            timestamp: patch.timestamp(),
+
            revision_count: patch.revisions().count(),
+
        }
+
    }
+

+
    pub fn timestamp(&self) -> u64 {
+
        self.timestamp.as_millis()
+
    }
+
}
+

+
#[derive(Serialize, Deserialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
pub struct ReviewEdit {
+
    #[ts(as = "String")]
+
    pub review_id: cob::patch::ReviewId,
+
    #[ts(as = "Option<String>")]
+
    pub verdict: Option<cob::patch::Verdict>,
+
    pub summary: Option<String>,
+
    #[ts(as = "Vec<String>")]
+
    pub labels: Vec<cob::Label>,
+
}
+

+
#[derive(Serialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
pub struct Revision {
+
    #[ts(as = "String")]
+
    id: patch::RevisionId,
+
    author: Author,
+
    description: Vec<Edit>,
+
    #[ts(as = "String")]
+
    base: git::Oid,
+
    #[ts(as = "String")]
+
    head: git::Oid,
+
    reviews: Vec<Review>,
+
    #[ts(type = "number")]
+
    timestamp: cob::common::Timestamp,
+
    discussion: Vec<thread::Comment<thread::CodeLocation>>,
+
    reactions: Vec<Reaction>,
+
}
+

+
impl Revision {
+
    pub fn new(value: cob::patch::Revision, aliases: &impl AliasStore) -> Self {
+
        Self {
+
            id: value.id(),
+
            author: Author::new(*value.author().id(), aliases),
+
            description: value
+
                .edits()
+
                .map(|e| Edit::new(e, aliases))
+
                .collect::<Vec<_>>(),
+
            base: *value.base(),
+
            head: 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)| {
+
                    thread::Comment::<thread::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| thread::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<thread::CodeLocation>,
+
}
+

+
impl Reaction {
+
    pub fn new(
+
        emoji: cob::Reaction,
+
        authors: Vec<&crypto::PublicKey>,
+
        location: Option<thread::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<thread::Comment<thread::CodeLocation>>,
+
    #[ts(type = "number")]
+
    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)| {
+
                    thread::Comment::<thread::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(type = "number")]
+
    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(TS, Serialize, Deserialize)]
+
#[ts(export)]
+
#[serde(rename_all = "camelCase")]
+
pub struct NewIssue {
+
    pub title: String,
+
    pub description: String,
+
    #[ts(as = "Vec<String>")]
+
    pub labels: Vec<cob::Label>,
+
    #[ts(as = "Vec<String>")]
+
    pub assignees: Vec<identity::Did>,
+
    #[ts(type = "{ name: string, content: string }[]")]
+
    pub embeds: Vec<cob::Embed<cob::Uri>>,
+
}
+

+
#[derive(TS, Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
pub struct CobOptions {
+
    #[ts(as = "Option<bool>")]
+
    #[ts(optional)]
+
    announce: Option<bool>,
+
}
+

+
impl CobOptions {
+
    pub fn announce(&self) -> bool {
+
        self.announce.unwrap_or(true)
+
    }
+
}
+

+
#[derive(TS, Serialize)]
+
#[ts(export)]
+
pub struct Stats {
+
    pub files_changed: usize,
+
    pub insertions: usize,
+
    pub deletions: usize,
+
}
+

+
impl Stats {
+
    pub fn new(stats: &radicle_surf::diff::Stats) -> Self {
+
        Self {
+
            files_changed: stats.files_changed,
+
            insertions: stats.insertions,
+
            deletions: stats.deletions,
+
        }
+
    }
+
}
+

+
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, TS)]
+
#[serde(tag = "type", rename_all = "camelCase")]
+
#[ts(export)]
+
pub enum IssueAction {
+
    /// Assign issue to an actor.
+
    #[serde(rename = "assign")]
+
    Assign {
+
        #[ts(as = "Vec<String>")]
+
        assignees: BTreeSet<identity::Did>,
+
    },
+

+
    /// Edit issue title.
+
    #[serde(rename = "edit")]
+
    Edit { title: String },
+

+
    /// Transition to a different state.
+
    #[serde(rename = "lifecycle")]
+
    Lifecycle {
+
        #[ts(type = "{ status: 'closed', reason: 'other' | 'solved' } | { status: 'open' } ")]
+
        state: issue::State,
+
    },
+

+
    /// Modify issue labels.
+
    #[serde(rename = "label")]
+
    Label {
+
        #[ts(as = "Vec<String>")]
+
        labels: BTreeSet<cob::Label>,
+
    },
+

+
    /// Comment on a thread.
+
    #[serde(rename_all = "camelCase")]
+
    #[serde(rename = "comment")]
+
    Comment {
+
        /// Comment body.
+
        body: String,
+
        /// Comment this is a reply to.
+
        /// Should be [`None` | `null`] if it's the top-level comment.
+
        /// Should be the root [`CommentId`] if it's a top-level comment.
+
        #[serde(default, skip_serializing_if = "Option::is_none")]
+
        #[ts(as = "Option<String>")]
+
        reply_to: Option<cob::thread::CommentId>,
+
        /// Embeded content.
+
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
+
        #[ts(as = "Vec<String>")]
+
        embeds: Vec<cob::Embed<cob::Uri>>,
+
    },
+

+
    /// Edit a comment.
+
    #[serde(rename = "comment.edit")]
+
    CommentEdit {
+
        /// Comment being edited.
+
        #[ts(as = "String")]
+
        id: cob::thread::CommentId,
+
        /// New value for the comment body.
+
        body: String,
+
        /// New value for the embeds list.
+
        #[ts(as = "Vec<String>")]
+
        embeds: Vec<cob::Embed<cob::Uri>>,
+
    },
+

+
    /// Redact a change. Not all changes can be redacted.
+
    #[serde(rename = "comment.redact")]
+
    CommentRedact {
+
        #[ts(as = "String")]
+
        id: cob::thread::CommentId,
+
    },
+

+
    /// React to a comment.
+
    #[serde(rename = "comment.react")]
+
    CommentReact {
+
        #[ts(as = "String")]
+
        id: cob::thread::CommentId,
+
        #[ts(as = "String")]
+
        reaction: cob::Reaction,
+
        active: bool,
+
    },
+
}
added crates/radicle-types/src/config.rs
@@ -0,0 +1,23 @@
+
use radicle::crypto::PublicKey;
+
use serde::Serialize;
+
use ts_rs::TS;
+

+
use radicle::node::config::DefaultSeedingPolicy;
+
use radicle::node::Alias;
+

+
/// Service configuration.
+
#[derive(TS, Serialize)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
pub struct Config {
+
    /// Node Public Key in NID format.
+
    #[ts(as = "String")]
+
    pub public_key: PublicKey,
+
    /// Node alias.
+
    #[ts(as = "String")]
+
    pub alias: Alias,
+
    /// Default seeding policy.
+
    #[serde(default)]
+
    #[ts(type = "{ default: 'allow', scope: 'followed' | 'all' } | { default: 'block' }")]
+
    pub seeding_policy: DefaultSeedingPolicy,
+
}
added crates/radicle-types/src/lib.rs
@@ -0,0 +1,4 @@
+
pub mod cobs;
+
pub mod config;
+
pub mod repo;
+
pub mod thread;
added crates/radicle-types/src/repo.rs
@@ -0,0 +1,52 @@
+
use serde::Serialize;
+
use serde_json::Value;
+
use ts_rs::TS;
+

+
use radicle::identity::RepoId;
+

+
use super::cobs::Author;
+

+
/// Repos info.
+
#[derive(Serialize, TS)]
+
#[ts(export)]
+
pub struct RepoInfo {
+
    pub payloads: SupportedPayloads,
+
    #[ts(as = "Vec<Author>")]
+
    pub delegates: Vec<Author>,
+
    pub threshold: usize,
+
    #[ts(type = "{ type: 'public' } | { type: 'private', allow?: string[] }")]
+
    pub visibility: radicle::identity::Visibility,
+
    #[ts(as = "String")]
+
    pub rid: RepoId,
+
    pub seeding: usize,
+
}
+

+
#[derive(Serialize, TS)]
+
#[ts(export)]
+
pub struct SupportedPayloads {
+
    #[serde(rename = "xyz.radicle.project")]
+
    #[serde(default, skip_serializing_if = "Option::is_none")]
+
    #[ts(optional)]
+
    #[ts(type = r#"{
+
  data: {
+
    defaultBranch: string,
+
    description: string,
+
    name: string,
+
  },
+
  meta: {
+
    head: string,
+
    issues: {
+
      open: number,
+
      closed: number,
+
    },
+
    patches: {
+
      open: number,
+
      draft: number,
+
      archived: number,
+
      merged: number,
+
    }
+
    lastCommit: number,
+
  }
+
}"#)]
+
    pub project: Option<Value>,
+
}
added crates/radicle-types/src/thread.rs
@@ -0,0 +1,200 @@
+
use serde::{Deserialize, Serialize};
+
use ts_rs::TS;
+

+
use radicle::node::AliasStore;
+
use radicle::{cob, git};
+

+
use crate::cobs;
+
use crate::thread;
+

+
#[derive(TS, Serialize, Deserialize)]
+
#[ts(export)]
+
#[serde(rename_all = "camelCase")]
+
pub struct CreateReviewComment {
+
    #[ts(as = "String")]
+
    pub review_id: cob::patch::ReviewId,
+
    pub body: String,
+
    #[ts(as = "Option<String>")]
+
    pub reply_to: Option<cob::thread::CommentId>,
+
    pub location: Option<thread::CodeLocation>,
+
    #[ts(type = "{ name: string, content: string }[]")]
+
    pub embeds: Vec<cob::Embed<cob::Uri>>,
+
}
+

+
#[derive(Serialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
pub struct Comment<T> {
+
    #[ts(as = "String")]
+
    id: cob::thread::CommentId,
+
    author: cobs::Author,
+
    edits: Vec<cobs::Edit>,
+
    reactions: Vec<cobs::Reaction>,
+
    #[ts(as = "Option<String>")]
+
    #[ts(optional)]
+
    reply_to: Option<cob::thread::CommentId>,
+
    #[ts(optional)]
+
    location: Option<T>,
+
    #[ts(type = "{ name: string, content: string }[]")]
+
    embeds: Vec<cob::Embed<cob::Uri>>,
+
    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: cobs::Author::new(comment.author().into(), aliases),
+
            edits: comment
+
                .edits()
+
                .map(|e| cobs::Edit::new(e, aliases))
+
                .collect::<Vec<_>>(),
+
            reactions: comment
+
                .reactions()
+
                .into_iter()
+
                .map(|(reaction, authors)| cobs::Reaction::new(*reaction, authors, None, aliases))
+
                .collect::<Vec<_>>(),
+
            reply_to: comment.reply_to(),
+
            location: comment.location().map(|l| CodeLocation::new(l.clone())),
+
            embeds: comment.embeds().to_vec(),
+
            resolved: comment.is_resolved(),
+
        }
+
    }
+
}
+

+
impl Comment<cobs::Never> {
+
    pub fn new(
+
        id: cob::thread::CommentId,
+
        comment: cob::thread::Comment,
+
        aliases: &impl AliasStore,
+
    ) -> Self {
+
        Self {
+
            id,
+
            author: cobs::Author::new(comment.author().into(), aliases),
+
            edits: comment
+
                .edits()
+
                .map(|e| cobs::Edit::new(e, aliases))
+
                .collect::<Vec<_>>(),
+
            reactions: comment
+
                .reactions()
+
                .into_iter()
+
                .map(|(reaction, authors)| cobs::Reaction::new(*reaction, authors, None, aliases))
+
                .collect::<Vec<_>>(),
+
            reply_to: comment.reply_to(),
+
            location: None,
+
            embeds: comment.embeds().to_vec(),
+
            resolved: comment.is_resolved(),
+
        }
+
    }
+
}
+

+
#[derive(Clone, TS, Serialize, Deserialize)]
+
#[ts(export)]
+
#[serde(rename_all = "camelCase")]
+
pub struct NewIssueComment {
+
    #[ts(as = "String")]
+
    pub id: git::Oid,
+
    pub body: String,
+
    #[ts(as = "Option<String>")]
+
    #[ts(optional)]
+
    pub reply_to: Option<cob::thread::CommentId>,
+
    #[ts(type = "{ name: string, content: string }[]")]
+
    pub embeds: Vec<cob::Embed<cob::Uri>>,
+
}
+

+
#[derive(Clone, TS, Serialize, Deserialize)]
+
#[ts(export)]
+
#[serde(rename_all = "camelCase")]
+
pub struct NewPatchComment {
+
    #[ts(as = "String")]
+
    pub id: git::Oid,
+
    #[ts(as = "String")]
+
    pub revision: git::Oid,
+
    pub body: String,
+
    #[ts(as = "Option<String>")]
+
    #[ts(optional)]
+
    pub reply_to: Option<cob::thread::CommentId>,
+
    pub location: Option<CodeLocation>,
+
    #[ts(type = "{ name: string, content: string }[]")]
+
    pub embeds: Vec<cob::Embed<cob::Uri>>,
+
}
+

+
#[derive(Clone, TS, Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
pub struct CodeLocation {
+
    #[ts(as = "String")]
+
    commit: git::Oid,
+
    path: std::path::PathBuf,
+
    old: Option<CodeRange>,
+
    new: Option<CodeRange>,
+
}
+

+
impl From<cob::CodeLocation> for CodeLocation {
+
    fn from(val: cob::CodeLocation) -> Self {
+
        Self {
+
            commit: val.commit,
+
            path: val.path,
+
            old: val.old.map(|o| o.into()),
+
            new: val.new.map(|o| o.into()),
+
        }
+
    }
+
}
+

+
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 {
+
            commit: val.commit,
+
            path: val.path,
+
            old: val.old.map(|o| o.into()),
+
            new: val.new.map(|o| o.into()),
+
        }
+
    }
+
}
+

+
#[derive(Clone, TS, Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase", tag = "type")]
+
#[ts(export)]
+
pub enum CodeRange {
+
    Lines {
+
        #[ts(type = "{ start: number, end: number }")]
+
        range: std::ops::Range<usize>,
+
    },
+
    Chars {
+
        line: usize,
+
        #[ts(type = "{ start: number, end: number }")]
+
        range: std::ops::Range<usize>,
+
    },
+
}
+

+
impl From<cob::CodeRange> for CodeRange {
+
    fn from(val: cob::CodeRange) -> Self {
+
        match val {
+
            cob::CodeRange::Chars { line, range } => Self::Chars { line, range },
+
            cob::CodeRange::Lines { range } => Self::Lines { range },
+
        }
+
    }
+
}
+

+
impl From<CodeRange> for cob::CodeRange {
+
    fn from(val: CodeRange) -> Self {
+
        match val {
+
            CodeRange::Chars { line, range } => Self::Chars { line, range },
+
            CodeRange::Lines { range } => Self::Lines { range },
+
        }
+
    }
+
}