1use crate::api_traits::{ProjectMember, RemoteProject, RemoteTag, Timestamp};
2use crate::cli::project::ProjectOptions;
3use crate::config::ConfigProperties;
4use crate::display::{self, Column, DisplayBody};
5use crate::error;
6use crate::io::CmdInfo;
7use crate::remote::{self, CacheType, GetRemoteCliArgs, ListBodyArgs, ListRemoteCliArgs};
8use crate::Result;
9use std::io::Write;
10use std::sync::Arc;
11
12use super::common;
13
14#[derive(Builder, Clone, Debug, Default, PartialEq)]
15pub struct Project {
16 pub id: i64,
17 default_branch: String,
18 #[builder(default)]
19 members: Vec<Member>,
20 html_url: String,
21 created_at: String,
22 description: String,
23 #[builder(default)]
25 language: String,
26}
27
28impl Project {
29 pub fn builder() -> ProjectBuilder {
30 ProjectBuilder::default()
31 }
32
33 pub fn new(id: i64, default_branch: &str) -> Self {
34 Project {
35 id,
36 default_branch: default_branch.to_string(),
37 members: Vec::new(),
38 html_url: String::new(),
39 created_at: String::new(),
40 description: String::new(),
41 language: String::new(),
42 }
43 }
44
45 pub fn with_html_url(mut self, html_url: &str) -> Self {
46 self.html_url = html_url.to_string();
47 self
48 }
49
50 pub fn with_created_at(mut self, created_at: &str) -> Self {
52 self.created_at = created_at.to_string();
53 self
54 }
55
56 pub fn default_branch(&self) -> &str {
57 &self.default_branch
58 }
59}
60
61impl From<Project> for DisplayBody {
62 fn from(p: Project) -> DisplayBody {
63 DisplayBody {
64 columns: vec![
65 Column::new("ID", p.id.to_string()),
66 Column::new("Default Branch", p.default_branch),
67 Column::new("URL", p.html_url),
68 Column::new("Created at", p.created_at),
69 Column::builder()
70 .name("Description".to_string())
71 .value(p.description)
72 .optional(true)
73 .build()
74 .unwrap(),
75 Column::builder()
76 .name("Language".to_string())
77 .value(p.language)
78 .optional(true)
79 .build()
80 .unwrap(),
81 ],
82 }
83 }
84}
85
86impl Timestamp for Project {
87 fn created_at(&self) -> String {
88 self.created_at.clone()
89 }
90}
91
92#[derive(Clone, Debug, PartialEq, Default)]
95pub enum MrMemberType {
96 Filled,
97 #[default]
98 Empty,
99}
100
101#[derive(Builder, Clone, Debug, PartialEq, Default)]
102pub struct Member {
103 #[builder(default)]
104 pub id: i64,
105 #[builder(default)]
106 pub name: String,
107 #[builder(default)]
108 pub username: String,
109 #[builder(default = "String::from(\"1970-01-01T00:00:00Z\")")]
110 pub created_at: String,
111 #[builder(default)]
112 pub mr_member_type: MrMemberType,
113}
114
115impl Member {
116 pub fn builder() -> MemberBuilder {
117 MemberBuilder::default()
118 }
119}
120
121impl Timestamp for Member {
122 fn created_at(&self) -> String {
123 self.created_at.clone()
124 }
125}
126
127impl From<Member> for DisplayBody {
128 fn from(m: Member) -> DisplayBody {
129 DisplayBody {
130 columns: vec![
131 Column::new("ID", m.id.to_string()),
132 Column::builder()
133 .name("Name".to_string())
134 .value(m.name)
135 .optional(true)
136 .build()
137 .unwrap(),
138 Column::new("Username", m.username),
139 ],
140 }
141 }
142}
143
144#[derive(Builder)]
145pub struct ProjectListCliArgs {
146 pub list_args: ListRemoteCliArgs,
147 #[builder(default)]
148 pub stars: bool,
149 #[builder(default)]
150 pub tags: bool,
151 #[builder(default)]
152 pub members: bool,
153}
154
155impl ProjectListCliArgs {
156 pub fn builder() -> ProjectListCliArgsBuilder {
157 ProjectListCliArgsBuilder::default()
158 }
159}
160
161#[derive(Builder)]
162pub struct ProjectListBodyArgs {
163 pub from_to_page: Option<ListBodyArgs>,
164 pub user: Option<Member>,
165 #[builder(default)]
166 pub stars: bool,
167 #[builder(default)]
168 pub tags: bool,
169 #[builder(default)]
170 pub members: bool,
171}
172
173impl ProjectListBodyArgs {
174 pub fn builder() -> ProjectListBodyArgsBuilder {
175 ProjectListBodyArgsBuilder::default()
176 }
177}
178
179#[derive(Builder)]
180pub struct ProjectMetadataGetCliArgs {
181 pub id: Option<i64>,
182 #[builder(default)]
183 pub path: Option<String>,
184 pub get_args: GetRemoteCliArgs,
185}
186
187impl ProjectMetadataGetCliArgs {
188 pub fn builder() -> ProjectMetadataGetCliArgsBuilder {
189 ProjectMetadataGetCliArgsBuilder::default()
190 }
191}
192
193#[derive(Builder, Clone)]
194pub struct Tag {
195 pub name: String,
196 pub sha: String,
197 pub created_at: String,
198}
199
200impl Tag {
201 pub fn builder() -> TagBuilder {
202 TagBuilder::default()
203 }
204}
205
206impl Timestamp for Tag {
207 fn created_at(&self) -> String {
208 self.created_at.clone()
209 }
210}
211
212impl From<Tag> for DisplayBody {
213 fn from(t: Tag) -> DisplayBody {
214 DisplayBody {
215 columns: vec![
216 Column::new("Name", t.name),
217 Column::new("SHA", t.sha),
218 Column::builder()
219 .name("Created at".to_string())
220 .value(t.created_at)
221 .optional(true)
222 .build()
223 .unwrap(),
224 ],
225 }
226 }
227}
228
229pub fn execute(
230 options: ProjectOptions,
231 config: Arc<dyn ConfigProperties>,
232 domain: String,
233 path: String,
234) -> Result<()> {
235 match options {
236 ProjectOptions::Info(cli_args) => {
237 let remote = remote::get_project(
238 domain,
239 path,
240 config,
241 Some(&cli_args.get_args.cache_args),
242 CacheType::File,
243 )?;
244 project_info(remote, std::io::stdout(), cli_args)
245 }
246 ProjectOptions::Members(cli_args) => {
247 let remote = remote::get_project_member(
248 domain,
249 path,
250 config,
251 Some(&cli_args.list_args.get_args.cache_args),
252 CacheType::File,
253 )?;
254 let from_to_args = remote::validate_from_to_page(&cli_args.list_args)?;
255 let body_args = ProjectListBodyArgs::builder()
256 .members(true)
257 .from_to_page(from_to_args)
258 .user(None)
259 .build()?;
260 if cli_args.list_args.num_pages {
261 return common::num_project_member_pages(remote, body_args, std::io::stdout());
262 }
263 if cli_args.list_args.num_resources {
264 return common::num_project_member_pages(remote, body_args, std::io::stdout());
265 }
266 list_project_members(remote, body_args, cli_args, std::io::stdout())
267 }
268 ProjectOptions::Tags(cli_args) => {
269 let remote = remote::get_tag(
270 domain,
271 path,
272 config,
273 Some(&cli_args.list_args.get_args.cache_args),
274 CacheType::File,
275 )?;
276 let from_to_args = remote::validate_from_to_page(&cli_args.list_args)?;
277 let body_args = ProjectListBodyArgs::builder()
278 .tags(true)
279 .from_to_page(from_to_args)
280 .user(None)
281 .build()?;
282 if cli_args.list_args.num_pages {
283 return common::num_tag_pages(remote, body_args, std::io::stdout());
284 }
285 if cli_args.list_args.num_resources {
286 return common::num_tag_resources(remote, body_args, std::io::stdout());
287 }
288 list_project_tags(remote, body_args, cli_args, std::io::stdout())
289 }
290 }
291}
292
293fn project_info<W: Write>(
294 remote: Arc<dyn RemoteProject>,
295 mut writer: W,
296 cli_args: ProjectMetadataGetCliArgs,
297) -> Result<()> {
298 let path = if let Some(path) = &cli_args.path {
300 debug_assert!(path.matches('/').count() >= 2);
302 Some(path.split('/').skip(1).collect::<Vec<&str>>().join("/"))
303 } else {
304 None
305 };
306 let CmdInfo::Project(project_data) = remote.get_project_data(cli_args.id, path.as_deref())?
307 else {
308 return Err(error::GRError::ApplicationError(
309 "remote.get_project_data expects CmdInfo::Project invariant".to_string(),
310 )
311 .into());
312 };
313 display::print(&mut writer, vec![project_data], cli_args.get_args)?;
314 Ok(())
315}
316
317fn list_project_tags<W: Write>(
318 remote: Arc<dyn RemoteTag>,
319 body_args: ProjectListBodyArgs,
320 cli_args: ProjectListCliArgs,
321 mut writer: W,
322) -> Result<()> {
323 common::list_project_tags(remote, body_args, cli_args, &mut writer)
324}
325
326fn list_project_members<W: Write>(
327 remote: Arc<dyn ProjectMember>,
328 body_args: ProjectListBodyArgs,
329 cli_args: ProjectListCliArgs,
330 mut writer: W,
331) -> Result<()> {
332 common::list_project_members(remote, body_args, cli_args, &mut writer)
333}
334
335#[cfg(test)]
336mod test {
337
338 use std::cell::RefCell;
339
340 use super::*;
341 use crate::cli::browse::BrowseOptions;
342
343 #[derive(Builder)]
344 struct ProjectDataProvider {
345 #[builder(default = "false")]
346 error: bool,
347 #[builder(default = "CmdInfo::Ignore")]
348 cmd_info: CmdInfo,
349 #[builder(default = "RefCell::new(false)")]
350 project_data_with_id_called: RefCell<bool>,
351 #[builder(default = "RefCell::new(false)")]
352 project_data_with_path_called: RefCell<bool>,
353 }
354
355 impl ProjectDataProvider {
356 pub fn builder() -> ProjectDataProviderBuilder {
357 ProjectDataProviderBuilder::default()
358 }
359 }
360
361 impl RemoteProject for ProjectDataProvider {
362 fn get_project_data(&self, id: Option<i64>, path: Option<&str>) -> crate::Result<CmdInfo> {
363 if id.is_some() {
364 *self.project_data_with_id_called.borrow_mut() = true;
365 }
366 if path.is_some() {
367 *self.project_data_with_path_called.borrow_mut() = true;
368 }
369 if self.error {
370 return Err(error::gen("Error"));
371 }
372 match self.cmd_info {
373 CmdInfo::Project(_) => Ok(self.cmd_info.clone()),
374 _ => Ok(CmdInfo::Ignore),
375 }
376 }
377
378 fn get_project_members(&self) -> crate::Result<CmdInfo> {
379 todo!()
380 }
381
382 fn get_url(&self, _option: BrowseOptions) -> String {
383 todo!()
384 }
385
386 fn list(&self, _args: ProjectListBodyArgs) -> Result<Vec<Project>> {
387 todo!()
388 }
389
390 fn num_pages(&self, _args: ProjectListBodyArgs) -> Result<Option<u32>> {
391 todo!()
392 }
393
394 fn num_resources(
395 &self,
396 _args: ProjectListBodyArgs,
397 ) -> Result<Option<crate::api_traits::NumberDeltaErr>> {
398 todo!()
399 }
400 }
401
402 impl RemoteTag for ProjectDataProvider {
403 fn list(&self, _args: ProjectListBodyArgs) -> Result<Vec<Tag>> {
404 let tag = Tag::builder()
405 .name("v1.0.0".to_string())
406 .sha("123456".to_string())
407 .created_at("2021-01-01".to_string())
408 .build()
409 .unwrap();
410 Ok(vec![tag])
411 }
412 }
413
414 impl ProjectMember for ProjectDataProvider {
415 fn list(&self, _args: ProjectListBodyArgs) -> Result<Vec<Member>> {
416 Ok(vec![Member::builder()
417 .id(1)
418 .name("Tom".to_string())
419 .username("tomsawyer".to_string())
420 .build()
421 .unwrap()])
422 }
423 }
424
425 #[test]
426 fn test_project_data_gets_persisted() {
427 let remote = ProjectDataProviderBuilder::default()
428 .cmd_info(CmdInfo::Project(Project::default()))
429 .build()
430 .unwrap();
431 let remote = Arc::new(remote);
432 let mut writer = Vec::new();
433 let get_args = GetRemoteCliArgs::default();
434 let cli_args = ProjectMetadataGetCliArgs::builder()
435 .id(Some(1))
436 .get_args(get_args)
437 .build()
438 .unwrap();
439 project_info(remote.clone(), &mut writer, cli_args).unwrap();
440 assert!(!writer.is_empty());
441 assert!(*remote.project_data_with_id_called.borrow());
442 }
443
444 #[test]
445 fn test_project_data_called_by_repo_path() {
446 let remote = ProjectDataProviderBuilder::default()
447 .cmd_info(CmdInfo::Project(Project::default()))
448 .build()
449 .unwrap();
450 let remote = Arc::new(remote);
451 let mut writer = Vec::new();
452 let get_args = GetRemoteCliArgs::default();
453 let cli_args = ProjectMetadataGetCliArgs::builder()
454 .id(None)
455 .path(Some("github.com/jordilin/gitar".to_string()))
456 .get_args(get_args)
457 .build()
458 .unwrap();
459 project_info(remote.clone(), &mut writer, cli_args).unwrap();
460 assert!(!writer.is_empty());
461 assert!(*remote.project_data_with_path_called.borrow());
462 }
463
464 #[test]
465 fn test_project_data_error() {
466 let remote = ProjectDataProviderBuilder::default()
467 .cmd_info(CmdInfo::Project(Project::default()))
468 .error(true)
469 .build()
470 .unwrap();
471 let remote = Arc::new(remote);
472 let mut writer = Vec::new();
473 let get_args = GetRemoteCliArgs::default();
474 let cli_args = ProjectMetadataGetCliArgs::builder()
475 .id(Some(1))
476 .get_args(get_args)
477 .build()
478 .unwrap();
479 project_info(remote, &mut writer, cli_args).unwrap_err();
480 assert!(writer.is_empty());
481 }
482
483 #[test]
484 fn test_get_project_data_wrong_cmdinfo_invariant() {
485 let remote = ProjectDataProviderBuilder::default()
486 .cmd_info(CmdInfo::Ignore)
487 .build()
488 .unwrap();
489 let remote = Arc::new(remote);
490 let mut writer = Vec::new();
491 let get_args = GetRemoteCliArgs::default();
492 let cli_args = ProjectMetadataGetCliArgs::builder()
493 .id(Some(1))
494 .get_args(get_args)
495 .build()
496 .unwrap();
497 let result = project_info(remote, &mut writer, cli_args);
498 match result {
499 Ok(_) => panic!("Expected error"),
500 Err(err) => match err.downcast_ref::<error::GRError>() {
501 Some(error::GRError::ApplicationError(_)) => (),
502 _ => panic!("Expected error::GRError::ApplicationError"),
503 },
504 }
505 }
506
507 #[test]
508 fn test_list_project_tags() {
509 let remote = ProjectDataProvider::builder().build().unwrap();
510 let remote = Arc::new(remote);
511 let mut writer = Vec::new();
512 let body_args = ProjectListBodyArgs::builder()
513 .tags(true)
514 .from_to_page(None)
515 .user(None)
516 .build()
517 .unwrap();
518 let cli_args = ProjectListCliArgs::builder()
519 .tags(true)
520 .list_args(ListRemoteCliArgs::builder().build().unwrap())
521 .build()
522 .unwrap();
523 list_project_tags(remote, body_args, cli_args, &mut writer).unwrap();
524 assert_eq!(
525 "Name|SHA\nv1.0.0|123456\n",
526 String::from_utf8(writer).unwrap()
527 );
528 }
529
530 #[test]
531 fn test_display_all_columns_project_tags() {
532 let remote = ProjectDataProvider::builder().build().unwrap();
533 let remote = Arc::new(remote);
534 let mut writer = Vec::new();
535 let body_args = ProjectListBodyArgs::builder()
536 .tags(true)
537 .from_to_page(None)
538 .user(None)
539 .build()
540 .unwrap();
541 let cli_args = ProjectListCliArgs::builder()
542 .tags(true)
543 .list_args(
544 ListRemoteCliArgs::builder()
545 .get_args(
546 GetRemoteCliArgs::builder()
547 .display_optional(true)
548 .build()
549 .unwrap(),
550 )
551 .build()
552 .unwrap(),
553 )
554 .build()
555 .unwrap();
556 list_project_tags(remote, body_args, cli_args, &mut writer).unwrap();
557 assert_eq!(
558 "Name|SHA|Created at\nv1.0.0|123456|2021-01-01\n",
559 String::from_utf8(writer).unwrap()
560 );
561 }
562
563 #[test]
564 fn test_display_all_columns_project_members() {
565 let remote = ProjectDataProvider::builder().build().unwrap();
566 let remote = Arc::new(remote);
567 let mut writer = Vec::new();
568 let body_args = ProjectListBodyArgs::builder()
569 .members(true)
570 .from_to_page(None)
571 .user(None)
572 .build()
573 .unwrap();
574 let cli_args = ProjectListCliArgs::builder()
575 .members(true)
576 .list_args(
577 ListRemoteCliArgs::builder()
578 .get_args(GetRemoteCliArgs::builder().build().unwrap())
579 .build()
580 .unwrap(),
581 )
582 .build()
583 .unwrap();
584 list_project_members(remote, body_args, cli_args, &mut writer).unwrap();
585 assert_eq!(
586 "ID|Username\n1|tomsawyer\n",
587 String::from_utf8(writer).unwrap()
588 );
589 }
590}