1use clap::Parser;
7use clap_complete::Shell;
8
9fn long_version() -> &'static str {
14 if env!("CQLSH_GIT_DIRTY") == "true" {
16 concat!(
17 env!("CARGO_PKG_VERSION"),
18 " (",
19 env!("CQLSH_GIT_SHA"),
20 "-dirty)"
21 )
22 } else {
23 concat!(env!("CARGO_PKG_VERSION"), " (", env!("CQLSH_GIT_SHA"), ")")
24 }
25}
26
27#[derive(Parser, Debug, Clone)]
32#[command(name = "cqlsh", version, long_version = long_version(), about, disable_help_flag = false)]
33pub struct CliArgs {
34 #[arg(value_name = "host")]
36 pub host: Option<String>,
37
38 #[arg(value_name = "port")]
40 pub port: Option<u16>,
41
42 #[arg(short = 'C', long = "color")]
44 pub color: bool,
45
46 #[arg(long = "no-color")]
48 pub no_color: bool,
49
50 #[arg(long = "browser", value_name = "BROWSER")]
52 pub browser: Option<String>,
53
54 #[arg(long = "ssl")]
56 pub ssl: bool,
57
58 #[arg(long = "no-file-io")]
60 pub no_file_io: bool,
61
62 #[arg(long = "debug")]
64 pub debug: bool,
65
66 #[arg(long = "coverage", hide = true)]
68 pub coverage: bool,
69
70 #[arg(short = 'e', long = "execute", value_name = "STATEMENT")]
72 pub execute: Option<String>,
73
74 #[arg(short = 'f', long = "file", value_name = "FILE")]
76 pub file: Option<String>,
77
78 #[arg(short = 'k', long = "keyspace", value_name = "KEYSPACE")]
80 pub keyspace: Option<String>,
81
82 #[arg(short = 'u', long = "username", value_name = "USERNAME")]
84 pub username: Option<String>,
85
86 #[arg(short = 'p', long = "password", value_name = "PASSWORD")]
88 pub password: Option<String>,
89
90 #[arg(long = "connect-timeout", value_name = "SECONDS")]
92 pub connect_timeout: Option<u64>,
93
94 #[arg(long = "request-timeout", value_name = "SECONDS")]
96 pub request_timeout: Option<u64>,
97
98 #[arg(short = 't', long = "tty")]
100 pub tty: bool,
101
102 #[arg(long = "encoding", value_name = "ENCODING")]
104 pub encoding: Option<String>,
105
106 #[arg(long = "cqlshrc", value_name = "FILE")]
108 pub cqlshrc: Option<String>,
109
110 #[arg(long = "cqlversion", value_name = "VERSION")]
112 pub cqlversion: Option<String>,
113
114 #[arg(long = "protocol-version", value_name = "VERSION")]
116 pub protocol_version: Option<u8>,
117
118 #[arg(long = "consistency-level", value_name = "LEVEL")]
120 pub consistency_level: Option<String>,
121
122 #[arg(long = "serial-consistency-level", value_name = "LEVEL")]
124 pub serial_consistency_level: Option<String>,
125
126 #[arg(long = "no_compact")]
128 pub no_compact: bool,
129
130 #[arg(long = "disable-history")]
132 pub disable_history: bool,
133
134 #[arg(short = 'b', long = "secure-connect-bundle", value_name = "BUNDLE")]
136 pub secure_connect_bundle: Option<String>,
137
138 #[arg(long = "completions", value_name = "SHELL")]
140 pub completions: Option<Shell>,
141
142 #[arg(long = "generate-man", hide = true)]
144 pub generate_man: bool,
145}
146
147impl CliArgs {
148 pub fn validate(&self) -> Result<(), String> {
150 if self.color && self.no_color {
151 return Err("Cannot use both --color and --no-color".to_string());
152 }
153
154 if self.execute.is_some() && self.file.is_some() {
155 return Err("Cannot use both --execute and --file".to_string());
156 }
157
158 if let Some(pv) = self.protocol_version {
159 if !(1..=6).contains(&pv) {
160 return Err(format!(
161 "Protocol version must be between 1 and 6, got {}",
162 pv
163 ));
164 }
165 }
166
167 Ok(())
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use clap::Parser;
175
176 fn parse(args: &[&str]) -> CliArgs {
177 let mut full_args = vec!["cqlsh-rs"];
178 full_args.extend_from_slice(args);
179 CliArgs::parse_from(full_args)
180 }
181
182 #[test]
183 fn no_args_defaults() {
184 let args = parse(&[]);
185 assert!(args.host.is_none());
186 assert!(args.port.is_none());
187 assert!(!args.color);
188 assert!(!args.no_color);
189 assert!(!args.ssl);
190 assert!(!args.debug);
191 assert!(!args.tty);
192 assert!(!args.no_file_io);
193 assert!(!args.no_compact);
194 assert!(!args.disable_history);
195 assert!(args.execute.is_none());
196 assert!(args.file.is_none());
197 assert!(args.keyspace.is_none());
198 assert!(args.username.is_none());
199 assert!(args.password.is_none());
200 assert!(args.connect_timeout.is_none());
201 assert!(args.request_timeout.is_none());
202 assert!(args.encoding.is_none());
203 assert!(args.cqlshrc.is_none());
204 assert!(args.cqlversion.is_none());
205 assert!(args.protocol_version.is_none());
206 assert!(args.consistency_level.is_none());
207 assert!(args.serial_consistency_level.is_none());
208 assert!(args.browser.is_none());
209 assert!(args.secure_connect_bundle.is_none());
210 }
211
212 #[test]
213 fn positional_host() {
214 let args = parse(&["192.168.1.1"]);
215 assert_eq!(args.host.as_deref(), Some("192.168.1.1"));
216 assert!(args.port.is_none());
217 }
218
219 #[test]
220 fn positional_host_and_port() {
221 let args = parse(&["192.168.1.1", "9043"]);
222 assert_eq!(args.host.as_deref(), Some("192.168.1.1"));
223 assert_eq!(args.port, Some(9043));
224 }
225
226 #[test]
227 fn execute_flag_short() {
228 let args = parse(&["-e", "SELECT * FROM system.local"]);
229 assert_eq!(args.execute.as_deref(), Some("SELECT * FROM system.local"));
230 }
231
232 #[test]
233 fn execute_flag_long() {
234 let args = parse(&["--execute", "DESC KEYSPACES"]);
235 assert_eq!(args.execute.as_deref(), Some("DESC KEYSPACES"));
236 }
237
238 #[test]
239 fn file_flag() {
240 let args = parse(&["-f", "/tmp/schema.cql"]);
241 assert_eq!(args.file.as_deref(), Some("/tmp/schema.cql"));
242 }
243
244 #[test]
245 fn keyspace_flag() {
246 let args = parse(&["-k", "my_keyspace"]);
247 assert_eq!(args.keyspace.as_deref(), Some("my_keyspace"));
248 }
249
250 #[test]
251 fn auth_flags() {
252 let args = parse(&["-u", "admin", "-p", "secret"]);
253 assert_eq!(args.username.as_deref(), Some("admin"));
254 assert_eq!(args.password.as_deref(), Some("secret"));
255 }
256
257 #[test]
258 fn ssl_flag() {
259 let args = parse(&["--ssl"]);
260 assert!(args.ssl);
261 }
262
263 #[test]
264 fn color_flag() {
265 let args = parse(&["-C"]);
266 assert!(args.color);
267 }
268
269 #[test]
270 fn no_color_flag() {
271 let args = parse(&["--no-color"]);
272 assert!(args.no_color);
273 }
274
275 #[test]
276 fn debug_flag() {
277 let args = parse(&["--debug"]);
278 assert!(args.debug);
279 }
280
281 #[test]
282 fn tty_flag_short() {
283 let args = parse(&["-t"]);
284 assert!(args.tty);
285 }
286
287 #[test]
288 fn tty_flag_long() {
289 let args = parse(&["--tty"]);
290 assert!(args.tty);
291 }
292
293 #[test]
294 fn timeout_flags() {
295 let args = parse(&["--connect-timeout", "30", "--request-timeout", "60"]);
296 assert_eq!(args.connect_timeout, Some(30));
297 assert_eq!(args.request_timeout, Some(60));
298 }
299
300 #[test]
301 fn encoding_flag() {
302 let args = parse(&["--encoding", "latin-1"]);
303 assert_eq!(args.encoding.as_deref(), Some("latin-1"));
304 }
305
306 #[test]
307 fn cqlshrc_flag() {
308 let args = parse(&["--cqlshrc", "/etc/cqlshrc"]);
309 assert_eq!(args.cqlshrc.as_deref(), Some("/etc/cqlshrc"));
310 }
311
312 #[test]
313 fn cqlversion_flag() {
314 let args = parse(&["--cqlversion", "3.4.5"]);
315 assert_eq!(args.cqlversion.as_deref(), Some("3.4.5"));
316 }
317
318 #[test]
319 fn protocol_version_flag() {
320 let args = parse(&["--protocol-version", "4"]);
321 assert_eq!(args.protocol_version, Some(4));
322 }
323
324 #[test]
325 fn consistency_level_flag() {
326 let args = parse(&["--consistency-level", "QUORUM"]);
327 assert_eq!(args.consistency_level.as_deref(), Some("QUORUM"));
328 }
329
330 #[test]
331 fn serial_consistency_level_flag() {
332 let args = parse(&["--serial-consistency-level", "LOCAL_SERIAL"]);
333 assert_eq!(
334 args.serial_consistency_level.as_deref(),
335 Some("LOCAL_SERIAL")
336 );
337 }
338
339 #[test]
340 fn no_file_io_flag() {
341 let args = parse(&["--no-file-io"]);
342 assert!(args.no_file_io);
343 }
344
345 #[test]
346 fn no_compact_flag() {
347 let args = parse(&["--no_compact"]);
348 assert!(args.no_compact);
349 }
350
351 #[test]
352 fn disable_history_flag() {
353 let args = parse(&["--disable-history"]);
354 assert!(args.disable_history);
355 }
356
357 #[test]
358 fn secure_connect_bundle_flag() {
359 let args = parse(&["-b", "/path/to/bundle.zip"]);
360 assert_eq!(
361 args.secure_connect_bundle.as_deref(),
362 Some("/path/to/bundle.zip")
363 );
364 }
365
366 #[test]
367 fn browser_flag() {
368 let args = parse(&["--browser", "firefox"]);
369 assert_eq!(args.browser.as_deref(), Some("firefox"));
370 }
371
372 #[test]
373 fn combined_flags() {
374 let args = parse(&[
375 "10.0.0.1",
376 "9142",
377 "-u",
378 "admin",
379 "-p",
380 "pass",
381 "-k",
382 "test_ks",
383 "--ssl",
384 "-C",
385 "--connect-timeout",
386 "15",
387 ]);
388 assert_eq!(args.host.as_deref(), Some("10.0.0.1"));
389 assert_eq!(args.port, Some(9142));
390 assert_eq!(args.username.as_deref(), Some("admin"));
391 assert_eq!(args.password.as_deref(), Some("pass"));
392 assert_eq!(args.keyspace.as_deref(), Some("test_ks"));
393 assert!(args.ssl);
394 assert!(args.color);
395 assert_eq!(args.connect_timeout, Some(15));
396 }
397
398 #[test]
401 fn validate_color_conflict() {
402 let args = parse(&["-C", "--no-color"]);
403 let result = args.validate();
404 assert!(result.is_err());
405 assert!(result.unwrap_err().contains("--color"));
406 }
407
408 #[test]
409 fn validate_execute_and_file_conflict() {
410 let args = parse(&["-e", "SELECT 1", "-f", "test.cql"]);
411 let result = args.validate();
412 assert!(result.is_err());
413 assert!(result.unwrap_err().contains("--execute"));
414 }
415
416 #[test]
417 fn validate_protocol_version_range() {
418 let args = parse(&["--protocol-version", "4"]);
419 assert!(args.validate().is_ok());
420 }
421
422 #[test]
423 fn validate_valid_args() {
424 let args = parse(&["-u", "admin", "--ssl", "-k", "test"]);
425 assert!(args.validate().is_ok());
426 }
427
428 #[test]
429 fn completions_flag() {
430 let args = parse(&["--completions", "bash"]);
431 assert_eq!(args.completions, Some(Shell::Bash));
432 }
433
434 #[test]
435 fn completions_flag_zsh() {
436 let args = parse(&["--completions", "zsh"]);
437 assert_eq!(args.completions, Some(Shell::Zsh));
438 }
439
440 #[test]
441 fn unknown_flag_produces_error() {
442 let result = CliArgs::try_parse_from(["cqlsh-rs", "--nonexistent"]);
443 assert!(result.is_err());
444 }
445}