gitlab/api/projects/access_tokens/
create.rs1use std::collections::BTreeSet;
8
9use chrono::NaiveDate;
10use derive_builder::Builder;
11
12use crate::api::common::{AccessLevel, NameOrId};
13use crate::api::endpoint_prelude::*;
14use crate::api::ParamValue;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
18#[non_exhaustive]
19pub enum ProjectAccessTokenScope {
20 Api,
22 ReadApi,
24 ReadRepository,
26 WriteRepository,
28 ReadRegistry,
30 WriteRegistry,
32 CreateRunner,
34 AiFeatures,
36 K8sProxy,
38}
39
40impl ProjectAccessTokenScope {
41 pub(crate) fn as_str(self) -> &'static str {
43 match self {
44 Self::Api => "api",
45 Self::ReadApi => "read_api",
46 Self::ReadRepository => "read_repository",
47 Self::WriteRepository => "write_repository",
48 Self::ReadRegistry => "read_registry",
49 Self::WriteRegistry => "write_registry",
50 Self::CreateRunner => "create_runner",
51 Self::AiFeatures => "ai_features",
52 Self::K8sProxy => "k8s_proxy",
53 }
54 }
55}
56
57impl ParamValue<'static> for ProjectAccessTokenScope {
58 fn as_value(&self) -> Cow<'static, str> {
59 self.as_str().into()
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
65#[non_exhaustive]
66pub enum ProjectAccessTokenAccessLevel {
67 Guest,
69 Planner,
71 Reporter,
73 Developer,
75 Maintainer,
77 Owner,
79}
80
81impl From<ProjectAccessTokenAccessLevel> for AccessLevel {
82 fn from(p: ProjectAccessTokenAccessLevel) -> Self {
83 match p {
84 ProjectAccessTokenAccessLevel::Guest => Self::Guest,
85 ProjectAccessTokenAccessLevel::Planner => Self::Planner,
86 ProjectAccessTokenAccessLevel::Reporter => Self::Reporter,
87 ProjectAccessTokenAccessLevel::Developer => Self::Developer,
88 ProjectAccessTokenAccessLevel::Maintainer => Self::Maintainer,
89 ProjectAccessTokenAccessLevel::Owner => Self::Owner,
90 }
91 }
92}
93
94#[derive(Debug, Builder, Clone)]
96#[builder(setter(strip_option))]
97pub struct CreateProjectAccessToken<'a> {
98 #[builder(setter(into))]
100 project: NameOrId<'a>,
101 #[builder(setter(into))]
103 name: Cow<'a, str>,
104 #[builder(setter(name = "_scopes"), private)]
106 scopes: BTreeSet<ProjectAccessTokenScope>,
107
108 #[builder(default)]
110 access_level: Option<ProjectAccessTokenAccessLevel>,
111 #[builder(default)]
113 expires_at: Option<NaiveDate>,
114 #[builder(setter(into), default)]
116 description: Option<Cow<'a, str>>,
117}
118
119impl<'a> CreateProjectAccessToken<'a> {
120 pub fn builder() -> CreateProjectAccessTokenBuilder<'a> {
122 CreateProjectAccessTokenBuilder::default()
123 }
124}
125
126impl CreateProjectAccessTokenBuilder<'_> {
127 pub fn scope(&mut self, scope: ProjectAccessTokenScope) -> &mut Self {
129 self.scopes.get_or_insert_with(BTreeSet::new).insert(scope);
130 self
131 }
132
133 pub fn scopes<I>(&mut self, scopes: I) -> &mut Self
135 where
136 I: Iterator<Item = ProjectAccessTokenScope>,
137 {
138 self.scopes.get_or_insert_with(BTreeSet::new).extend(scopes);
139 self
140 }
141}
142
143impl Endpoint for CreateProjectAccessToken<'_> {
144 fn method(&self) -> Method {
145 Method::POST
146 }
147
148 fn endpoint(&self) -> Cow<'static, str> {
149 format!("projects/{}/access_tokens", self.project).into()
150 }
151
152 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, BodyError> {
153 let mut params = FormParams::default();
154
155 params
156 .push("name", &self.name)
157 .push_opt(
158 "access_level",
159 self.access_level.map(|a| AccessLevel::from(a).as_u64()),
160 )
161 .push_opt("expires_at", self.expires_at)
162 .push_opt("description", self.description.as_ref());
163
164 params.extend(self.scopes.iter().map(|&value| ("scopes[]", value)));
165
166 params.into_body()
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use chrono::NaiveDate;
173 use http::Method;
174
175 use crate::api::projects::access_tokens::{
176 CreateProjectAccessToken, CreateProjectAccessTokenBuilderError,
177 ProjectAccessTokenAccessLevel, ProjectAccessTokenScope,
178 };
179 use crate::api::{self, Query};
180 use crate::test::client::{ExpectedUrl, SingleTestClient};
181
182 #[test]
183 fn personal_access_token_create_scope_as_str() {
184 let items = &[
185 (ProjectAccessTokenScope::Api, "api"),
186 (ProjectAccessTokenScope::ReadApi, "read_api"),
187 (ProjectAccessTokenScope::ReadRepository, "read_repository"),
188 (ProjectAccessTokenScope::WriteRepository, "write_repository"),
189 (ProjectAccessTokenScope::ReadRegistry, "read_registry"),
190 (ProjectAccessTokenScope::WriteRegistry, "write_registry"),
191 (ProjectAccessTokenScope::CreateRunner, "create_runner"),
192 (ProjectAccessTokenScope::AiFeatures, "ai_features"),
193 (ProjectAccessTokenScope::K8sProxy, "k8s_proxy"),
194 ];
195
196 for (i, s) in items {
197 assert_eq!(i.as_str(), *s);
198 }
199 }
200
201 #[test]
202 fn project_token_access_level_from() {
203 use crate::api::common::AccessLevel;
204
205 let items = &[
206 (ProjectAccessTokenAccessLevel::Guest, AccessLevel::Guest),
207 (ProjectAccessTokenAccessLevel::Planner, AccessLevel::Planner),
208 (
209 ProjectAccessTokenAccessLevel::Reporter,
210 AccessLevel::Reporter,
211 ),
212 (
213 ProjectAccessTokenAccessLevel::Developer,
214 AccessLevel::Developer,
215 ),
216 (
217 ProjectAccessTokenAccessLevel::Maintainer,
218 AccessLevel::Maintainer,
219 ),
220 (ProjectAccessTokenAccessLevel::Owner, AccessLevel::Owner),
221 ];
222
223 for (i, s) in items {
224 assert_eq!(AccessLevel::from(*i), *s);
225 }
226 }
227
228 #[test]
229 fn project_name_and_scopes_are_necessary() {
230 let err = CreateProjectAccessToken::builder().build().unwrap_err();
231 crate::test::assert_missing_field!(err, CreateProjectAccessTokenBuilderError, "project");
232 }
233
234 #[test]
235 fn project_is_necessary() {
236 let err = CreateProjectAccessToken::builder()
237 .name("name")
238 .scope(ProjectAccessTokenScope::K8sProxy)
239 .build()
240 .unwrap_err();
241 crate::test::assert_missing_field!(err, CreateProjectAccessTokenBuilderError, "project");
242 }
243
244 #[test]
245 fn name_is_necessary() {
246 let err = CreateProjectAccessToken::builder()
247 .project(1)
248 .scope(ProjectAccessTokenScope::K8sProxy)
249 .build()
250 .unwrap_err();
251 crate::test::assert_missing_field!(err, CreateProjectAccessTokenBuilderError, "name");
252 }
253
254 #[test]
255 fn scopes_is_necessary() {
256 let err = CreateProjectAccessToken::builder()
257 .project(1)
258 .name("name")
259 .build()
260 .unwrap_err();
261 crate::test::assert_missing_field!(err, CreateProjectAccessTokenBuilderError, "scopes");
262 }
263
264 #[test]
265 fn project_name_and_scopes_are_sufficient() {
266 CreateProjectAccessToken::builder()
267 .project(1)
268 .name("name")
269 .scope(ProjectAccessTokenScope::K8sProxy)
270 .build()
271 .unwrap();
272 }
273
274 #[test]
275 fn endpoint() {
276 let endpoint = ExpectedUrl::builder()
277 .method(Method::POST)
278 .endpoint("projects/1/access_tokens")
279 .content_type("application/x-www-form-urlencoded")
280 .body_str(concat!("name=name", "&scopes%5B%5D=k8s_proxy"))
281 .build()
282 .unwrap();
283 let client = SingleTestClient::new_raw(endpoint, "");
284
285 let endpoint = CreateProjectAccessToken::builder()
286 .project(1)
287 .name("name")
288 .scopes([ProjectAccessTokenScope::K8sProxy].iter().cloned())
289 .build()
290 .unwrap();
291 api::ignore(endpoint).query(&client).unwrap();
292 }
293
294 #[test]
295 fn endpoint_access_level() {
296 let endpoint = ExpectedUrl::builder()
297 .method(Method::POST)
298 .endpoint("projects/1/access_tokens")
299 .content_type("application/x-www-form-urlencoded")
300 .body_str(concat!(
301 "name=name",
302 "&access_level=30",
303 "&scopes%5B%5D=k8s_proxy",
304 ))
305 .build()
306 .unwrap();
307 let client = SingleTestClient::new_raw(endpoint, "");
308
309 let endpoint = CreateProjectAccessToken::builder()
310 .project(1)
311 .name("name")
312 .scope(ProjectAccessTokenScope::K8sProxy)
313 .access_level(ProjectAccessTokenAccessLevel::Developer)
314 .build()
315 .unwrap();
316 api::ignore(endpoint).query(&client).unwrap();
317 }
318
319 #[test]
320 fn endpoint_expires_at() {
321 let endpoint = ExpectedUrl::builder()
322 .method(Method::POST)
323 .endpoint("projects/1/access_tokens")
324 .content_type("application/x-www-form-urlencoded")
325 .body_str(concat!(
326 "name=name",
327 "&expires_at=2022-01-01",
328 "&scopes%5B%5D=k8s_proxy",
329 ))
330 .build()
331 .unwrap();
332 let client = SingleTestClient::new_raw(endpoint, "");
333
334 let endpoint = CreateProjectAccessToken::builder()
335 .project(1)
336 .name("name")
337 .scope(ProjectAccessTokenScope::K8sProxy)
338 .expires_at(NaiveDate::from_ymd_opt(2022, 1, 1).unwrap())
339 .build()
340 .unwrap();
341 api::ignore(endpoint).query(&client).unwrap();
342 }
343
344 #[test]
345 fn endpoint_description() {
346 let endpoint = ExpectedUrl::builder()
347 .method(Method::POST)
348 .endpoint("projects/1/access_tokens")
349 .content_type("application/x-www-form-urlencoded")
350 .body_str(concat!(
351 "name=name",
352 "&description=a+description",
353 "&scopes%5B%5D=k8s_proxy",
354 ))
355 .build()
356 .unwrap();
357 let client = SingleTestClient::new_raw(endpoint, "");
358
359 let endpoint = CreateProjectAccessToken::builder()
360 .project(1)
361 .name("name")
362 .scope(ProjectAccessTokenScope::K8sProxy)
363 .description("a description")
364 .build()
365 .unwrap();
366 api::ignore(endpoint).query(&client).unwrap();
367 }
368}