api.rs
1 use std::collections::BTreeMap; 2 use std::sync::Arc; 3 4 use axum::response::{IntoResponse, Json}; 5 use axum::routing::get; 6 use axum::Router; 7 use serde_json::{json, Value}; 8 9 use radicle::identity::doc::PayloadId; 10 use radicle::identity::{DocAt, RepoId}; 11 use radicle::issue::cache::Issues as _; 12 use radicle::node::routing::Store; 13 use radicle::node::NodeId; 14 use radicle::patch::cache::Patches as _; 15 use radicle::storage::git::Repository; 16 use radicle::storage::{ReadRepository, ReadStorage}; 17 use radicle::Profile; 18 19 mod error; 20 mod json; 21 pub(crate) mod query; 22 mod v1; 23 24 use crate::api::error::Error; 25 use crate::cache::Cache; 26 use crate::Options; 27 28 pub const RADICLE_VERSION: &str = env!("RADICLE_VERSION"); 29 // This version has to be updated on every breaking change to the radicle-httpd API. 30 pub const API_VERSION: &str = "6.1.0"; 31 32 #[derive(Clone)] 33 pub struct Context { 34 profile: Arc<Profile>, 35 cache: Option<Cache>, 36 } 37 38 impl Context { 39 pub fn new(profile: Arc<Profile>, options: &Options) -> Self { 40 Self { 41 profile, 42 cache: options.cache.map(Cache::new), 43 } 44 } 45 46 #[allow(clippy::result_large_err)] 47 pub fn repo_info<R: ReadRepository + radicle::cob::Store<Namespace = NodeId>>( 48 &self, 49 repo: &R, 50 doc: DocAt, 51 ) -> Result<repo::Info, error::Error> { 52 let DocAt { doc, .. } = doc; 53 let rid = repo.id(); 54 55 let aliases = self.profile.aliases(); 56 let delegates = doc 57 .delegates() 58 .iter() 59 .map(|did| json::Author::new(did).as_json(&aliases)) 60 .collect::<Vec<_>>(); 61 let db = &self.profile.database()?; 62 let seeding = db.count(&rid).unwrap_or_default(); 63 64 let payloads: BTreeMap<PayloadId, Value> = doc 65 .payload() 66 .iter() 67 .filter_map(|(id, payload)| { 68 if id == &PayloadId::project() { 69 let (_, head) = repo.head().ok()?; 70 let patches = self.profile.patches(repo).ok()?; 71 let patches = patches.counts().ok()?; 72 let issues = self.profile.issues(repo).ok()?; 73 let issues = issues.counts().ok()?; 74 75 Some(( 76 id.clone(), 77 json!({ 78 "data": payload, 79 "meta": { 80 "head": head, 81 "issues": issues, 82 "patches": patches 83 } 84 }), 85 )) 86 } else { 87 Some((id.clone(), json!({ "data": payload }))) 88 } 89 }) 90 .collect(); 91 92 Ok(repo::Info { 93 payloads, 94 delegates, 95 threshold: doc.threshold(), 96 visibility: doc.visibility().clone(), 97 rid, 98 seeding, 99 }) 100 } 101 102 /// Get a repository by RID, checking to make sure we're allowed to view it. 103 #[allow(clippy::result_large_err)] 104 pub fn repo(&self, rid: RepoId) -> Result<(Repository, DocAt), error::Error> { 105 let repo = self.profile.storage.repository(rid)?; 106 let doc = repo.identity_doc()?; 107 // Don't allow accessing private repos. 108 if doc.visibility().is_private() { 109 return Err(Error::NotFound); 110 } 111 Ok((repo, doc)) 112 } 113 114 #[cfg(test)] 115 pub fn profile(&self) -> &Arc<Profile> { 116 &self.profile 117 } 118 } 119 120 pub fn router(ctx: Context) -> Router { 121 Router::new() 122 .route("/", get(root_handler)) 123 .merge(v1::router(ctx)) 124 } 125 126 async fn root_handler() -> impl IntoResponse { 127 let response = json!({ 128 "path": "/api", 129 "links": [ 130 { 131 "href": "/v1", 132 "rel": "v1", 133 "type": "GET" 134 } 135 ] 136 }); 137 138 Json(response) 139 } 140 141 mod search { 142 use std::cmp::Ordering; 143 use std::collections::BTreeMap; 144 145 use serde::{Deserialize, Serialize}; 146 use serde_json::json; 147 148 use radicle::identity::doc::{Payload, PayloadId}; 149 use radicle::identity::RepoId; 150 use radicle::node::routing::Store; 151 use radicle::node::{AliasStore, Database}; 152 use radicle::profile::Aliases; 153 use radicle::storage::RepositoryInfo; 154 155 #[derive(Serialize, Deserialize)] 156 #[serde(rename_all = "camelCase")] 157 pub struct SearchQueryString { 158 pub q: Option<String>, 159 pub page: Option<usize>, 160 pub per_page: Option<usize>, 161 } 162 163 #[derive(Serialize, Deserialize, Eq, Debug)] 164 pub struct SearchResult { 165 pub rid: RepoId, 166 pub payloads: BTreeMap<PayloadId, Payload>, 167 pub delegates: Vec<serde_json::Value>, 168 pub seeds: usize, 169 #[serde(skip)] 170 pub index: usize, 171 } 172 173 impl SearchResult { 174 pub fn new( 175 q: &str, 176 info: RepositoryInfo, 177 db: &Database, 178 aliases: &Aliases, 179 ) -> Option<Self> { 180 if info.doc.visibility().is_private() { 181 return None; 182 } 183 let Ok(Some(index)) = info.doc.project().map(|p| p.name().find(q)) else { 184 return None; 185 }; 186 let seeds = db.count(&info.rid).unwrap_or_default(); 187 let delegates = info 188 .doc 189 .delegates() 190 .iter() 191 .map(|did| match aliases.alias(did) { 192 Some(alias) => json!({ 193 "id": did, 194 "alias": alias, 195 }), 196 None => json!({ 197 "id": did, 198 }), 199 }) 200 .collect::<Vec<_>>(); 201 202 Some(SearchResult { 203 rid: info.rid, 204 payloads: info.doc.payload().clone(), 205 delegates, 206 seeds, 207 index, 208 }) 209 } 210 } 211 212 impl Ord for SearchResult { 213 fn cmp(&self, other: &Self) -> Ordering { 214 match (self.index, other.index) { 215 (0, 0) => self.seeds.cmp(&other.seeds), 216 (0, _) => std::cmp::Ordering::Less, 217 (_, 0) => std::cmp::Ordering::Greater, 218 (ai, bi) if ai == bi => self.seeds.cmp(&other.seeds), 219 (_, _) => self.seeds.cmp(&other.seeds), 220 } 221 } 222 } 223 224 impl PartialOrd for SearchResult { 225 fn partial_cmp(&self, other: &Self) -> Option<Ordering> { 226 Some(self.cmp(other)) 227 } 228 } 229 230 impl PartialEq for SearchResult { 231 fn eq(&self, other: &Self) -> bool { 232 self.rid == other.rid 233 } 234 } 235 } 236 237 mod repo { 238 use std::collections::BTreeMap; 239 240 use serde::Serialize; 241 use serde_json::Value; 242 243 use radicle::identity::doc::PayloadId; 244 use radicle::identity::{RepoId, Visibility}; 245 246 /// Repos info. 247 #[derive(Serialize)] 248 #[serde(rename_all = "camelCase")] 249 pub struct Info { 250 pub payloads: BTreeMap<PayloadId, Value>, 251 pub delegates: Vec<Value>, 252 pub threshold: usize, 253 pub visibility: Visibility, 254 pub rid: RepoId, 255 pub seeding: usize, 256 } 257 }