gr/cli/
common.rs

1use std::fmt::{self, Display, Formatter};
2
3use clap::{Parser, ValueEnum};
4
5use crate::{
6    display::Format,
7    remote::{CacheCliArgs, GetRemoteCliArgs, ListRemoteCliArgs, ListSortMode},
8    time::Milliseconds,
9};
10
11#[derive(Clone, Parser)]
12#[clap(next_help_heading = "List options")]
13pub struct ListArgs {
14    /// List the given page number
15    #[clap(long)]
16    page: Option<i64>,
17    /// From page
18    #[clap(long)]
19    pub from_page: Option<i64>,
20    /// To page
21    #[clap(long)]
22    pub to_page: Option<i64>,
23    /// How many pages are available
24    #[clap(long)]
25    num_pages: bool,
26    /// How many resources are available. Result is an approximation depending
27    /// on total pages and default per_page query param. Total given as an
28    /// interval (min, max)
29    #[clap(long)]
30    pub num_resources: bool,
31    /// Created after date (ISO 8601 YYYY-MM-DDTHH:MM:SSZ)
32    #[clap(long)]
33    created_after: Option<String>,
34    /// Created before date (ISO 8601 YYYY-MM-DDTHH:MM:SSZ)
35    #[clap(long)]
36    created_before: Option<String>,
37    /// Flush results to STDOUT as they are received. No sorting and no date
38    /// filtering is applied
39    #[clap(long, visible_alias = "flush")]
40    pub stream: bool,
41    /// Throttle the requests to the server. Fixed time to wait in milliseconds
42    /// between each HTTP request.
43    #[clap(long, value_name = "MILLISECONDS", group = "throttle_arg")]
44    pub throttle: Option<u64>,
45    /// Throttle the requests using a random wait time between the given range.
46    /// The MIN and MAX values are in milliseconds.
47    #[arg(long, value_parser=parse_throttle_range, value_name = "MIN-MAX", group = "throttle_arg")]
48    throttle_range: Option<(u64, u64)>,
49    #[clap(long, default_value_t=SortModeCli::Asc)]
50    sort: SortModeCli,
51    #[clap(flatten)]
52    pub get_args: GetArgs,
53}
54
55#[derive(Clone, Parser)]
56pub struct GetArgs {
57    #[clap(flatten)]
58    pub format_args: FormatArgs,
59    #[clap(flatten)]
60    pub cache_args: CacheArgs,
61    #[clap(flatten)]
62    pub retry_args: RetryArgs,
63}
64
65#[derive(Clone, Parser)]
66#[clap(next_help_heading = "Cache options")]
67pub struct CacheArgs {
68    /// Refresh the cache
69    #[clap(long, short, group = "cache")]
70    pub refresh: bool,
71    /// Disable caching data on disk
72    #[clap(long, group = "cache")]
73    pub no_cache: bool,
74}
75
76#[derive(Clone, Parser)]
77#[clap(next_help_heading = "Formatting options")]
78pub struct FormatArgs {
79    /// Do not print headers
80    #[clap(long)]
81    pub no_headers: bool,
82    /// Output format
83    #[clap(long, default_value_t=FormatCli::Pipe)]
84    pub format: FormatCli,
85    /// Display additional fields
86    #[clap(visible_short_alias = 'o', long)]
87    pub more_output: bool,
88}
89
90#[derive(Clone, Parser)]
91#[clap(next_help_heading = "Retry options")]
92pub struct RetryArgs {
93    /// Retries request on error. Backs off exponentially if enabled
94    #[clap(long)]
95    pub backoff: bool,
96    /// Number of retries
97    #[clap(long, default_value = "0", requires = "backoff")]
98    pub max_retries: u32,
99    /// Additional delay in seconds before retrying the request when backoff is
100    /// enabled
101    #[clap(long, default_value = "60", requires = "backoff")]
102    pub retry_after: u64,
103}
104
105#[derive(ValueEnum, Clone, Debug)]
106pub enum FormatCli {
107    Csv,
108    Json,
109    Pipe,
110    Toml,
111}
112
113impl Display for FormatCli {
114    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
115        match self {
116            FormatCli::Csv => write!(f, "csv"),
117            FormatCli::Pipe => write!(f, "pipe"),
118            FormatCli::Json => write!(f, "json"),
119            FormatCli::Toml => write!(f, "toml"),
120        }
121    }
122}
123
124#[derive(ValueEnum, Clone, Debug)]
125enum SortModeCli {
126    Asc,
127    Desc,
128}
129
130impl Display for SortModeCli {
131    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
132        match self {
133            SortModeCli::Asc => write!(f, "asc"),
134            SortModeCli::Desc => write!(f, "desc"),
135        }
136    }
137}
138
139impl From<ListArgs> for ListRemoteCliArgs {
140    fn from(args: ListArgs) -> Self {
141        ListRemoteCliArgs::builder()
142            .from_page(args.from_page)
143            .to_page(args.to_page)
144            .page_number(args.page)
145            .num_pages(args.num_pages)
146            .num_resources(args.num_resources)
147            .created_after(args.created_after)
148            .created_before(args.created_before)
149            .sort(args.sort.into())
150            .get_args(args.get_args.into())
151            .flush(args.stream)
152            .throttle_time(args.throttle.map(Milliseconds::from))
153            .throttle_range(
154                args.throttle_range
155                    .map(|(min, max)| (Milliseconds::from(min), Milliseconds::from(max))),
156            )
157            .build()
158            .unwrap()
159    }
160}
161
162impl From<GetArgs> for GetRemoteCliArgs {
163    fn from(args: GetArgs) -> Self {
164        GetRemoteCliArgs::builder()
165            .no_headers(args.format_args.no_headers)
166            .format(args.format_args.format.into())
167            .display_optional(args.format_args.more_output)
168            .cache_args(args.cache_args.into())
169            .backoff_max_retries(args.retry_args.max_retries)
170            .backoff_retry_after(args.retry_args.retry_after)
171            .build()
172            .unwrap()
173    }
174}
175
176impl From<CacheArgs> for CacheCliArgs {
177    fn from(args: CacheArgs) -> Self {
178        CacheCliArgs::builder()
179            .refresh(args.refresh)
180            .no_cache(args.no_cache)
181            .build()
182            .unwrap()
183    }
184}
185
186impl From<FormatCli> for Format {
187    fn from(format: FormatCli) -> Self {
188        match format {
189            FormatCli::Csv => Format::CSV,
190            FormatCli::Json => Format::JSON,
191            FormatCli::Pipe => Format::PIPE,
192            FormatCli::Toml => Format::TOML,
193        }
194    }
195}
196
197impl From<SortModeCli> for ListSortMode {
198    fn from(sort: SortModeCli) -> Self {
199        match sort {
200            SortModeCli::Asc => ListSortMode::Asc,
201            SortModeCli::Desc => ListSortMode::Desc,
202        }
203    }
204}
205
206pub fn validate_project_repo_path(path: &str) -> Result<String, String> {
207    let (fields, empty_fields) = fields(path);
208    if fields.count() == 2 && empty_fields == 0 {
209        Ok(path.to_string())
210    } else {
211        Err("Path must be in the format `OWNER/PROJECT_NAME`".to_string())
212    }
213}
214
215pub fn validate_domain_project_repo_path(path: &str) -> Result<String, String> {
216    let (fields, empty_fields) = fields(path);
217    if fields.count() == 3 && empty_fields == 0 {
218        Ok(path.to_string())
219    } else {
220        Err("Path must be in the format `DOMAIN/OWNER/PROJECT_NAME`".to_string())
221    }
222}
223
224fn fields(path: &str) -> (std::str::Split<'_, char>, usize) {
225    let fields = path.split('/');
226    let empty_fields = fields.clone().filter(|f| f.is_empty()).count();
227    (fields, empty_fields)
228}
229
230fn parse_throttle_range(s: &str) -> Result<(u64, u64), String> {
231    let parts: Vec<&str> = s.split('-').collect();
232    if parts.len() != 2 {
233        return Err(String::from("Throttle range must be in the format min-max"));
234    }
235    let min = parts[0].parse::<u64>().map_err(|_| "Invalid MIN value")?;
236    let max = parts[1].parse::<u64>().map_err(|_| "Invalid MAX value")?;
237    if min >= max {
238        return Err(String::from("MIN must be less than MAX"));
239    }
240    Ok((min, max))
241}
242
243#[cfg(test)]
244mod test {
245    use super::*;
246
247    #[test]
248    fn test_validate_project_repo_path() {
249        assert!(validate_project_repo_path("owner/project").is_ok());
250        assert!(validate_project_repo_path("owner/project/extra").is_err());
251        assert!(validate_project_repo_path("owner").is_err());
252        assert!(validate_project_repo_path("owner/project/extra/extra").is_err());
253        assert!(validate_project_repo_path("owner/").is_err());
254    }
255
256    #[test]
257    fn test_validate_domain_project_repo_path() {
258        assert!(validate_domain_project_repo_path("github.com/jordilin/gitar").is_ok());
259        assert!(validate_domain_project_repo_path("github.com/jordilin/").is_err());
260        assert!(validate_domain_project_repo_path("github.com///").is_err());
261        assert!(validate_domain_project_repo_path("github.com/jordilin/project/extra").is_err());
262        assert!(validate_domain_project_repo_path("github.com/jordilin").is_err());
263        assert!(
264            validate_domain_project_repo_path("github.com/jordilin/project/extra/extra").is_err()
265        );
266    }
267
268    #[test]
269    fn test_valid_throttle_range() {
270        assert_eq!(parse_throttle_range("100-500"), Ok((100, 500)));
271        assert_eq!(parse_throttle_range("0-1000"), Ok((0, 1000)));
272        assert_eq!(parse_throttle_range("1-2"), Ok((1, 2)));
273    }
274
275    #[test]
276    fn test_invalid_number_of_arguments() {
277        assert!(parse_throttle_range("100").is_err());
278        assert!(parse_throttle_range("100-200 300").is_err());
279        assert!(parse_throttle_range("").is_err());
280    }
281
282    #[test]
283    fn test_invalid_number_format() {
284        assert!(parse_throttle_range("abc-500").is_err());
285        assert!(parse_throttle_range("100-def").is_err());
286        assert!(parse_throttle_range("100.5-500").is_err());
287    }
288
289    #[test]
290    fn test_min_greater_than_or_equal_to_max() {
291        assert!(parse_throttle_range("500-100").is_err());
292        assert!(parse_throttle_range("100-100").is_err());
293    }
294
295    #[test]
296    fn test_error_messages() {
297        assert_eq!(
298            parse_throttle_range("100"),
299            Err("Throttle range must be in the format min-max".to_string())
300        );
301        assert_eq!(
302            parse_throttle_range("abc-500"),
303            Err("Invalid MIN value".to_string())
304        );
305        assert_eq!(
306            parse_throttle_range("100-def"),
307            Err("Invalid MAX value".to_string())
308        );
309        assert_eq!(
310            parse_throttle_range("500-100"),
311            Err("MIN must be less than MAX".to_string())
312        );
313    }
314}