git_repos/provider/
gitlab.rs

1use serde::Deserialize;
2
3use crate::{
4    provider::{escape, ApiErrorResponse, Filter, JsonError, Project, Provider},
5    token::AuthToken,
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) -> String {
39        self.name.clone()
40    }
41
42    fn namespace(&self) -> Option<String> {
43        if let Some((namespace, _name)) = self.path_with_namespace.rsplit_once('/') {
44            Some(namespace.to_string())
45        } else {
46            None
47        }
48    }
49
50    fn ssh_url(&self) -> String {
51        self.ssh_url_to_repo.clone()
52    }
53
54    fn http_url(&self) -> String {
55        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: AuthToken,
78    api_url_override: Option<String>,
79}
80
81impl Gitlab {
82    fn api_url(&self) -> String {
83        self.api_url_override
84            .as_ref()
85            .unwrap_or(&GITLAB_API_BASEURL.to_string())
86            .trim_end_matches('/')
87            .to_string()
88    }
89}
90
91impl Provider for Gitlab {
92    type Error = GitlabApiErrorResponse;
93    type Project = GitlabProject;
94
95    fn new(
96        filter: Filter,
97        secret_token: AuthToken,
98        api_url_override: Option<String>,
99    ) -> Result<Self, String> {
100        Ok(Self { filter, secret_token, api_url_override })
101    }
102
103    fn filter(&self) -> &Filter {
104        &self.filter
105    }
106
107    fn secret_token(&self) -> &AuthToken {
108        &self.secret_token
109    }
110
111    fn auth_header_key() -> &'static str {
112        "bearer"
113    }
114
115    fn get_user_projects(
116        &self,
117        user: &str,
118    ) -> Result<Vec<GitlabProject>, ApiErrorResponse<GitlabApiErrorResponse>> {
119        self.call_list(
120            &format!("{}/api/v4/users/{}/projects", self.api_url(), escape(user)),
121            Some(ACCEPT_HEADER_JSON),
122        )
123    }
124
125    fn get_group_projects(
126        &self,
127        group: &str,
128    ) -> Result<Vec<GitlabProject>, ApiErrorResponse<GitlabApiErrorResponse>> {
129        self.call_list(
130            &format!(
131                "{}/api/v4/groups/{}/projects?include_subgroups=true&archived=false",
132                self.api_url(),
133                escape(group),
134            ),
135            Some(ACCEPT_HEADER_JSON),
136        )
137    }
138
139    fn get_accessible_projects(
140        &self,
141    ) -> Result<Vec<GitlabProject>, ApiErrorResponse<GitlabApiErrorResponse>> {
142        self.call_list(&format!("{}/api/v4/projects", self.api_url(),), Some(ACCEPT_HEADER_JSON))
143    }
144
145    fn get_current_user(&self) -> Result<String, ApiErrorResponse<GitlabApiErrorResponse>> {
146        Ok(super::call::<GitlabUser, GitlabApiErrorResponse>(
147            &format!("{}/api/v4/user", self.api_url()),
148            Self::auth_header_key(),
149            self.secret_token(),
150            Some(ACCEPT_HEADER_JSON),
151        )?
152        .username)
153    }
154}