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 #[clap(long)]
16 page: Option<i64>,
17 #[clap(long)]
19 pub from_page: Option<i64>,
20 #[clap(long)]
22 pub to_page: Option<i64>,
23 #[clap(long)]
25 num_pages: bool,
26 #[clap(long)]
30 pub num_resources: bool,
31 #[clap(long)]
33 created_after: Option<String>,
34 #[clap(long)]
36 created_before: Option<String>,
37 #[clap(long, visible_alias = "flush")]
40 pub stream: bool,
41 #[clap(long, value_name = "MILLISECONDS", group = "throttle_arg")]
44 pub throttle: Option<u64>,
45 #[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 #[clap(long, short, group = "cache")]
70 pub refresh: bool,
71 #[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 #[clap(long)]
81 pub no_headers: bool,
82 #[clap(long, default_value_t=FormatCli::Pipe)]
84 pub format: FormatCli,
85 #[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 #[clap(long)]
95 pub backoff: bool,
96 #[clap(long, default_value = "0", requires = "backoff")]
98 pub max_retries: u32,
99 #[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}