facto_core/forges/
codeberg.rs1use std::future::Future;
2use std::pin::Pin;
3
4use crate::forges::{Forge, ForgeError, ForgeResult};
5use crate::models::*;
6
7pub struct Codeberg {
8 client: reqwest::Client,
9}
10
11impl Codeberg {
12 pub fn new(client: reqwest::Client) -> Self {
13 Self { client }
14 }
15}
16
17#[derive(serde::Deserialize)]
18struct CodebergRelease {
19 tag_name: String,
20 name: Option<String>,
21 published_at: Option<String>,
22 prerelease: bool,
23 draft: bool,
24 html_url: Option<String>,
25 assets: Vec<CodebergAsset>,
26}
27
28#[derive(serde::Deserialize)]
29struct CodebergAsset {
30 name: String,
31 size: u64,
32 browser_download_url: String,
33 download_count: u64,
34 #[serde(default)]
35 content_type: String,
36}
37
38#[derive(serde::Deserialize)]
39struct CodebergSearchResponse {
40 data: Vec<CodebergRepo>,
41}
42
43#[derive(serde::Deserialize)]
44struct CodebergRepo {
45 full_name: String,
46 name: String,
47 owner: CodebergRepoOwner,
48 description: Option<String>,
49 stars_count: u64,
50 language: Option<String>,
51 #[serde(default)]
52 topics: Vec<String>,
53 html_url: String,
54 updated_at: Option<String>,
55}
56
57#[derive(serde::Deserialize)]
58struct CodebergRepoOwner {
59 login: String,
60}
61
62impl Forge for Codeberg {
63 fn id(&self) -> &str {
64 "codeberg"
65 }
66
67 fn display_name(&self) -> &str {
68 "Codeberg"
69 }
70
71 fn supports_releases(&self) -> bool {
72 true
73 }
74
75 fn list_releases<'a>(
76 &'a self,
77 owner: &'a str,
78 repo: &'a str,
79 limit: usize,
80 ) -> Pin<Box<dyn Future<Output = ForgeResult<Vec<ReleaseInfo>>> + Send + 'a>> {
81 Box::pin(async move {
82 let url = format!(
83 "https://codeberg.org/api/v1/repos/{}/{}/releases?limit={}",
84 urlencoding::encode(owner),
85 urlencoding::encode(repo),
86 limit
87 );
88 let resp = self.client.get(&url).send().await?;
89
90 match resp.status().as_u16() {
91 200 => {
92 let releases: Vec<CodebergRelease> = crate::forges::bounded_json(resp).await?;
93 Ok(releases
94 .into_iter()
95 .map(|r| ReleaseInfo {
96 tag: r.tag_name,
97 name: r.name,
98 published_at: r.published_at.as_deref().and_then(|ts| ts.parse().ok()),
99 prerelease: r.prerelease,
100 draft: r.draft,
101 html_url: r.html_url.unwrap_or_default(),
102 assets: r
103 .assets
104 .into_iter()
105 .map(|a| ReleaseAsset {
106 name: a.name,
107 size: a.size,
108 download_url: a.browser_download_url,
109 download_count: a.download_count,
110 content_type: a.content_type,
111 })
112 .collect(),
113 })
114 .collect())
115 }
116 404 => Ok(Vec::new()),
117 429 => Err(ForgeError::RateLimited),
118 status => Err(ForgeError::Parse(format!("unexpected status {}", status))),
119 }
120 })
121 }
122
123 fn supports_search(&self) -> bool {
124 true
125 }
126
127 fn search_repositories<'a>(
128 &'a self,
129 query: &'a str,
130 sort: RepoSort,
131 _language: Option<&'a str>,
132 limit: usize,
133 ) -> Pin<Box<dyn Future<Output = ForgeResult<Vec<RepositorySearchResult>>> + Send + 'a>> {
134 Box::pin(async move {
135 let mut url = format!(
136 "https://codeberg.org/api/v1/repos/search?q={}&limit={}",
137 urlencoding::encode(query),
138 limit.min(50),
139 );
140 match sort {
142 RepoSort::Stars => url.push_str("&sort=stars&order=desc"),
143 RepoSort::Updated => url.push_str("&sort=updated&order=desc"),
144 RepoSort::Relevance => {}
145 }
146 let resp = self.client.get(&url).send().await?;
147 match resp.status().as_u16() {
148 200 => {}
149 429 => return Err(ForgeError::RateLimited),
150 status => return Err(ForgeError::Parse(format!("unexpected status {status}"))),
151 }
152 let data: CodebergSearchResponse = crate::forges::bounded_json(resp).await?;
153 Ok(data
154 .data
155 .into_iter()
156 .map(|r| RepositorySearchResult {
157 forge: "codeberg".into(),
158 full_name: r.full_name,
159 owner: r.owner.login,
160 name: r.name,
161 description: r.description,
162 stars: r.stars_count,
163 language: r.language,
164 topics: r.topics,
165 url: r.html_url,
166 updated_at: r.updated_at.as_deref().and_then(|ts| ts.parse().ok()),
167 })
168 .collect())
169 })
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[tokio::test]
178 #[ignore] async fn test_codeberg_list_releases() {
180 let cb = Codeberg::new(crate::http::default_client().unwrap());
181 let releases = cb.list_releases("forgejo", "forgejo", 5).await.unwrap();
182 assert!(!releases.is_empty());
183 assert!(releases.len() <= 5);
184 }
185
186 #[tokio::test]
187 #[ignore] async fn test_search_repositories() {
189 let cb = Codeberg::new(crate::http::default_client().unwrap());
190 let results = cb
191 .search_repositories("forgejo", RepoSort::Stars, None, 5)
192 .await
193 .unwrap();
194 assert!(!results.is_empty());
195 assert!(results.len() <= 5);
196 assert_eq!(results[0].forge, "codeberg");
197 }
198}