1use crate::api_traits::{ApiOperation, ProjectMember, RemoteProject, RemoteTag};
2use crate::cli::browse::BrowseOptions;
3use crate::cmds::project::{Member, Project, ProjectListBodyArgs, Tag};
4use crate::error::GRError;
5use crate::gitlab::encode_path;
6use crate::io::{CmdInfo, HttpResponse, HttpRunner};
7use crate::remote::query;
8use crate::remote::URLQueryParamBuilder;
9use crate::Result;
10
11use super::Gitlab;
12
13impl<R: HttpRunner<Response = HttpResponse>> RemoteProject for Gitlab<R> {
14 fn get_project_data(&self, id: Option<i64>, path: Option<&str>) -> Result<CmdInfo> {
15 let url = match (id, path) {
16 (Some(id), None) => format!("{}/{}", self.base_project_url, id),
17 (None, Some(path)) => {
18 format!("{}/{}", self.base_project_url, encode_path(path))
19 }
20 (None, None) => self.rest_api_basepath().to_string(),
21 (Some(_), Some(_)) => {
22 return Err(GRError::ApplicationError(
23 "Invalid arguments, can only get project data by id or by owner/repo path"
24 .to_string(),
25 )
26 .into());
27 }
28 };
29 let project = query::get::<_, (), _>(
30 &self.runner,
31 &url,
32 None,
33 self.headers(),
34 ApiOperation::Project,
35 |value| GitlabProjectFields::from(value).into(),
36 )?;
37 Ok(CmdInfo::Project(project))
38 }
39
40 fn get_project_members(&self) -> Result<CmdInfo> {
41 let url = format!("{}/members/all", self.rest_api_basepath());
42 let members = query::paged(
43 &self.runner,
44 &url,
45 None,
46 self.headers(),
47 None,
48 ApiOperation::Project,
49 |value| GitlabMemberFields::from(value).into(),
50 )?;
51 Ok(CmdInfo::Members(members))
52 }
53
54 fn get_url(&self, option: BrowseOptions) -> String {
55 let base_url = format!("https://{}/{}", self.domain, self.path);
56 match option {
57 BrowseOptions::Repo => base_url,
58 BrowseOptions::MergeRequests => format!("{base_url}/merge_requests"),
59 BrowseOptions::MergeRequestId(id) => format!("{base_url}/-/merge_requests/{id}"),
60 BrowseOptions::Pipelines => format!("{base_url}/pipelines"),
61 BrowseOptions::PipelineId(id) => format!("{base_url}/-/pipelines/{id}"),
62 BrowseOptions::Releases => format!("{base_url}/-/releases"),
63 BrowseOptions::Manual => unreachable!(),
66 }
67 }
68
69 fn list(&self, args: ProjectListBodyArgs) -> Result<Vec<Project>> {
70 let url = self.list_project_url(&args, false);
71 let projects = query::paged(
72 &self.runner,
73 &url,
74 args.from_to_page,
75 self.headers(),
76 None,
77 ApiOperation::Project,
78 |value| GitlabProjectFields::from(value).into(),
79 )?;
80 Ok(projects)
81 }
82
83 fn num_pages(&self, args: ProjectListBodyArgs) -> Result<Option<u32>> {
84 let url = self.list_project_url(&args, true);
85 query::num_pages(&self.runner, &url, self.headers(), ApiOperation::Project)
86 }
87
88 fn num_resources(
89 &self,
90 args: ProjectListBodyArgs,
91 ) -> Result<Option<crate::api_traits::NumberDeltaErr>> {
92 let url = self.list_project_url(&args, true);
93 query::num_resources(&self.runner, &url, self.headers(), ApiOperation::Project)
94 }
95}
96
97impl<R: HttpRunner<Response = HttpResponse>> RemoteTag for Gitlab<R> {
98 fn list(&self, args: ProjectListBodyArgs) -> Result<Vec<Tag>> {
100 let url = format!("{}/repository/tags", self.projects_base_url);
101 let tags = query::paged(
102 &self.runner,
103 &url,
104 args.from_to_page,
105 self.headers(),
106 None,
107 ApiOperation::RepositoryTag,
108 |value| GitlabProjectTagFields::from(value).into(),
109 )?;
110 Ok(tags)
111 }
112 }
118
119impl<R: HttpRunner<Response = HttpResponse>> ProjectMember for Gitlab<R> {
120 fn list(&self, args: ProjectListBodyArgs) -> Result<Vec<Member>> {
121 let url = format!("{}/members/all", self.rest_api_basepath());
122 let members = query::paged(
123 &self.runner,
124 &url,
125 args.from_to_page,
126 self.headers(),
127 None,
128 ApiOperation::Project,
129 |value| GitlabMemberFields::from(value).into(),
130 )?;
131 Ok(members)
132 }
133}
134
135impl<R> Gitlab<R> {
136 fn list_project_url(&self, args: &ProjectListBodyArgs, num_pages: bool) -> String {
137 let mut url = if args.tags {
138 URLQueryParamBuilder::new(&format!("{}/repository/tags", self.projects_base_url))
139 } else if args.members {
140 URLQueryParamBuilder::new(&format!("{}/members/all", self.projects_base_url))
141 } else {
142 let user = args.user.as_ref().unwrap().clone();
143 if args.stars {
144 URLQueryParamBuilder::new(&format!(
145 "{}/{}/starred_projects",
146 self.base_users_url, user.id
147 ))
148 } else {
149 URLQueryParamBuilder::new(&format!("{}/{}/projects", self.base_users_url, user.id))
150 }
151 };
152 if num_pages {
153 return url.add_param("page", "1").build();
154 }
155 url.build()
156 }
157}
158
159pub struct GitlabProjectTagFields {
160 tag: Tag,
161}
162
163impl From<&serde_json::Value> for GitlabProjectTagFields {
164 fn from(data: &serde_json::Value) -> Self {
165 GitlabProjectTagFields {
166 tag: Tag::builder()
167 .name(data["name"].as_str().unwrap().to_string())
168 .sha(data["commit"]["id"].as_str().unwrap().to_string())
169 .created_at(data["created_at"].as_str().unwrap().to_string())
170 .build()
171 .unwrap(),
172 }
173 }
174}
175
176impl From<GitlabProjectTagFields> for Tag {
177 fn from(fields: GitlabProjectTagFields) -> Self {
178 fields.tag
179 }
180}
181
182pub struct GitlabProjectFields {
183 project: Project,
184}
185
186impl From<&serde_json::Value> for GitlabProjectFields {
187 fn from(data: &serde_json::Value) -> Self {
188 GitlabProjectFields {
189 project: Project::builder()
190 .id(data["id"].as_i64().unwrap())
191 .default_branch(data["default_branch"].as_str().unwrap().to_string())
192 .html_url(data["web_url"].as_str().unwrap().to_string())
193 .created_at(data["created_at"].as_str().unwrap().to_string())
194 .description(data["description"].as_str().unwrap_or_default().to_string())
195 .build()
197 .unwrap(),
198 }
199 }
200}
201
202impl From<GitlabProjectFields> for Project {
203 fn from(fields: GitlabProjectFields) -> Self {
204 fields.project
205 }
206}
207
208pub struct GitlabMemberFields {
209 member: Member,
210}
211
212impl From<&serde_json::Value> for GitlabMemberFields {
213 fn from(data: &serde_json::Value) -> Self {
214 GitlabMemberFields {
215 member: Member::builder()
216 .id(data["id"].as_i64().unwrap())
217 .name(data["name"].as_str().unwrap().to_string())
218 .username(data["username"].as_str().unwrap().to_string())
219 .created_at(data["created_at"].as_str().unwrap().to_string())
220 .build()
221 .unwrap(),
222 }
223 }
224}
225
226impl From<GitlabMemberFields> for Member {
227 fn from(fields: GitlabMemberFields) -> Self {
228 fields.member
229 }
230}
231
232#[cfg(test)]
233mod test {
234
235 use crate::api_traits::ApiOperation;
236 use crate::cmds::project::ProjectListBodyArgs;
237 use crate::http::Headers;
238 use crate::setup_client;
239 use crate::test::utils::{
240 default_gitlab, get_contract, BasePath, ClientType, ContractType, Domain, ResponseContracts,
241 };
242
243 use crate::io::CmdInfo;
244
245 use super::*;
246
247 #[test]
248 fn test_get_project_data_no_id() {
249 let contracts =
250 ResponseContracts::new(ContractType::Gitlab).add_contract(200, "project.json", None);
251 let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn RemoteProject);
252 gitlab.get_project_data(None, None).unwrap();
253 assert_eq!(
254 "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi",
255 client.url().to_string(),
256 );
257 assert_eq!("1234", client.headers().get("PRIVATE-TOKEN").unwrap());
258 assert_eq!(Some(ApiOperation::Project), *client.api_operation.borrow());
259 }
260
261 #[test]
262 fn test_get_project_data_with_given_id() {
263 let contracts =
264 ResponseContracts::new(ContractType::Gitlab).add_contract(200, "project.json", None);
265 let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn RemoteProject);
266 gitlab.get_project_data(Some(54345), None).unwrap();
267 assert_eq!(
268 "https://gitlab.com/api/v4/projects/54345",
269 client.url().to_string(),
270 );
271 assert_eq!("1234", client.headers().get("PRIVATE-TOKEN").unwrap());
272 assert_eq!(Some(ApiOperation::Project), *client.api_operation.borrow());
273 }
274
275 #[test]
276 fn test_get_project_data_given_owner_repo_path() {
277 let path = "gitlab-org/gitlab-foss";
279 let client_type =
280 ClientType::Gitlab(Domain("gitlab.com".to_string()), BasePath(path.to_string()));
281 let contracts =
282 ResponseContracts::new(ContractType::Gitlab).add_contract(200, "project.json", None);
283 let (client, gitlab) = setup_client!(contracts, client_type, dyn RemoteProject);
284 let result = gitlab.get_project_data(None, Some("jordilin/gitlapi"));
286 assert_eq!(
287 "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi",
288 client.url().to_string(),
289 );
290 match result {
291 Ok(CmdInfo::Project(project)) => {
292 assert_eq!(44438708, project.id);
293 }
294 _ => panic!("Expected project"),
295 }
296 }
297
298 #[test]
299 fn test_get_project_data_error_if_both_id_and_path_given() {
300 let contracts = ResponseContracts::new(ContractType::Gitlab);
301 let (_, gitlab) = setup_client!(contracts, default_gitlab(), dyn RemoteProject);
302 let result = gitlab.get_project_data(Some(54345), Some("jordilin/gitlapi"));
303 match result {
304 Err(err) => match err.downcast_ref::<GRError>() {
305 Some(GRError::ApplicationError(msg)) => {
306 assert_eq!(
307 "Invalid arguments, can only get project data by id or by owner/repo path",
308 msg
309 );
310 }
311 _ => panic!("Expected ApplicationError"),
312 },
313 _ => panic!("Expected ApplicationError"),
314 }
315 }
316
317 #[test]
318 fn test_get_project_members() {
319 let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
320 200,
321 "project_members.json",
322 None,
323 );
324 let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn RemoteProject);
325 let CmdInfo::Members(members) = gitlab.get_project_members().unwrap() else {
326 panic!("Expected members");
327 };
328 assert_eq!(2, members.len());
329 assert_eq!("test_user_0", members[0].username);
330 assert_eq!("test_user_1", members[1].username);
331 assert_eq!("1234", client.headers().get("PRIVATE-TOKEN").unwrap());
332 assert_eq!(
333 "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/members/all",
334 *client.url(),
335 );
336 assert_eq!(Some(ApiOperation::Project), *client.api_operation.borrow());
337 }
338
339 #[test]
340 fn test_list_user_projects() {
341 let contracts = ResponseContracts::new(ContractType::Gitlab).add_body(
342 200,
343 Some(format!(
344 "[{}]",
345 get_contract(ContractType::Gitlab, "project.json")
346 )),
347 None,
348 );
349 let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn RemoteProject);
350 let body_args = ProjectListBodyArgs::builder()
351 .from_to_page(None)
352 .user(Some(
353 Member::builder()
354 .id(1)
355 .name("jordi".to_string())
356 .username("jordilin".to_string())
357 .build()
358 .unwrap(),
359 ))
360 .build()
361 .unwrap();
362 gitlab.list(body_args).unwrap();
363 assert_eq!(
364 "https://gitlab.com/api/v4/users/1/projects",
365 client.url().to_string(),
366 );
367 assert_eq!("1234", client.headers().get("PRIVATE-TOKEN").unwrap());
368 assert_eq!(Some(ApiOperation::Project), *client.api_operation.borrow());
369 }
370
371 #[test]
372 fn test_get_my_starred_projects() {
373 let contracts =
374 ResponseContracts::new(ContractType::Gitlab).add_contract(200, "stars.json", None);
375 let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn RemoteProject);
376 let body_args = ProjectListBodyArgs::builder()
377 .from_to_page(None)
378 .user(Some(
379 Member::builder()
380 .id(1)
381 .name("jordi".to_string())
382 .username("jordilin".to_string())
383 .build()
384 .unwrap(),
385 ))
386 .stars(true)
387 .build()
388 .unwrap();
389 gitlab.list(body_args).unwrap();
390 assert_eq!(
391 "https://gitlab.com/api/v4/users/1/starred_projects",
392 client.url().to_string(),
393 );
394 assert_eq!("1234", client.headers().get("PRIVATE-TOKEN").unwrap());
395 assert_eq!(Some(ApiOperation::Project), *client.api_operation.borrow());
396 }
397
398 #[test]
399 fn test_get_num_pages_url_for_user_projects() {
400 let link_headers = "<https://gitlab.com/api/v4/users/1/projects?page=2&per_page=20>; rel=\"next\", <https://gitlab.com/api/v4/users/1/projects?page=2&per_page=20>; rel=\"last\"";
401 let mut headers = Headers::new();
402 headers.set("link", link_headers);
403 let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
404 200,
405 "project.json",
406 Some(headers),
407 );
408 let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn RemoteProject);
409 let body_args = ProjectListBodyArgs::builder()
410 .from_to_page(None)
411 .user(Some(
412 Member::builder()
413 .id(1)
414 .name("jordi".to_string())
415 .username("jordilin".to_string())
416 .build()
417 .unwrap(),
418 ))
419 .build()
420 .unwrap();
421 gitlab.num_pages(body_args).unwrap();
422 assert_eq!(
423 "https://gitlab.com/api/v4/users/1/projects?page=1",
424 client.url().to_string(),
425 );
426 assert_eq!(
427 ApiOperation::Project,
428 *client.api_operation.borrow().as_ref().unwrap()
429 );
430 }
431
432 #[test]
433 fn test_get_project_num_pages_url_for_starred() {
434 let link_headers = "<https://gitlab.com/api/v4/users/1/starred_projects?page=2&per_page=20>; rel=\"next\", <https://gitlab.com/api/v4/users/1/starred_projects?page=2&per_page=20>; rel=\"last\"";
435 let mut headers = Headers::new();
436 headers.set("link", link_headers);
437 let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
438 200,
439 "project.json",
440 Some(headers),
441 );
442 let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn RemoteProject);
443 let body_args = ProjectListBodyArgs::builder()
444 .from_to_page(None)
445 .user(Some(
446 Member::builder()
447 .id(1)
448 .name("jordi".to_string())
449 .username("jordilin".to_string())
450 .build()
451 .unwrap(),
452 ))
453 .stars(true)
454 .build()
455 .unwrap();
456 gitlab.num_pages(body_args).unwrap();
457 assert_eq!(
458 "https://gitlab.com/api/v4/users/1/starred_projects?page=1",
459 client.url().to_string(),
460 );
461 assert_eq!(
462 ApiOperation::Project,
463 *client.api_operation.borrow().as_ref().unwrap()
464 );
465 }
466
467 #[test]
468 fn test_get_url_pipeline_id() {
469 let contracts = ResponseContracts::new(ContractType::Gitlab);
470 let (_, github) = setup_client!(contracts, default_gitlab(), dyn RemoteProject);
471 let url = github.get_url(BrowseOptions::PipelineId(9527070386));
472 assert_eq!(
473 "https://gitlab.com/jordilin/gitlapi/-/pipelines/9527070386",
474 url
475 );
476 }
477
478 #[test]
479 fn test_list_tags() {
480 let contracts =
481 ResponseContracts::new(ContractType::Gitlab).add_contract(200, "list_tags.json", None);
482 let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn RemoteTag);
483 let body_args = ProjectListBodyArgs::builder()
484 .user(None)
485 .from_to_page(None)
486 .tags(true)
487 .build()
488 .unwrap();
489 RemoteTag::list(&*gitlab, body_args).unwrap();
490 assert_eq!(
491 "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/repository/tags",
492 *client.url()
493 );
494 assert_eq!(
495 ApiOperation::RepositoryTag,
496 *client.api_operation.borrow().as_ref().unwrap()
497 );
498 }
499
500 #[test]
501 fn test_get_project_tags_num_pages() {
502 let link_header = "<https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/repository/tags?page=2&per_page=20>; rel=\"next\", <https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/repository/tags?page=2&per_page=20>; rel=\"last\"";
503 let mut headers = Headers::new();
504 headers.set("link", link_header);
505 let contracts = ResponseContracts::new(ContractType::Gitlab).add_body::<String>(
506 200,
507 None,
508 Some(headers),
509 );
510 let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn RemoteTag);
511 let body_args = ProjectListBodyArgs::builder()
512 .user(None)
513 .from_to_page(None)
514 .tags(true)
515 .build()
516 .unwrap();
517 gitlab.num_pages(body_args).unwrap();
518 assert_eq!(
519 "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/repository/tags?page=1",
520 *client.url()
521 );
522 }
523
524 #[test]
525 fn test_list_project_members() {
526 let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
527 200,
528 "project_members.json",
529 None,
530 );
531 let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn ProjectMember);
532 let args = ProjectListBodyArgs::builder()
533 .members(true)
534 .user(None)
535 .from_to_page(None)
536 .build()
537 .unwrap();
538 let members = ProjectMember::list(&*gitlab, args).unwrap();
539 assert_eq!(2, members.len());
540 assert_eq!("test_user_0", members[0].username);
541 assert_eq!("test_user_1", members[1].username);
542 assert_eq!("1234", client.headers().get("PRIVATE-TOKEN").unwrap());
543 assert_eq!(
544 "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/members/all",
545 *client.url(),
546 );
547 assert_eq!(Some(ApiOperation::Project), *client.api_operation.borrow());
548 }
549
550 #[test]
551 fn test_list_project_members_num_pages() {
552 let link_header = "<https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/members/all?page=2&per_page=20>; rel=\"next\", <https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/members/all?page=2&per_page=20>; rel=\"last\"";
553 let mut headers = Headers::new();
554 headers.set("link", link_header);
555 let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
556 200,
557 "project_members.json",
558 Some(headers),
559 );
560 let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn ProjectMember);
561 let body_args = ProjectListBodyArgs::builder()
562 .members(true)
563 .user(None)
564 .from_to_page(None)
565 .build()
566 .unwrap();
567 gitlab.num_pages(body_args).unwrap();
568 assert_eq!(
569 "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/members/all?page=1",
570 *client.url()
571 );
572 }
573
574 #[test]
575 fn test_list_project_members_num_resources() {
576 let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
577 200,
578 "project_members.json",
579 None,
580 );
581 let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn RemoteProject);
582 let body_args = ProjectListBodyArgs::builder()
583 .members(true)
584 .user(None)
585 .from_to_page(None)
586 .build()
587 .unwrap();
588 gitlab.num_resources(body_args).unwrap();
589 assert_eq!(
590 "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/members/all?page=1",
591 *client.url()
592 );
593 }
594}