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