grm/provider/
gitlab.rs

1use serde::Deserialize;
2
3use super::{
4    ApiError, Error, Filter, JsonError, Project, ProjectName, ProjectNamespace, Provider,
5    RemoteUrl, Url, auth, escape,
6};
7
8const ACCEPT_HEADER_JSON: &str = "application/json";
9const GITLAB_API_BASEURL: &str = match option_env!("GITLAB_API_BASEURL") {
10    Some(url) => url,
11    None => "https://gitlab.com",
12};
13
14#[derive(Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum GitlabVisibility {
17    Private,
18    Internal,
19    Public,
20}
21
22#[derive(Deserialize)]
23pub struct GitlabProject {
24    #[serde(rename = "path")]
25    pub name: String,
26    pub path_with_namespace: String,
27    pub http_url_to_repo: String,
28    pub ssh_url_to_repo: String,
29    pub visibility: GitlabVisibility,
30}
31
32#[derive(Deserialize)]
33struct GitlabUser {
34    pub username: String,
35}
36
37impl Project for GitlabProject {
38    fn name(&self) -> ProjectName {
39        ProjectName::new(self.name.clone())
40    }
41
42    fn namespace(&self) -> Option<ProjectNamespace> {
43        if let Some((namespace, _name)) = self.path_with_namespace.rsplit_once('/') {
44            Some(ProjectNamespace::new(namespace.to_owned()))
45        } else {
46            None
47        }
48    }
49
50    fn ssh_url(&self) -> RemoteUrl {
51        RemoteUrl::new(self.ssh_url_to_repo.clone())
52    }
53
54    fn http_url(&self) -> RemoteUrl {
55        RemoteUrl::new(self.http_url_to_repo.clone())
56    }
57
58    fn private(&self) -> bool {
59        !matches!(self.visibility, GitlabVisibility::Public)
60    }
61}
62
63#[derive(Deserialize)]
64pub struct GitlabApiErrorResponse {
65    #[serde(alias = "error_description", alias = "error")]
66    pub message: String,
67}
68
69impl JsonError for GitlabApiErrorResponse {
70    fn to_string(self) -> String {
71        self.message
72    }
73}
74
75pub struct Gitlab {
76    filter: Filter,
77    secret_token: auth::AuthToken,
78    api_url_override: Option<Url>,
79}
80
81impl Gitlab {
82    fn api_url(&self) -> Url {
83        Url::new(
84            self.api_url_override
85                .as_ref()
86                .map(Url::as_str)
87                .unwrap_or(GITLAB_API_BASEURL)
88                .trim_end_matches('/')
89                .to_owned(),
90        )
91    }
92}
93
94impl Provider for Gitlab {
95    type Error = GitlabApiErrorResponse;
96    type Project = GitlabProject;
97
98    fn new(
99        filter: Filter,
100        secret_token: auth::AuthToken,
101        api_url_override: Option<Url>,
102    ) -> Result<Self, Error> {
103        Ok(Self {
104            filter,
105            secret_token,
106            api_url_override,
107        })
108    }
109
110    fn filter(&self) -> &Filter {
111        &self.filter
112    }
113
114    fn secret_token(&self) -> &auth::AuthToken {
115        &self.secret_token
116    }
117
118    fn auth_header_key() -> &'static str {
119        "bearer"
120    }
121
122    fn get_user_projects(
123        &self,
124        user: &super::User,
125    ) -> Result<Vec<GitlabProject>, ApiError<GitlabApiErrorResponse>> {
126        self.call_list(
127            &Url::new(format!(
128                "{}/api/v4/users/{}/projects",
129                self.api_url().as_str(),
130                escape(&user.0)
131            )),
132            Some(ACCEPT_HEADER_JSON),
133        )
134    }
135
136    fn get_group_projects(
137        &self,
138        group: &super::Group,
139    ) -> Result<Vec<GitlabProject>, ApiError<GitlabApiErrorResponse>> {
140        self.call_list(
141            &Url::new(format!(
142                "{}/api/v4/groups/{}/projects?include_subgroups=true&archived=false",
143                self.api_url().as_str(),
144                escape(&group.0),
145            )),
146            Some(ACCEPT_HEADER_JSON),
147        )
148    }
149
150    fn get_accessible_projects(
151        &self,
152    ) -> Result<Vec<GitlabProject>, ApiError<GitlabApiErrorResponse>> {
153        self.call_list(
154            &Url::new(format!("{}/api/v4/projects", self.api_url().as_str())),
155            Some(ACCEPT_HEADER_JSON),
156        )
157    }
158
159    fn get_current_user(&self) -> Result<super::User, ApiError<GitlabApiErrorResponse>> {
160        Ok(super::User(
161            super::call::<GitlabUser, GitlabApiErrorResponse>(
162                &format!("{}/api/v4/user", self.api_url().as_str()),
163                Self::auth_header_key(),
164                self.secret_token(),
165                Some(ACCEPT_HEADER_JSON),
166            )?
167            .username,
168        ))
169    }
170}