/ radicle-httpd / src / api / json.rs
json.rs
  1  //! Utilities for building JSON responses of our API.
  2  
  3  use std::path::Path;
  4  use std::str;
  5  
  6  use base64::prelude::{Engine, BASE64_STANDARD};
  7  use serde::Serialize;
  8  use serde_json::{json, Value};
  9  
 10  use radicle::cob::issue::{Issue, IssueId};
 11  use radicle::cob::patch::Merge;
 12  use radicle::cob::patch::Review;
 13  use radicle::cob::patch::{Patch, PatchId};
 14  use radicle::cob::thread;
 15  use radicle::cob::thread::CommentId;
 16  use radicle::cob::{ActorId, Author, Embed, Reaction, Timestamp, Uri};
 17  use radicle::git::RefString;
 18  use radicle::node::{Alias, AliasStore};
 19  use radicle::prelude::NodeId;
 20  use radicle::storage::{git, refs, ReadRepository};
 21  use radicle_surf::blob::Blob;
 22  use radicle_surf::tree::Tree;
 23  use radicle_surf::{Commit, Oid, Stats};
 24  
 25  use crate::api::auth::Session;
 26  
 27  /// Returns JSON of a commit.
 28  pub(crate) fn commit(commit: &Commit) -> Value {
 29      json!({
 30        "id": commit.id,
 31        "author": {
 32          "name": commit.author.name,
 33          "email": commit.author.email
 34        },
 35        "summary": commit.summary,
 36        "description": commit.description(),
 37        "parents": commit.parents,
 38        "committer": {
 39          "name": commit.committer.name,
 40          "email": commit.committer.email,
 41          "time": commit.committer.time.seconds()
 42        }
 43      })
 44  }
 45  
 46  /// Returns JSON of a session.
 47  pub(crate) fn session(session_id: String, session: &Session) -> Value {
 48      json!({
 49        "sessionId": session_id,
 50        "status": session.status,
 51        "publicKey": session.public_key,
 52        "alias": session.alias,
 53        "issuedAt": session.issued_at.unix_timestamp(),
 54        "expiresAt": session.expires_at.unix_timestamp()
 55      })
 56  }
 57  
 58  /// Returns JSON for a blob with a given `path`.
 59  pub(crate) fn blob<T: AsRef<[u8]>>(blob: &Blob<T>, path: &str) -> Value {
 60      json!({
 61          "binary": blob.is_binary(),
 62          "name": name_in_path(path),
 63          "content": blob_content(blob),
 64          "path": path,
 65          "lastCommit": commit(blob.commit())
 66      })
 67  }
 68  
 69  /// Returns a string for the blob content, encoded in base64 if binary.
 70  pub fn blob_content<T: AsRef<[u8]>>(blob: &Blob<T>) -> String {
 71      match str::from_utf8(blob.content()) {
 72          Ok(s) => s.to_owned(),
 73          Err(_) => BASE64_STANDARD.encode(blob.content()),
 74      }
 75  }
 76  
 77  /// Returns JSON for a tree with a given `path` and `stats`.
 78  pub(crate) fn tree(tree: &Tree, path: &str, stats: &Stats) -> Value {
 79      let prefix = Path::new(path);
 80      let entries = tree
 81          .entries()
 82          .iter()
 83          .map(|entry| {
 84              json!({
 85                  "path": prefix.join(entry.name()),
 86                  "name": entry.name(),
 87                  "kind": if entry.is_tree() { "tree" } else { "blob" },
 88              })
 89          })
 90          .collect::<Vec<_>>();
 91  
 92      json!({
 93          "entries": &entries,
 94          "lastCommit": commit(tree.commit()),
 95          "name": name_in_path(path),
 96          "path": path,
 97          "stats": stats,
 98      })
 99  }
100  
101  /// Returns JSON for an `issue`.
102  pub(crate) fn issue(id: IssueId, issue: Issue, aliases: &impl AliasStore) -> Value {
103      json!({
104          "id": id.to_string(),
105          "author": author(&issue.author(), aliases.alias(issue.author().id())),
106          "title": issue.title(),
107          "state": issue.state(),
108          "assignees": issue.assigned().collect::<Vec<_>>(),
109          "discussion": issue
110            .comments()
111            .map(|(id, comment)| Comment::new(id, comment, aliases))
112            .collect::<Vec<_>>(),
113          "labels": issue.labels().collect::<Vec<_>>(),
114      })
115  }
116  
117  /// Returns JSON for a `patch`.
118  pub(crate) fn patch(
119      id: PatchId,
120      patch: Patch,
121      repo: &git::Repository,
122      aliases: &impl AliasStore,
123  ) -> Value {
124      json!({
125          "id": id.to_string(),
126          "author": author(patch.author(), aliases.alias(patch.author().id())),
127          "title": patch.title(),
128          "state": patch.state(),
129          "target": patch.target(),
130          "labels": patch.labels().collect::<Vec<_>>(),
131          "merges": patch.merges().map(|(nid, m)| merge(m, nid, aliases.alias(nid))).collect::<Vec<_>>(),
132          "assignees": patch.assignees().collect::<Vec<_>>(),
133          "revisions": patch.revisions().map(|(id, rev)| {
134              json!({
135                  "id": id,
136                  "author": author(rev.author(), aliases.alias(rev.author().id())),
137                  "description": rev.description(),
138                  "base": rev.base(),
139                  "oid": rev.head(),
140                  "refs": get_refs(repo, patch.author().id(), &rev.head()).unwrap_or(vec![]),
141                  "discussions": rev.discussion().comments()
142                    .map(|(id, comment)| Comment::new(id, comment,  aliases))
143                    .collect::<Vec<_>>(),
144                  "timestamp": rev.timestamp().as_secs(),
145                  "reviews": rev.reviews().map(|(nid, _review)| review(nid, aliases.alias(nid), _review)).collect::<Vec<_>>(),
146              })
147          }).collect::<Vec<_>>(),
148      })
149  }
150  
151  /// Returns JSON for an `author` and fills in `alias` when present.
152  fn author(author: &Author, alias: Option<Alias>) -> Value {
153      match alias {
154          Some(alias) => json!({
155              "id": author.id,
156              "alias": alias,
157          }),
158          None => json!(author),
159      }
160  }
161  
162  /// Returns JSON for a patch `Merge` and fills in `alias` when present.
163  fn merge(merge: &Merge, nid: &NodeId, alias: Option<Alias>) -> Value {
164      match alias {
165          Some(alias) => json!({
166              "author": {
167                  "id": nid,
168                  "alias": alias,
169              },
170              "commit": merge.commit,
171              "timestamp": merge.timestamp.as_secs(),
172              "revision": merge.revision,
173          }),
174          None => json!({
175              "author": {
176                  "id": nid,
177              },
178              "commit": merge.commit,
179              "timestamp": merge.timestamp.as_secs(),
180              "revision": merge.revision,
181          }),
182      }
183  }
184  
185  /// Returns JSON for a patch `Review` and fills in `alias` when present.
186  fn review(nid: &NodeId, alias: Option<Alias>, review: &Review) -> Value {
187      match alias {
188          Some(alias) => json!({
189              "author": {
190                  "id": nid,
191                  "alias": alias,
192              },
193              "verdict": review.verdict(),
194              "summary": review.summary(),
195              "comments": review.comments().collect::<Vec<_>>(),
196              "timestamp": review.timestamp().as_secs(),
197          }),
198          None => json!({
199              "author": {
200                  "id": nid,
201              },
202              "verdict": review.verdict(),
203              "summary": review.summary(),
204              "comments": review.comments().collect::<Vec<_>>(),
205              "timestamp": review.timestamp().as_secs(),
206          }),
207      }
208  }
209  
210  /// Returns the name part of a path string.
211  fn name_in_path(path: &str) -> &str {
212      match path.rsplit('/').next() {
213          Some(name) => name,
214          None => path,
215      }
216  }
217  
218  fn get_refs(
219      repo: &git::Repository,
220      id: &ActorId,
221      head: &Oid,
222  ) -> Result<Vec<RefString>, refs::Error> {
223      let remote = repo.remote(id)?;
224      let refs = remote
225          .refs
226          .iter()
227          .filter_map(|(name, o)| {
228              if o == head {
229                  Some(name.to_owned())
230              } else {
231                  None
232              }
233          })
234          .collect::<Vec<_>>();
235  
236      Ok(refs)
237  }
238  
239  #[derive(Serialize)]
240  #[serde(rename_all = "camelCase")]
241  struct Comment<'a> {
242      id: CommentId,
243      author: Value,
244      body: &'a str,
245      embeds: Vec<Embed<Uri>>,
246      reactions: Vec<(&'a ActorId, &'a Reaction)>,
247      #[serde(with = "radicle::serde_ext::localtime::time")]
248      timestamp: Timestamp,
249      reply_to: Option<CommentId>,
250      resolved: bool,
251  }
252  
253  impl<'a> Comment<'a> {
254      fn new(id: &'a CommentId, comment: &'a thread::Comment, aliases: &impl AliasStore) -> Self {
255          let comment_author = Author::new(comment.author());
256          Self {
257              id: *id,
258              author: author(&comment_author, aliases.alias(comment_author.id())),
259              body: comment.body(),
260              embeds: comment.embeds().to_vec(),
261              reactions: comment.reactions().collect::<Vec<_>>(),
262              timestamp: comment.timestamp(),
263              reply_to: comment.reply_to(),
264              resolved: comment.resolved(),
265          }
266      }
267  }