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