releasekit/platform/
gitlab.rs1use crate::client::{HeaderMap, HttpClient};
4use crate::error::{Error, Result};
5use crate::model::{Asset, Release};
6use crate::platform::Forge;
7use serde::Deserialize;
8
9pub struct GitLab<C: HttpClient> {
14 client: C,
15 token: Option<String>,
16 base_url: String,
17}
18
19impl<C: HttpClient> GitLab<C> {
20 pub fn new(client: C) -> Self {
25 Self {
26 client,
27 token: None,
28 base_url: "https://gitlab.com".to_string(),
29 }
30 }
31
32 pub fn with_base_url(mut self, url: impl Into<String>) -> Self {
34 self.base_url = url.into().trim_end_matches('/').to_string();
35 self
36 }
37
38 pub fn with_token(mut self, token: impl Into<String>) -> Self {
40 self.token = Some(token.into());
41 self
42 }
43
44 pub fn with_token_from_env(mut self, names: &[&str]) -> Self {
49 if self.token.is_none() {
50 for name in names {
51 if let Ok(val) = std::env::var(name) {
52 let trimmed = val.trim().to_string();
53 if !trimmed.is_empty() {
54 self.token = Some(trimmed);
55 break;
56 }
57 }
58 }
59 }
60 self
61 }
62
63 fn auth_headers(&self) -> HeaderMap {
64 let mut headers = HeaderMap::new();
65 if let Some(ref token) = self.token {
66 headers.insert("PRIVATE-TOKEN", token.trim());
67 }
68 headers
69 }
70}
71
72#[derive(Deserialize)]
73struct GlRelease {
74 name: Option<String>,
75 tag_name: String,
76 #[serde(default)]
77 upcoming_release: bool,
78 released_at: String,
79 description: Option<String>,
80 assets: GlAssets,
81}
82
83#[derive(Deserialize)]
84struct GlAssets {
85 links: Vec<GlLink>,
86}
87
88#[derive(Deserialize)]
89struct GlLink {
90 name: String,
91 direct_asset_url: String,
92}
93
94impl From<GlRelease> for Release {
95 fn from(g: GlRelease) -> Self {
96 Release {
97 name: g.name,
98 tag: g.tag_name,
99 prerelease: g.upcoming_release,
100 published_at: g.released_at,
101 body: g.description,
102 assets: g.assets.links.into_iter().map(Asset::from).collect(),
103 }
104 }
105}
106
107impl From<GlLink> for Asset {
108 fn from(l: GlLink) -> Self {
109 Asset {
110 name: l.name,
111 size: None,
112 url: l.direct_asset_url,
113 }
114 }
115}
116
117impl<C: HttpClient> Forge for GitLab<C> {
118 fn fetch_releases(&self, project: &str, tag: Option<&str>) -> Result<Vec<Release>> {
123 let project_ref = if project.chars().all(|c| c.is_ascii_digit()) {
124 project.to_string()
125 } else {
126 urlencoding::encode(project).into_owned()
127 };
128 let url = match tag {
129 Some(t) => {
130 let enc_tag = urlencoding::encode(t);
131 format!(
132 "{}/api/v4/projects/{project_ref}/releases/{enc_tag}",
133 self.base_url
134 )
135 }
136 None => format!("{}/api/v4/projects/{project_ref}/releases", self.base_url),
137 };
138
139 let resp = self.client.get(&url, &self.auth_headers())?;
140 let raw: serde_json::Value = serde_json::from_str(&resp.body)?;
141
142 let releases: Vec<GlRelease> = match raw {
143 serde_json::Value::Array(arr) => serde_json::from_value(serde_json::Value::Array(arr))?,
144 obj @ serde_json::Value::Object(_) => vec![serde_json::from_value(obj)?],
145 _ => return Err(Error::NoReleases),
146 };
147
148 Ok(releases.into_iter().map(Release::from).collect())
149 }
150}