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