From 65842500b66f6b3bf8171ccee965f869c10a0b2e Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Fri, 30 Aug 2024 16:06:27 +0200 Subject: [PATCH] http: Decouple api responses from heartwood crates --- radicle-httpd/src/api.rs | 14 +- radicle-httpd/src/api/json.rs | 319 +++++---------------------- radicle-httpd/src/api/json/cobs.rs | 118 ++++++++++ radicle-httpd/src/api/json/commit.rs | 97 ++++++++ radicle-httpd/src/api/json/diff.rs | 207 +++++++++++++++++ radicle-httpd/src/api/json/thread.rs | 43 ++++ radicle-httpd/src/api/v1/repos.rs | 28 ++- 7 files changed, 538 insertions(+), 288 deletions(-) create mode 100644 radicle-httpd/src/api/json/cobs.rs create mode 100644 radicle-httpd/src/api/json/commit.rs create mode 100644 radicle-httpd/src/api/json/diff.rs create mode 100644 radicle-httpd/src/api/json/thread.rs diff --git a/radicle-httpd/src/api.rs b/radicle-httpd/src/api.rs index 51c937cec..526109312 100644 --- a/radicle-httpd/src/api.rs +++ b/radicle-httpd/src/api.rs @@ -7,19 +7,15 @@ use axum::http::Method; use axum::response::{IntoResponse, Json}; use axum::routing::get; use axum::Router; -use radicle::identity::doc::PayloadId; -use radicle::issue::cache::Issues as _; -use radicle::patch::cache::Patches as _; -use radicle::storage::git::Repository; -use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use tower_http::cors::{self, CorsLayer}; -use radicle::cob::{issue, patch, Author}; +use radicle::identity::doc::PayloadId; use radicle::identity::{DocAt, RepoId}; -use radicle::node::policy::Scope; +use radicle::issue::cache::Issues as _; use radicle::node::routing::Store; -use radicle::node::{AliasStore, NodeId}; +use radicle::patch::cache::Patches as _; +use radicle::storage::git::Repository; use radicle::storage::{ReadRepository, ReadStorage}; use radicle::Profile; @@ -62,7 +58,7 @@ impl Context { let delegates = doc .delegates .into_iter() - .map(|did| json::author(&Author::new(did), aliases.alias(did.as_key()))) + .map(|did| json::Author::new(&did).as_json(&aliases)) .collect::>(); let db = &self.profile.database()?; let seeding = db.count(&rid).unwrap_or_default(); diff --git a/radicle-httpd/src/api/json.rs b/radicle-httpd/src/api/json.rs index d4a17fd62..fd6148288 100644 --- a/radicle-httpd/src/api/json.rs +++ b/radicle-httpd/src/api/json.rs @@ -1,296 +1,81 @@ -//! Utilities for building JSON responses of our API. - use std::collections::BTreeMap; -use std::path::Path; -use std::str; -use base64::prelude::{Engine, BASE64_STANDARD}; use serde_json::{json, Value}; -use radicle::cob::issue::{Issue, IssueId}; -use radicle::cob::patch::{Merge, Patch, PatchId, Review}; -use radicle::cob::thread::{Comment, CommentId, Edit}; -use radicle::cob::{ActorId, Author, CodeLocation, Reaction}; -use radicle::git::RefString; -use radicle::node::{Alias, AliasStore}; -use radicle::prelude::NodeId; -use radicle::storage::{git, refs, RemoteRepository}; -use radicle_surf::blob::Blob; -use radicle_surf::tree::{EntryKind, Tree}; -use radicle_surf::{Commit, Oid}; - -/// Returns JSON of a commit. -pub(crate) fn commit(commit: &Commit) -> Value { - json!({ - "id": commit.id, - "author": { - "name": commit.author.name, - "email": commit.author.email - }, - "summary": commit.summary, - "description": commit.description(), - "parents": commit.parents, - "committer": { - "name": commit.committer.name, - "email": commit.committer.email, - "time": commit.committer.time.seconds() - } - }) -} - -/// Returns JSON for a blob with a given `path`. -pub(crate) fn blob>(blob: &Blob, path: &str) -> Value { - json!({ - "binary": blob.is_binary(), - "name": name_in_path(path), - "content": blob_content(blob), - "path": path, - "lastCommit": commit(blob.commit()) - }) -} - -/// Returns a string for the blob content, encoded in base64 if binary. -pub fn blob_content>(blob: &Blob) -> String { - match str::from_utf8(blob.content()) { - Ok(s) => s.to_owned(), - Err(_) => BASE64_STANDARD.encode(blob.content()), - } -} - -/// Returns JSON for a tree with a given `path` and `stats`. -pub(crate) fn tree(tree: &Tree, path: &str) -> Value { - let prefix = Path::new(path); - let entries = tree - .entries() - .iter() - .map(|entry| { - json!({ - "path": prefix.join(entry.name()), - "oid": entry.object_id(), - "name": entry.name(), - "kind": match entry.entry() { - EntryKind::Tree(_) => "tree", - EntryKind::Blob(_) => "blob", - EntryKind::Submodule { .. } => "submodule" - }, - }) - }) - .collect::>(); - - json!({ - "entries": &entries, - "lastCommit": commit(tree.commit()), - "name": name_in_path(path), - "path": path, - }) -} - -/// Returns JSON for an `issue`. -pub(crate) fn issue(id: IssueId, issue: Issue, aliases: &impl AliasStore) -> Value { - json!({ - "id": id.to_string(), - "author": author(&issue.author(), aliases.alias(issue.author().id())), - "title": issue.title(), - "state": issue.state(), - "assignees": issue.assignees().map(|assignee| - author(&Author::from(*assignee.as_key()), aliases.alias(assignee)) - ).collect::>(), - "discussion": issue.comments().map(|(id, c)| issue_comment(id, c, aliases)).collect::>(), - "labels": issue.labels().collect::>(), - }) -} +use radicle::cob; +use radicle::identity; +use radicle::node::AliasStore; -/// Returns JSON for a `patch`. -pub(crate) fn patch( - id: PatchId, - patch: Patch, - repo: &git::Repository, - aliases: &impl AliasStore, -) -> Value { - json!({ - "id": id.to_string(), - "author": author(patch.author(), aliases.alias(patch.author().id())), - "title": patch.title(), - "state": patch.state(), - "target": patch.target(), - "labels": patch.labels().collect::>(), - "merges": patch.merges().map(|(nid, m)| merge(nid, m, aliases)).collect::>(), - "assignees": patch.assignees().map(|assignee| - author(&Author::from(*assignee), aliases.alias(&assignee)) - ).collect::>(), - "revisions": patch.revisions().map(|(id, rev)| { - json!({ - "id": id, - "author": author(rev.author(), aliases.alias(rev.author().id())), - "description": rev.description(), - "edits": rev.edits().map(|e| edit(e, aliases)).collect::>(), - "reactions": rev.reactions().iter().flat_map(|(location, reaction)| { - reactions(reaction.iter().fold(BTreeMap::new(), |mut acc: BTreeMap<&Reaction, Vec<_>>, (author, emoji)| { - acc.entry(emoji).or_default().push(author); - acc - }), location.as_ref(), aliases) - }).collect::>(), - "base": rev.base(), - "oid": rev.head(), - "refs": get_refs(repo, patch.author().id(), &rev.head()).unwrap_or_default(), - "discussions": rev.discussion().comments().map(|(id, c)| { - patch_comment(id, c, aliases) - }).collect::>(), - "timestamp": rev.timestamp().as_secs(), - "reviews": rev.reviews().into_iter().map(move |(_, r)| { - review(r, aliases) - }).collect::>(), - }) - }).collect::>(), - }) -} +pub(crate) mod cobs; +pub(crate) mod commit; +pub(crate) mod diff; +pub(crate) mod thread; /// Returns JSON for a `reaction`. -fn reactions( - reactions: BTreeMap<&Reaction, Vec<&ActorId>>, - location: Option<&CodeLocation>, +pub fn reactions( + reactions: BTreeMap<&cob::Reaction, Vec<&cob::ActorId>>, + location: Option<&cob::CodeLocation>, aliases: &impl AliasStore, ) -> Vec { reactions .into_iter() .map(|(emoji, authors)| { - if let Some(l) = location { - json!({ "location": l, "emoji": emoji, "authors": authors.into_iter().map(|a| - author(&Author::from(*a), aliases.alias(a)) - ).collect::>()}) + if let Some(loc) = location { + json!({ + "location": diff::CodeLocation::new(loc).as_json(), + "emoji": emoji, + "authors": authors.into_iter().map(|a| Author::new(&a.into()).as_json(aliases)).collect::>() }) } else { - json!({ "emoji": emoji, "authors": authors.into_iter().map(|a| - author(&Author::from(*a), aliases.alias(a)) - ).collect::>()}) + json!({ + "emoji": emoji, + "authors": authors.into_iter().map(|a| Author::new(&a.into()).as_json(aliases)).collect::>() }) } }) .collect::>() } -/// Returns JSON for an `author` and fills in `alias` when present. -pub(crate) fn author(author: &Author, alias: Option) -> Value { - match alias { - Some(alias) => json!({ - "id": author.id, - "alias": alias, - }), - None => json!(author), - } -} - -/// Returns JSON for a patch `Merge` and fills in `alias` when present. -fn merge(nid: &NodeId, merge: &Merge, aliases: &impl AliasStore) -> Value { - json!({ - "author": author(&Author::from(*nid), aliases.alias(nid)), - "commit": merge.commit, - "timestamp": merge.timestamp.as_secs(), - "revision": merge.revision, - }) -} - -/// Returns JSON for a patch `Review` and fills in `alias` when present. -fn review(review: &Review, aliases: &impl AliasStore) -> Value { - let a = review.author(); - json!({ - "id": review.id(), - "author": author(a, aliases.alias(a.id())), - "verdict": review.verdict(), - "summary": review.summary(), - "comments": review.comments().map(|(id, c)| review_comment(id, c, aliases)).collect::>(), - "timestamp": review.timestamp().as_secs(), - }) -} - /// Returns JSON for an `Edit`. -fn edit(edit: &Edit, aliases: &impl AliasStore) -> Value { - json!({ - "author": author(&Author::from(edit.author), aliases.alias(&edit.author)), - "body": edit.body, - "timestamp": edit.timestamp.as_secs(), - "embeds": edit.embeds, - }) -} - -/// Returns JSON for a Issue `Comment`. -fn issue_comment(id: &CommentId, comment: &Comment, aliases: &impl AliasStore) -> Value { - json!({ - "id": *id, - "author": author(&Author::from(comment.author()), aliases.alias(&comment.author())), - "body": comment.body(), - "edits": comment.edits().map(|e| edit(e, aliases)).collect::>(), - "embeds": comment.embeds().to_vec(), - "reactions": reactions(comment.reactions(), None, aliases), - "timestamp": comment.timestamp().as_secs(), - "replyTo": comment.reply_to(), - "resolved": comment.is_resolved(), - }) +pub fn embeds(embeds: &[cob::Embed]) -> Vec { + embeds + .into_iter() + .map(|e| { + json!({ + "name": e.name, + "content": e.content, + }) + }) + .collect::>() } -/// Returns JSON for a Patch `Comment`. -fn patch_comment( - id: &CommentId, - comment: &Comment, - aliases: &impl AliasStore, -) -> Value { +/// Returns JSON for an `Edit`. +pub fn edit(edit: &cob::thread::Edit, aliases: &impl AliasStore) -> Value { json!({ - "id": *id, - "author": author(&Author::from(comment.author()), aliases.alias(&comment.author())), - "body": comment.body(), - "edits": comment.edits().map(|e| edit(e, aliases)).collect::>(), - "embeds": comment.embeds().to_vec(), - "reactions": reactions(comment.reactions(), None, aliases), - "timestamp": comment.timestamp().as_secs(), - "replyTo": comment.reply_to(), - "location": comment.location(), - "resolved": comment.is_resolved(), + "author": Author::new(&edit.author.into()).as_json(aliases), + "body": edit.body, + "timestamp": edit.timestamp.as_secs(), + "embeds": edit.embeds.iter().map(|e| json!({ + "name": e.name, + "content": e.content, + })).collect::>(), }) } -/// Returns JSON for a `Review`. -fn review_comment( - id: &CommentId, - comment: &Comment, - aliases: &impl AliasStore, -) -> Value { - json!({ - "id": *id, - "author": author(&Author::from(comment.author()), aliases.alias(&comment.author())), - "body": comment.body(), - "edits": comment.edits().map(|e| edit(e, aliases)).collect::>(), - "embeds": comment.embeds().to_vec(), - "reactions": reactions(comment.reactions(), None, aliases), - "timestamp": comment.timestamp().as_secs(), - "replyTo": comment.reply_to(), - "location": comment.location(), - "resolved": comment.is_resolved(), - }) -} +pub(crate) struct Author<'a>(&'a identity::Did); -/// Returns the name part of a path string. -fn name_in_path(path: &str) -> &str { - match path.rsplit('/').next() { - Some(name) => name, - None => path, +impl<'a> Author<'a> { + pub fn new(did: &'a identity::Did) -> Self { + Self(did) } -} - -fn get_refs( - repo: &git::Repository, - id: &ActorId, - head: &Oid, -) -> Result, refs::Error> { - let remote = repo.remote(id)?; - let refs = remote - .refs - .iter() - .filter_map(|(name, o)| { - if o == head { - Some(name.to_owned()) - } else { - None - } - }) - .collect::>(); - Ok(refs) + pub fn as_json(&self, aliases: &impl AliasStore) -> Value { + match aliases.alias(&self.0) { + Some(alias) => json!({ + "id": self.0, + "alias": alias, + }), + None => json!({ + "id": self.0 + }), + } + } } diff --git a/radicle-httpd/src/api/json/cobs.rs b/radicle-httpd/src/api/json/cobs.rs new file mode 100644 index 000000000..70b9ded5a --- /dev/null +++ b/radicle-httpd/src/api/json/cobs.rs @@ -0,0 +1,118 @@ +use std::collections::BTreeMap; + +use radicle_surf as surf; +use serde_json::{json, Value}; + +use radicle::cob; +use radicle::cob::{issue, patch}; +use radicle::identity; +use radicle::node::AliasStore; +use radicle::storage::{git, refs, RemoteRepository}; + +use super::thread; +use super::{edit, reactions, Author}; + +pub(crate) struct Issue<'a>(&'a issue::Issue); + +impl<'a> Issue<'a> { + pub fn new(issue: &'a issue::Issue) -> Self { + Self(issue) + } + + pub fn as_json(&self, id: issue::IssueId, aliases: &impl AliasStore) -> Value { + json!({ + "id": id.to_string(), + "author": Author::new(&self.0.author().id()).as_json(aliases), + "title": self.0.title(), + "state": self.0.state(), + "assignees": self.0.assignees().map(|assignee| Author::new(assignee).as_json(aliases)).collect::>(), + "discussion": self.0.comments().map(|(id, c)| + thread::Comment::Issue(&c).as_json(id, aliases) + ).collect::>(), + "labels": self.0.labels().collect::>(), + }) + } +} + +pub(crate) struct Patch<'a>(&'a patch::Patch); + +impl<'a> Patch<'a> { + pub fn new(patch: &'a patch::Patch) -> Self { + Self(patch) + } + + pub fn as_json( + &self, + id: patch::PatchId, + repo: &git::Repository, + aliases: &impl AliasStore, + ) -> Value { + json!({ + "id": id.to_string(), + "author": Author::new(self.0.author().id()).as_json(aliases), + "title": self.0.title(), + "state": self.0.state(), + "target": self.0.target(), + "labels": self.0.labels().collect::>(), + "merges": self.0.merges().map(|(nid, m)| json!({ + "author": Author::new(&identity::Did::from(nid)).as_json(aliases), + "commit": m.commit, + "timestamp": m.timestamp.as_secs(), + "revision": m.revision, + })).collect::>(), + "assignees": self.0.assignees().map(|assignee| Author::new(&assignee).as_json(aliases)).collect::>(), + "revisions": self.0.revisions().map(|(id, rev)| { + json!({ + "id": id, + "author": Author::new(rev.author().id()).as_json(aliases), + "description": rev.description(), + "edits": rev.edits().map(|e| edit(e, aliases)).collect::>(), + "reactions": rev.reactions().iter().flat_map(|(location, reaction)| { + reactions(reaction.iter().fold(BTreeMap::new(), |mut acc: BTreeMap<&cob::Reaction, Vec<_>>, (author, emoji)| { + acc.entry(emoji).or_default().push(author); + acc + }), location.as_ref(), aliases) + }).collect::>(), + "base": rev.base(), + "oid": rev.head(), + "refs": get_refs(repo, self.0.author().id(), &rev.head()).unwrap_or_default(), + "discussions": rev.discussion().comments().map(|(id, c)| + thread::Comment::Patch(&c).as_json(id, aliases) + ).collect::>(), + "timestamp": rev.timestamp().as_secs(), + "reviews": rev.reviews().into_iter().map(move |(_, r)| json!({ + "id": r.id(), + "author": Author::new(r.author().id()).as_json(aliases), + "verdict": r.verdict(), + "summary": r.summary(), + "comments": r.comments().map(|(id, c)| + thread::Comment::Patch(&c).as_json(id, aliases) + ).collect::>(), + "timestamp": r.timestamp().as_secs(), + })).collect::>(), + }) + }).collect::>(), + }) + } +} + +fn get_refs( + repo: &git::Repository, + id: &cob::ActorId, + head: &surf::Oid, +) -> Result, refs::Error> { + let remote = repo.remote(id)?; + let refs = remote + .refs + .iter() + .filter_map(|(name, o)| { + if o == head { + Some(name.to_owned()) + } else { + None + } + }) + .collect::>(); + + Ok(refs) +} diff --git a/radicle-httpd/src/api/json/commit.rs b/radicle-httpd/src/api/json/commit.rs new file mode 100644 index 000000000..14d7ca998 --- /dev/null +++ b/radicle-httpd/src/api/json/commit.rs @@ -0,0 +1,97 @@ +use std::path::Path; +use std::str; + +use base64::{prelude::BASE64_STANDARD, Engine}; +use radicle_surf as surf; +use serde_json::{json, Value}; + +pub(crate) struct Commit<'a>(&'a surf::Commit); + +impl<'a> Commit<'a> { + pub fn new(commit: &'a surf::Commit) -> Self { + Self(commit) + } + + pub fn as_json(&self) -> Value { + json!({ + "id": self.0.id, + "author": { + "name": self.0.author.name, + "email": self.0.author.email + }, + "summary": self.0.summary, + "description": self.0.description(), + "parents": self.0.parents, + "committer": { + "name": self.0.committer.name, + "email": self.0.committer.email, + "time": self.0.committer.time.seconds() + } + }) + } +} + +pub(crate) struct Blob<'a, T: AsRef<[u8]>>(&'a surf::blob::Blob); + +impl<'a, T: AsRef<[u8]>> Blob<'a, T> { + pub fn new(blob: &'a surf::blob::Blob) -> Self { + Self(blob) + } + + pub fn as_json(&self, path: &str) -> Value { + json!({ + "binary": self.0.is_binary(), + "name": name_in_path(path), + "content": match str::from_utf8(self.0.content()) { + Ok(s) => s.to_owned(), + Err(_) => BASE64_STANDARD.encode(self.0.content()), + }, + "path": path, + "lastCommit": Commit(self.0.commit()).as_json() + }) + } +} + +pub(crate) struct Tree<'a>(&'a surf::tree::Tree); + +impl<'a> Tree<'a> { + pub fn new(tree: &'a surf::tree::Tree) -> Self { + Self(tree) + } + + pub fn as_json(&self, path: &str) -> Value { + let prefix = Path::new(path); + let entries = self + .0 + .entries() + .iter() + .map(|entry| { + json!({ + "path": prefix.join(entry.name()), + "oid": entry.object_id(), + "name": entry.name(), + "kind": match entry.entry() { + surf::tree::EntryKind::Tree(_) => "tree", + surf::tree::EntryKind::Blob(_) => "blob", + surf::tree::EntryKind::Submodule { .. } => "submodule" + }, + }) + }) + .collect::>(); + + json!({ + "entries": &entries, + "lastCommit": Commit::new(self.0.commit()).as_json(), + "name": name_in_path(path), + "path": path, + }) + } +} + +/// Returns the name part of a path string. +fn name_in_path(path: &str) -> &str { + match path.rsplit('/').next() { + Some(name) => name, + None => path, + } +} diff --git a/radicle-httpd/src/api/json/diff.rs b/radicle-httpd/src/api/json/diff.rs new file mode 100644 index 000000000..9bc237b27 --- /dev/null +++ b/radicle-httpd/src/api/json/diff.rs @@ -0,0 +1,207 @@ +use radicle_surf as surf; +use serde_json::{json, Value}; + +use radicle::cob; + +pub(crate) struct Diff<'a>(&'a surf::diff::Diff); + +impl<'a> Diff<'a> { + pub fn new(diff: &'a surf::diff::Diff) -> Self { + Self(diff) + } + + pub fn as_json(&self) -> Value { + let s = self.0.stats(); + json!({ + "files": self.0.files().into_iter().map(|f| { + match f { + surf::diff::FileDiff::Added(added) => json!({ + "status": "added", + "path": added.path, + "diff": DiffContent::new(&added.diff).as_json(), + "new": DiffFile::new(&added.new).as_json(), + }), + surf::diff::FileDiff::Deleted(deleted) => json!({ + "status": "deleted", + "path": deleted.path, + "diff": DiffContent::new(&deleted.diff).as_json(), + "old": DiffFile::new(&deleted.old).as_json(), + }), + surf::diff::FileDiff::Modified(modified) => json!({ + "status": "modified", + "path": modified.path, + "diff": DiffContent::new(&modified.diff).as_json(), + "old": DiffFile::new(&modified.old).as_json(), + "new": DiffFile::new(&modified.new).as_json(), + }), + surf::diff::FileDiff::Moved(moved) => { + if moved.old == moved.new { + json!({ + "status": "moved", + "oldPath": moved.old_path, + "newPath": moved.new_path, + "current": DiffFile::new(&moved.new).as_json(), + }) + } else { + json!({ + "status": "moved", + "oldPath": moved.old_path, + "newPath": moved.new_path, + "old": DiffFile::new(&moved.old).as_json(), + "new": DiffFile::new(&moved.new).as_json(), + "diff": DiffContent::new(&moved.diff).as_json() + }) + } + }, + surf::diff::FileDiff::Copied(copied) => { + if copied.old == copied.new { + json!({ + "status": "copied", + "oldPath": copied.old_path, + "newPath": copied.new_path, + "current": DiffFile::new(&copied.new).as_json() + }) + } else { + json!({ + "status": "copied", + "oldPath": copied.old_path, + "newPath": copied.new_path, + "old": DiffFile::new(&copied.old).as_json(), + "new": DiffFile::new(&copied.new).as_json(), + "diff": DiffContent::new(&copied.diff).as_json() + }) + } + }, + } + }).collect::>(), + "stats": json!({ + "filesChanged": s.files_changed, + "insertions": s.insertions, + "deletions": s.deletions, + }), + }) + } +} + +pub(crate) struct CodeLocation<'a>(&'a cob::CodeLocation); + +impl<'a> CodeLocation<'a> { + pub fn new(location: &'a cob::CodeLocation) -> Self { + Self(location) + } + + pub fn as_json(&self) -> Value { + if let (Some(old), Some(new)) = (&self.0.old, &self.0.new) { + json!({ "commit": self.0.commit, "path": self.0.path, "old": code_range(old), "new": code_range(new) }) + } else if let (None, Some(new)) = (&self.0.old, &self.0.new) { + json!({ "commit": self.0.commit, "path": self.0.path, "new": code_range(new) }) + } else if let (Some(old), None) = (&self.0.old, &self.0.new) { + json!({ "commit": self.0.commit, "path": self.0.path, "old": code_range(old) }) + } else { + json!({ "commit": self.0.commit, "path": self.0.path }) + } + } +} + +fn code_range(range: &cob::CodeRange) -> Value { + match range { + cob::CodeRange::Lines { range } => json!({ "type": "lines", "range": range }), + cob::CodeRange::Chars { line, range } => { + json!({ "type": "chars", "line": line, "range": range }) + } + } +} + +pub(crate) struct DiffContent<'a>(&'a surf::diff::DiffContent); + +impl<'a> DiffContent<'a> { + pub fn new(value: &'a surf::diff::DiffContent) -> Self { + Self(value) + } + + pub fn as_json(&self) -> Value { + match self.0 { + surf::diff::DiffContent::Binary => json!({ "type": "binary" }), + surf::diff::DiffContent::Empty => json!({ "type": "empty" }), + surf::diff::DiffContent::Plain { hunks, stats, eof } => { + json!({ + "type": "plain", + "hunks": hunks.iter().map(|h| Hunk::new(&h).as_json()).collect::>(), + "stats": json!({ + "additions": stats.additions, + "deletions": stats.deletions + }), + "eof": match eof { + surf::diff::EofNewLine::OldMissing => "oldMissing", + surf::diff::EofNewLine::NewMissing => "newMissing", + surf::diff::EofNewLine::BothMissing => "bothMissing", + surf::diff::EofNewLine::NoneMissing => "noneMissing", + } + }) + } + } + } +} + +pub(crate) struct DiffFile<'a>(&'a surf::diff::DiffFile); + +impl<'a> DiffFile<'a> { + pub fn new(value: &'a surf::diff::DiffFile) -> Self { + Self(value) + } + + pub fn as_json(&self) -> Value { + json!({ "oid": self.0.oid, "mode": match self.0.mode { + surf::diff::FileMode::Blob => "blob", + surf::diff::FileMode::BlobExecutable => "blobExecutable", + surf::diff::FileMode::Tree => "tree", + surf::diff::FileMode::Link => "link", + surf::diff::FileMode::Commit => "commit", + } }) + } +} + +pub(crate) struct Modification<'a>(&'a surf::diff::Modification); + +impl<'a> Modification<'a> { + pub fn new(value: &'a surf::diff::Modification) -> Self { + Self(value) + } + + pub fn as_json(&self) -> Value { + match self.0 { + surf::diff::Modification::Addition(addition) => { + json!({ "type": "addition", "line": addition.line, "lineNo": addition.line_no }) + } + surf::diff::Modification::Deletion(deletion) => { + json!({ "type": "deletion", "line": deletion.line, "lineNo": deletion.line_no }) + } + surf::diff::Modification::Context { + line, + line_no_old, + line_no_new, + } => { + json!({ "type": "context", "line": line, "lineNoOld": line_no_old, "lineNoNew": line_no_new }) + } + } + } +} + +pub(crate) struct Hunk<'a>(&'a surf::diff::Hunk); + +impl<'a> Hunk<'a> { + pub fn new(value: &'a surf::diff::Hunk) -> Self { + Self(value) + } + + pub fn as_json(&self) -> Value { + json!({ + "header": self.0.header, + "lines": self.0.lines.iter().map(|line| + Modification::new(&line).as_json() + ).collect::>(), + "old": self.0.old, + "new": self.0.new, + }) + } +} diff --git a/radicle-httpd/src/api/json/thread.rs b/radicle-httpd/src/api/json/thread.rs new file mode 100644 index 000000000..f6305707a --- /dev/null +++ b/radicle-httpd/src/api/json/thread.rs @@ -0,0 +1,43 @@ +use serde_json::{json, Value}; + +use radicle::cob; +use radicle::cob::CodeLocation; +use radicle::git::Oid; +use radicle::node::AliasStore; + +use super::{diff, edit, embeds, reactions, Author}; + +pub(crate) enum Comment<'a> { + Patch(&'a cob::thread::Comment), + Issue(&'a cob::thread::Comment), +} + +impl<'a> Comment<'a> { + pub fn as_json(&self, id: &Oid, aliases: &impl AliasStore) -> Value { + match self { + Comment::Issue(c) => json!({ + "id": *id, + "author": Author::new(&c.author().into()).as_json(aliases), + "body": c.body(), + "edits": c.edits().map(|e| edit(e, aliases)).collect::>(), + "embeds": embeds(c.embeds()), + "reactions": reactions(c.reactions(), None, aliases), + "timestamp": c.timestamp().as_secs(), + "replyTo": c.reply_to(), + "resolved": c.is_resolved(), + }), + Comment::Patch(c) => json!({ + "id": *id, + "author": Author::new(&c.author().into()).as_json(aliases), + "body": c.body(), + "edits": c.edits().map(|e| edit(e, aliases)).collect::>(), + "embeds": embeds(c.embeds()), + "reactions": reactions(c.reactions(), None, aliases), + "timestamp": c.timestamp().as_secs(), + "replyTo": c.reply_to(), + "location": c.location().map(|l| diff::CodeLocation::new(&l).as_json()), + "resolved": c.is_resolved(), + }), + } + } +} diff --git a/radicle-httpd/src/api/v1/repos.rs b/radicle-httpd/src/api/v1/repos.rs index 776ecaabd..4191770ee 100644 --- a/radicle-httpd/src/api/v1/repos.rs +++ b/radicle-httpd/src/api/v1/repos.rs @@ -201,7 +201,7 @@ async fn history_handler( .filter_map(|commit| { let commit = commit.ok()?; let time = commit.committer.time.seconds(); - let commit = api::json::commit(&commit); + let commit = api::json::commit::Commit::new(&commit).as_json(); match (since, until) { (Some(since), Some(until)) if time >= since && time < until => Some(commit), (Some(since), None) if time >= since => Some(commit), @@ -279,8 +279,8 @@ async fn commit_handler( }); let response: serde_json::Value = json!({ - "commit": api::json::commit(&commit), - "diff": diff, + "commit": api::json::commit::Commit::new(&commit).as_json(), + "diff": api::json::diff::Diff::new(&diff).as_json(), "files": files, "branches": branches }); @@ -346,7 +346,7 @@ async fn diff_handler( false } }) - .map(|r| r.map(|c| api::json::commit(&c))) + .map(|r| r.map(|c| api::json::commit::Commit::new(&c).as_json())) .collect::, _>>()?; let response = json!({ "diff": diff, "files": files, "commits": commits }); @@ -409,7 +409,7 @@ async fn tree_handler( let repo = Repository::open(repo.path())?; let tree = repo.tree(sha, &path)?; - let response = api::json::tree(&tree, &path); + let response = api::json::commit::Tree::new(&tree).as_json(&path); if let Some(cache) = &ctx.cache { let cache = &mut cache.tree.lock().await; @@ -518,7 +518,9 @@ async fn blob_handler( .into_response(), ); } - Ok::<_, Error>(immutable_response(api::json::blob(&blob, &path)).into_response()) + Ok::<_, Error>( + immutable_response(api::json::commit::Blob::new(&blob).as_json(&path)).into_response(), + ) } /// Get repo readme. @@ -557,7 +559,8 @@ async fn readme_handler( } return Ok::<_, Error>( - immutable_response(api::json::blob(&blob, &path)).into_response(), + immutable_response(api::json::commit::Blob::new(&blob).as_json(&path)) + .into_response(), ); } } @@ -594,7 +597,7 @@ async fn issues_handler( let aliases = &ctx.profile.aliases(); let issues = issues .into_iter() - .map(|(id, issue)| api::json::issue(id, issue, aliases)) + .map(|(id, issue)| api::json::cobs::Issue::new(&issue).as_json(id, aliases)) .skip(page * per_page) .take(per_page) .collect::>(); @@ -616,7 +619,9 @@ async fn issue_handler( .ok_or(Error::NotFound)?; let aliases = ctx.profile.aliases(); - Ok::<_, Error>(Json(api::json::issue(issue_id.into(), issue, &aliases))) + Ok::<_, Error>(Json( + api::json::cobs::Issue::new(&issue).as_json(issue_id.into(), &aliases), + )) } /// Get repo patches list. @@ -647,7 +652,7 @@ async fn patches_handler( let aliases = ctx.profile.aliases(); let patches = patches .into_iter() - .map(|(id, patch)| api::json::patch(id, patch, &repo, &aliases)) + .map(|(id, patch)| api::json::cobs::Patch::new(&patch).as_json(id, &repo, &aliases)) .skip(page * per_page) .take(per_page) .collect::>(); @@ -666,9 +671,8 @@ async fn patch_handler( let patch = patches.get(&patch_id.into())?.ok_or(Error::NotFound)?; let aliases = ctx.profile.aliases(); - Ok::<_, Error>(Json(api::json::patch( + Ok::<_, Error>(Json(api::json::cobs::Patch::new(&patch).as_json( patch_id.into(), - patch, &repo, &aliases, )))