1use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11
12use anyhow::{Context, Result};
13use configparser::ini::Ini;
14use thiserror::Error;
15
16use crate::cli::CliArgs;
17
18#[derive(Error, Debug)]
20pub enum ConfigError {
21 #[error("failed to parse cqlshrc file at {path}: {reason}")]
22 ParseError { path: String, reason: String },
23
24 #[error("invalid value for {key}: {reason}")]
25 InvalidValue { key: String, reason: String },
26}
27
28#[derive(Debug, Clone, Default)]
30pub struct CqlshrcConfig {
31 pub authentication: AuthenticationSection,
32 pub connection: ConnectionSection,
33 pub ssl: SslSection,
34 pub certfiles: HashMap<String, String>,
35 pub ui: UiSection,
36 pub cql: CqlSection,
37 pub csv: CsvSection,
38 pub copy: CopySection,
39 pub copy_to: CopyToSection,
40 pub copy_from: CopyFromSection,
41 pub tracing: TracingSection,
42}
43
44#[derive(Debug, Clone, Default)]
45pub struct AuthenticationSection {
46 pub credentials: Option<String>,
47 pub username: Option<String>,
48 pub password: Option<String>,
49 pub keyspace: Option<String>,
50}
51
52#[derive(Debug, Clone, Default)]
53pub struct ConnectionSection {
54 pub hostname: Option<String>,
55 pub port: Option<u16>,
56 pub factory: Option<String>,
57 pub timeout: Option<u64>,
58 pub request_timeout: Option<u64>,
59 pub connect_timeout: Option<u64>,
60 pub client_timeout: Option<u64>,
61}
62
63#[derive(Debug, Clone, Default)]
64pub struct SslSection {
65 pub certfile: Option<String>,
66 pub validate: Option<bool>,
67 pub userkey: Option<String>,
68 pub usercert: Option<String>,
69 pub version: Option<String>,
70}
71
72#[derive(Debug, Clone, Default)]
73pub struct UiSection {
74 pub color: Option<bool>,
75 pub datetimeformat: Option<String>,
76 pub timezone: Option<String>,
77 pub float_precision: Option<u32>,
78 pub double_precision: Option<u32>,
79 pub max_trace_wait: Option<f64>,
80 pub encoding: Option<String>,
81 pub completekey: Option<String>,
82 pub browser: Option<String>,
83}
84
85#[derive(Debug, Clone, Default)]
86pub struct CqlSection {
87 pub version: Option<String>,
88}
89
90#[derive(Debug, Clone, Default)]
91pub struct CsvSection {
92 pub field_size_limit: Option<usize>,
93}
94
95#[derive(Debug, Clone, Default)]
96pub struct CopySection {
97 pub numprocesses: Option<u32>,
98 pub maxattempts: Option<u32>,
99 pub reportfrequency: Option<f64>,
100}
101
102#[derive(Debug, Clone, Default)]
103pub struct CopyToSection {
104 pub pagesize: Option<u32>,
105 pub pagetimeout: Option<u64>,
106 pub begintoken: Option<String>,
107 pub endtoken: Option<String>,
108 pub maxrequests: Option<u32>,
109 pub maxoutputsize: Option<i64>,
110 pub floatprecision: Option<u32>,
111 pub doubleprecision: Option<u32>,
112}
113
114#[derive(Debug, Clone, Default)]
115pub struct CopyFromSection {
116 pub maxbatchsize: Option<u32>,
117 pub minbatchsize: Option<u32>,
118 pub chunksize: Option<u32>,
119 pub ingestrate: Option<u64>,
120 pub maxparseerrors: Option<i64>,
121 pub maxinserterrors: Option<i64>,
122 pub preparedstatements: Option<bool>,
123 pub ttl: Option<i64>,
124}
125
126#[derive(Debug, Clone, Default)]
127pub struct TracingSection {
128 pub max_trace_wait: Option<f64>,
129}
130
131impl CqlshrcConfig {
132 pub fn load(path: &Path) -> Result<Self> {
134 if !path.exists() {
135 return Ok(Self::default());
136 }
137
138 let mut ini = Ini::new_cs(); ini.load(path).map_err(|e| ConfigError::ParseError {
140 path: path.display().to_string(),
141 reason: e,
142 })?;
143
144 Ok(Self::from_ini(&ini))
145 }
146
147 pub fn parse(content: &str) -> Result<Self> {
149 let mut ini = Ini::new_cs();
150 ini.read(content.to_string())
151 .map_err(|e| ConfigError::ParseError {
152 path: "<string>".to_string(),
153 reason: e,
154 })?;
155
156 Ok(Self::from_ini(&ini))
157 }
158
159 fn from_ini(ini: &Ini) -> Self {
160 Self {
161 authentication: AuthenticationSection {
162 credentials: ini.get("authentication", "credentials"),
163 username: ini.get("authentication", "username"),
164 password: ini.get("authentication", "password"),
165 keyspace: ini.get("authentication", "keyspace"),
166 },
167 connection: ConnectionSection {
168 hostname: ini.get("connection", "hostname"),
169 port: ini.get("connection", "port").and_then(|v| v.parse().ok()),
170 factory: ini.get("connection", "factory"),
171 timeout: ini
172 .get("connection", "timeout")
173 .and_then(|v| v.parse().ok()),
174 request_timeout: ini
175 .get("connection", "request_timeout")
176 .and_then(|v| v.parse().ok()),
177 connect_timeout: ini
178 .get("connection", "connect_timeout")
179 .and_then(|v| v.parse().ok()),
180 client_timeout: ini
181 .get("connection", "client_timeout")
182 .and_then(|v| v.parse().ok()),
183 },
184 ssl: SslSection {
185 certfile: ini.get("ssl", "certfile"),
186 validate: ini.get("ssl", "validate").map(|v| parse_bool(&v)),
187 userkey: ini.get("ssl", "userkey"),
188 usercert: ini.get("ssl", "usercert"),
189 version: ini.get("ssl", "version"),
190 },
191 certfiles: ini
192 .get_map()
193 .and_then(|m| m.get("certfiles").cloned())
194 .unwrap_or_default()
195 .into_iter()
196 .filter_map(|(k, v)| v.map(|val| (k, val)))
197 .collect(),
198 ui: UiSection {
199 color: ini.get("ui", "color").map(|v| parse_bool(&v)),
200 datetimeformat: ini.get("ui", "datetimeformat"),
201 timezone: ini.get("ui", "timezone"),
202 float_precision: ini
203 .get("ui", "float_precision")
204 .and_then(|v| v.parse().ok()),
205 double_precision: ini
206 .get("ui", "double_precision")
207 .and_then(|v| v.parse().ok()),
208 max_trace_wait: ini.get("ui", "max_trace_wait").and_then(|v| v.parse().ok()),
209 encoding: ini.get("ui", "encoding"),
210 completekey: ini.get("ui", "completekey"),
211 browser: ini.get("ui", "browser"),
212 },
213 cql: CqlSection {
214 version: ini.get("cql", "version"),
215 },
216 csv: CsvSection {
217 field_size_limit: ini
218 .get("csv", "field_size_limit")
219 .and_then(|v| v.parse().ok()),
220 },
221 copy: CopySection {
222 numprocesses: ini.get("copy", "numprocesses").and_then(|v| v.parse().ok()),
223 maxattempts: ini.get("copy", "maxattempts").and_then(|v| v.parse().ok()),
224 reportfrequency: ini
225 .get("copy", "reportfrequency")
226 .and_then(|v| v.parse().ok()),
227 },
228 copy_to: CopyToSection {
229 pagesize: ini.get("copy-to", "pagesize").and_then(|v| v.parse().ok()),
230 pagetimeout: ini
231 .get("copy-to", "pagetimeout")
232 .and_then(|v| v.parse().ok()),
233 begintoken: ini.get("copy-to", "begintoken").filter(|s| !s.is_empty()),
234 endtoken: ini.get("copy-to", "endtoken").filter(|s| !s.is_empty()),
235 maxrequests: ini
236 .get("copy-to", "maxrequests")
237 .and_then(|v| v.parse().ok()),
238 maxoutputsize: ini
239 .get("copy-to", "maxoutputsize")
240 .and_then(|v| v.parse().ok()),
241 floatprecision: ini
242 .get("copy-to", "floatprecision")
243 .and_then(|v| v.parse().ok()),
244 doubleprecision: ini
245 .get("copy-to", "doubleprecision")
246 .and_then(|v| v.parse().ok()),
247 },
248 copy_from: CopyFromSection {
249 maxbatchsize: ini
250 .get("copy-from", "maxbatchsize")
251 .and_then(|v| v.parse().ok()),
252 minbatchsize: ini
253 .get("copy-from", "minbatchsize")
254 .and_then(|v| v.parse().ok()),
255 chunksize: ini
256 .get("copy-from", "chunksize")
257 .and_then(|v| v.parse().ok()),
258 ingestrate: ini
259 .get("copy-from", "ingestrate")
260 .and_then(|v| v.parse().ok()),
261 maxparseerrors: ini
262 .get("copy-from", "maxparseerrors")
263 .and_then(|v| v.parse().ok()),
264 maxinserterrors: ini
265 .get("copy-from", "maxinserterrors")
266 .and_then(|v| v.parse().ok()),
267 preparedstatements: ini
268 .get("copy-from", "preparedstatements")
269 .map(|v| parse_bool(&v)),
270 ttl: ini.get("copy-from", "ttl").and_then(|v| v.parse().ok()),
271 },
272 tracing: TracingSection {
273 max_trace_wait: ini
274 .get("tracing", "max_trace_wait")
275 .and_then(|v| v.parse().ok()),
276 },
277 }
278 }
279}
280
281fn parse_bool(val: &str) -> bool {
284 matches!(val.to_lowercase().as_str(), "true" | "yes" | "on" | "1")
285}
286
287#[derive(Debug, Clone)]
290pub struct MergedConfig {
291 pub host: String,
292 pub port: u16,
293 pub username: Option<String>,
294 pub password: Option<String>,
295 pub keyspace: Option<String>,
296 pub ssl: bool,
297 pub color: ColorMode,
298 pub debug: bool,
299 pub tty: bool,
300 pub no_file_io: bool,
301 pub no_compact: bool,
302 pub disable_history: bool,
303 pub execute: Option<String>,
304 pub file: Option<String>,
305 pub connect_timeout: u64,
306 pub request_timeout: u64,
307 pub encoding: String,
308 pub cqlversion: Option<String>,
309 pub protocol_version: Option<u8>,
310 pub consistency_level: Option<String>,
311 pub serial_consistency_level: Option<String>,
312 pub browser: Option<String>,
313 pub secure_connect_bundle: Option<String>,
314 pub cqlshrc_path: PathBuf,
315 pub cqlshrc: CqlshrcConfig,
316}
317
318#[derive(Debug, Clone, PartialEq, Eq)]
319pub enum ColorMode {
320 On,
321 Off,
322 Auto,
323}
324
325const DEFAULT_CONNECT_TIMEOUT: u64 = 5;
327const DEFAULT_REQUEST_TIMEOUT: u64 = 10;
329const DEFAULT_HOST: &str = "127.0.0.1";
331const DEFAULT_PORT: u16 = 9042;
333
334#[derive(Debug, Clone, Default)]
336pub struct EnvConfig {
337 pub host: Option<String>,
338 pub port: Option<u16>,
339 pub ssl_certfile: Option<String>,
340 pub ssl_validate: Option<bool>,
341 pub connect_timeout: Option<u64>,
342 pub request_timeout: Option<u64>,
343 pub history_file: Option<String>,
344}
345
346impl EnvConfig {
347 pub fn from_env() -> Self {
349 Self {
350 host: std::env::var("CQLSH_HOST").ok(),
351 port: std::env::var("CQLSH_PORT")
352 .ok()
353 .and_then(|v| v.parse().ok()),
354 ssl_certfile: std::env::var("SSL_CERTFILE").ok(),
355 ssl_validate: std::env::var("SSL_VALIDATE").ok().map(|v| parse_bool(&v)),
356 connect_timeout: std::env::var("CQLSH_DEFAULT_CONNECT_TIMEOUT_SECONDS")
357 .ok()
358 .and_then(|v| v.parse().ok()),
359 request_timeout: std::env::var("CQLSH_DEFAULT_REQUEST_TIMEOUT_SECONDS")
360 .ok()
361 .and_then(|v| v.parse().ok()),
362 history_file: std::env::var("CQL_HISTORY").ok(),
363 }
364 }
365}
366
367impl MergedConfig {
368 pub fn build(
372 cli: &CliArgs,
373 env: &EnvConfig,
374 cqlshrc: CqlshrcConfig,
375 cqlshrc_path: PathBuf,
376 ) -> Self {
377 let host = cli
379 .host
380 .clone()
381 .or_else(|| env.host.clone())
382 .or_else(|| cqlshrc.connection.hostname.clone())
383 .unwrap_or_else(|| DEFAULT_HOST.to_string());
384
385 let port = cli
387 .port
388 .or(env.port)
389 .or(cqlshrc.connection.port)
390 .unwrap_or(DEFAULT_PORT);
391
392 let username = cli
394 .username
395 .clone()
396 .or_else(|| cqlshrc.authentication.username.clone());
397
398 let password = cli
400 .password
401 .clone()
402 .or_else(|| cqlshrc.authentication.password.clone());
403
404 let keyspace = cli
406 .keyspace
407 .clone()
408 .or_else(|| cqlshrc.authentication.keyspace.clone());
409
410 let color = if cli.color {
412 ColorMode::On
413 } else if cli.no_color {
414 ColorMode::Off
415 } else {
416 match &cqlshrc.ui.color {
417 Some(true) => ColorMode::On,
418 Some(false) => ColorMode::Off,
419 None => ColorMode::Auto,
420 }
421 };
422
423 let connect_timeout = cli
425 .connect_timeout
426 .or(env.connect_timeout)
427 .or(cqlshrc.connection.connect_timeout)
428 .unwrap_or(DEFAULT_CONNECT_TIMEOUT);
429
430 let request_timeout = cli
432 .request_timeout
433 .or(env.request_timeout)
434 .or(cqlshrc.connection.request_timeout)
435 .unwrap_or(DEFAULT_REQUEST_TIMEOUT);
436
437 let encoding = cli
439 .encoding
440 .clone()
441 .or_else(|| cqlshrc.ui.encoding.clone())
442 .unwrap_or_else(|| "utf-8".to_string());
443
444 let cqlversion = cli
446 .cqlversion
447 .clone()
448 .or_else(|| cqlshrc.cql.version.clone());
449
450 let browser = cli.browser.clone().or_else(|| cqlshrc.ui.browser.clone());
452
453 MergedConfig {
454 host,
455 port,
456 username,
457 password,
458 keyspace,
459 ssl: cli.ssl,
460 color,
461 debug: cli.debug,
462 tty: cli.tty,
463 no_file_io: cli.no_file_io,
464 no_compact: cli.no_compact,
465 disable_history: cli.disable_history,
466 execute: cli.execute.clone(),
467 file: cli.file.clone(),
468 connect_timeout,
469 request_timeout,
470 encoding,
471 cqlversion,
472 protocol_version: cli.protocol_version,
473 consistency_level: cli.consistency_level.clone(),
474 serial_consistency_level: cli.serial_consistency_level.clone(),
475 browser,
476 secure_connect_bundle: cli.secure_connect_bundle.clone(),
477 cqlshrc_path,
478 cqlshrc,
479 }
480 }
481}
482
483pub fn resolve_cqlshrc_path(cli_path: Option<&str>) -> PathBuf {
485 if let Some(path) = cli_path {
486 PathBuf::from(path)
487 } else {
488 default_cqlshrc_path()
489 }
490}
491
492pub fn default_cqlshrc_path() -> PathBuf {
494 dirs::home_dir()
495 .unwrap_or_else(|| PathBuf::from("."))
496 .join(".cassandra")
497 .join("cqlshrc")
498}
499
500pub fn load_config(cli: &CliArgs) -> Result<MergedConfig> {
502 let cqlshrc_path = resolve_cqlshrc_path(cli.cqlshrc.as_deref());
503 let cqlshrc = CqlshrcConfig::load(&cqlshrc_path)
504 .with_context(|| format!("loading cqlshrc from {}", cqlshrc_path.display()))?;
505 let env = EnvConfig::from_env();
506 Ok(MergedConfig::build(cli, &env, cqlshrc, cqlshrc_path))
507}
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512
513 #[test]
516 fn parse_bool_true_variants() {
517 assert!(parse_bool("true"));
518 assert!(parse_bool("True"));
519 assert!(parse_bool("TRUE"));
520 assert!(parse_bool("yes"));
521 assert!(parse_bool("Yes"));
522 assert!(parse_bool("on"));
523 assert!(parse_bool("ON"));
524 assert!(parse_bool("1"));
525 }
526
527 #[test]
528 fn parse_bool_false_variants() {
529 assert!(!parse_bool("false"));
530 assert!(!parse_bool("False"));
531 assert!(!parse_bool("no"));
532 assert!(!parse_bool("off"));
533 assert!(!parse_bool("0"));
534 assert!(!parse_bool(""));
535 assert!(!parse_bool("anything"));
536 }
537
538 #[test]
541 fn parse_empty_config() {
542 let config = CqlshrcConfig::parse("").unwrap();
543 assert!(config.authentication.username.is_none());
544 assert!(config.connection.hostname.is_none());
545 assert!(config.ui.color.is_none());
546 }
547
548 #[test]
549 fn parse_authentication_section() {
550 let config = CqlshrcConfig::parse(
551 "[authentication]\nusername = admin\npassword = secret\nkeyspace = test_ks\n",
552 )
553 .unwrap();
554 assert_eq!(config.authentication.username.as_deref(), Some("admin"));
555 assert_eq!(config.authentication.password.as_deref(), Some("secret"));
556 assert_eq!(config.authentication.keyspace.as_deref(), Some("test_ks"));
557 }
558
559 #[test]
560 fn parse_connection_section() {
561 let config = CqlshrcConfig::parse(
562 "[connection]\nhostname = 10.0.0.1\nport = 9043\ntimeout = 30\nrequest_timeout = 60\nconnect_timeout = 15\n",
563 )
564 .unwrap();
565 assert_eq!(config.connection.hostname.as_deref(), Some("10.0.0.1"));
566 assert_eq!(config.connection.port, Some(9043));
567 assert_eq!(config.connection.timeout, Some(30));
568 assert_eq!(config.connection.request_timeout, Some(60));
569 assert_eq!(config.connection.connect_timeout, Some(15));
570 }
571
572 #[test]
573 fn parse_ssl_section() {
574 let config = CqlshrcConfig::parse(
575 "[ssl]\ncertfile = /path/to/cert.pem\nvalidate = true\nuserkey = /path/to/key.pem\nusercert = /path/to/usercert.pem\nversion = TLSv1_2\n",
576 )
577 .unwrap();
578 assert_eq!(config.ssl.certfile.as_deref(), Some("/path/to/cert.pem"));
579 assert_eq!(config.ssl.validate, Some(true));
580 assert_eq!(config.ssl.userkey.as_deref(), Some("/path/to/key.pem"));
581 assert_eq!(config.ssl.version.as_deref(), Some("TLSv1_2"));
582 }
583
584 #[test]
585 fn parse_ui_section() {
586 let config = CqlshrcConfig::parse(
587 "[ui]\ncolor = on\ndatetimeformat = %Y-%m-%d %H:%M:%S%z\ntimezone = UTC\nfloat_precision = 5\ndouble_precision = 12\nmax_trace_wait = 10.0\nencoding = utf-8\ncompletekey = tab\n",
588 )
589 .unwrap();
590 assert_eq!(config.ui.color, Some(true));
591 assert_eq!(
592 config.ui.datetimeformat.as_deref(),
593 Some("%Y-%m-%d %H:%M:%S%z")
594 );
595 assert_eq!(config.ui.timezone.as_deref(), Some("UTC"));
596 assert_eq!(config.ui.float_precision, Some(5));
597 assert_eq!(config.ui.double_precision, Some(12));
598 assert_eq!(config.ui.max_trace_wait, Some(10.0));
599 assert_eq!(config.ui.encoding.as_deref(), Some("utf-8"));
600 assert_eq!(config.ui.completekey.as_deref(), Some("tab"));
601 }
602
603 #[test]
604 fn parse_cql_section() {
605 let config = CqlshrcConfig::parse("[cql]\nversion = 3.4.7\n").unwrap();
606 assert_eq!(config.cql.version.as_deref(), Some("3.4.7"));
607 }
608
609 #[test]
610 fn parse_csv_section() {
611 let config = CqlshrcConfig::parse("[csv]\nfield_size_limit = 131072\n").unwrap();
612 assert_eq!(config.csv.field_size_limit, Some(131072));
613 }
614
615 #[test]
616 fn parse_copy_section() {
617 let config = CqlshrcConfig::parse(
618 "[copy]\nnumprocesses = 4\nmaxattempts = 5\nreportfrequency = 0.25\n",
619 )
620 .unwrap();
621 assert_eq!(config.copy.numprocesses, Some(4));
622 assert_eq!(config.copy.maxattempts, Some(5));
623 assert_eq!(config.copy.reportfrequency, Some(0.25));
624 }
625
626 #[test]
627 fn parse_copy_to_section() {
628 let config = CqlshrcConfig::parse(
629 "[copy-to]\npagesize = 1000\npagetimeout = 10\nmaxrequests = 6\nmaxoutputsize = -1\nfloatprecision = 5\ndoubleprecision = 12\n",
630 )
631 .unwrap();
632 assert_eq!(config.copy_to.pagesize, Some(1000));
633 assert_eq!(config.copy_to.pagetimeout, Some(10));
634 assert_eq!(config.copy_to.maxrequests, Some(6));
635 assert_eq!(config.copy_to.maxoutputsize, Some(-1));
636 assert_eq!(config.copy_to.floatprecision, Some(5));
637 assert_eq!(config.copy_to.doubleprecision, Some(12));
638 }
639
640 #[test]
641 fn parse_copy_from_section() {
642 let config = CqlshrcConfig::parse(
643 "[copy-from]\nmaxbatchsize = 20\nminbatchsize = 10\nchunksize = 5000\ningestrate = 100000\nmaxparseerrors = -1\nmaxinserterrors = 1000\npreparedstatements = true\nttl = 3600\n",
644 )
645 .unwrap();
646 assert_eq!(config.copy_from.maxbatchsize, Some(20));
647 assert_eq!(config.copy_from.minbatchsize, Some(10));
648 assert_eq!(config.copy_from.chunksize, Some(5000));
649 assert_eq!(config.copy_from.ingestrate, Some(100000));
650 assert_eq!(config.copy_from.maxparseerrors, Some(-1));
651 assert_eq!(config.copy_from.maxinserterrors, Some(1000));
652 assert_eq!(config.copy_from.preparedstatements, Some(true));
653 assert_eq!(config.copy_from.ttl, Some(3600));
654 }
655
656 #[test]
657 fn parse_tracing_section() {
658 let config = CqlshrcConfig::parse("[tracing]\nmax_trace_wait = 10.0\n").unwrap();
659 assert_eq!(config.tracing.max_trace_wait, Some(10.0));
660 }
661
662 #[test]
663 fn parse_certfiles_section() {
664 let config = CqlshrcConfig::parse(
665 "[certfiles]\n172.31.10.22 = ~/keys/node0.cer.pem\n172.31.8.141 = ~/keys/node1.cer.pem\n",
666 )
667 .unwrap();
668 assert_eq!(
669 config.certfiles.get("172.31.10.22").map(|s| s.as_str()),
670 Some("~/keys/node0.cer.pem")
671 );
672 assert_eq!(
673 config.certfiles.get("172.31.8.141").map(|s| s.as_str()),
674 Some("~/keys/node1.cer.pem")
675 );
676 }
677
678 #[test]
679 fn parse_full_sample_config() {
680 let content = r#"
681[authentication]
682username = cassandra
683password = cassandra
684keyspace = my_keyspace
685
686[connection]
687hostname = 127.0.0.1
688port = 9042
689timeout = 10
690request_timeout = 10
691connect_timeout = 5
692
693[ssl]
694certfile = /path/to/ca-cert.pem
695validate = true
696
697[ui]
698color = on
699datetimeformat = %Y-%m-%d %H:%M:%S%z
700timezone = UTC
701float_precision = 5
702double_precision = 12
703
704[cql]
705version = 3.4.7
706
707[csv]
708field_size_limit = 131072
709
710[copy]
711numprocesses = 4
712maxattempts = 5
713
714[copy-to]
715pagesize = 1000
716floatprecision = 5
717
718[copy-from]
719maxbatchsize = 20
720chunksize = 5000
721preparedstatements = true
722ttl = 3600
723
724[tracing]
725max_trace_wait = 10.0
726"#;
727 let config = CqlshrcConfig::parse(content).unwrap();
728 assert_eq!(config.authentication.username.as_deref(), Some("cassandra"));
729 assert_eq!(config.connection.port, Some(9042));
730 assert_eq!(config.ui.color, Some(true));
731 assert_eq!(config.cql.version.as_deref(), Some("3.4.7"));
732 assert_eq!(config.copy.numprocesses, Some(4));
733 assert_eq!(config.copy_to.pagesize, Some(1000));
734 assert_eq!(config.copy_from.ttl, Some(3600));
735 assert_eq!(config.tracing.max_trace_wait, Some(10.0));
736 }
737
738 #[test]
739 fn parse_unknown_keys_ignored() {
740 let config =
741 CqlshrcConfig::parse("[authentication]\nunknown_key = value\nusername = test\n")
742 .unwrap();
743 assert_eq!(config.authentication.username.as_deref(), Some("test"));
744 }
745
746 #[test]
747 fn load_nonexistent_file_returns_default() {
748 let config = CqlshrcConfig::load(Path::new("/nonexistent/path/cqlshrc")).unwrap();
749 assert!(config.authentication.username.is_none());
750 assert!(config.connection.hostname.is_none());
751 }
752
753 fn default_cli() -> CliArgs {
756 CliArgs {
757 host: None,
758 port: None,
759 color: false,
760 no_color: false,
761 browser: None,
762 ssl: false,
763 no_file_io: false,
764 debug: false,
765 coverage: false,
766 execute: None,
767 file: None,
768 keyspace: None,
769 username: None,
770 password: None,
771 connect_timeout: None,
772 request_timeout: None,
773 tty: false,
774 encoding: None,
775 cqlshrc: None,
776 cqlversion: None,
777 protocol_version: None,
778 consistency_level: None,
779 serial_consistency_level: None,
780 no_compact: false,
781 disable_history: false,
782 secure_connect_bundle: None,
783 completions: None,
784 generate_man: false,
785 }
786 }
787
788 #[test]
789 fn merged_defaults() {
790 let cli = default_cli();
791 let env = EnvConfig::default();
792 let cqlshrc = CqlshrcConfig::default();
793 let config = MergedConfig::build(&cli, &env, cqlshrc, default_cqlshrc_path());
794
795 assert_eq!(config.host, "127.0.0.1");
796 assert_eq!(config.port, 9042);
797 assert!(config.username.is_none());
798 assert!(config.password.is_none());
799 assert!(config.keyspace.is_none());
800 assert!(!config.ssl);
801 assert_eq!(config.color, ColorMode::Auto);
802 assert_eq!(config.connect_timeout, DEFAULT_CONNECT_TIMEOUT);
803 assert_eq!(config.request_timeout, DEFAULT_REQUEST_TIMEOUT);
804 assert_eq!(config.encoding, "utf-8");
805 }
806
807 #[test]
808 fn cli_overrides_everything() {
809 let cli = CliArgs {
810 host: Some("cli-host".to_string()),
811 port: Some(9999),
812 username: Some("cli-user".to_string()),
813 connect_timeout: Some(99),
814 ..default_cli()
815 };
816 let env = EnvConfig {
817 host: Some("env-host".to_string()),
818 port: Some(8888),
819 connect_timeout: Some(88),
820 ..EnvConfig::default()
821 };
822 let mut cqlshrc = CqlshrcConfig::default();
823 cqlshrc.connection.hostname = Some("cqlshrc-host".to_string());
824 cqlshrc.connection.port = Some(7777);
825 cqlshrc.connection.connect_timeout = Some(77);
826 cqlshrc.authentication.username = Some("cqlshrc-user".to_string());
827
828 let config = MergedConfig::build(&cli, &env, cqlshrc, default_cqlshrc_path());
829
830 assert_eq!(config.host, "cli-host");
831 assert_eq!(config.port, 9999);
832 assert_eq!(config.username.as_deref(), Some("cli-user"));
833 assert_eq!(config.connect_timeout, 99);
834 }
835
836 #[test]
837 fn env_overrides_cqlshrc() {
838 let cli = default_cli();
839 let env = EnvConfig {
840 host: Some("env-host".to_string()),
841 port: Some(8888),
842 connect_timeout: Some(88),
843 ..EnvConfig::default()
844 };
845 let mut cqlshrc = CqlshrcConfig::default();
846 cqlshrc.connection.hostname = Some("cqlshrc-host".to_string());
847 cqlshrc.connection.port = Some(7777);
848 cqlshrc.connection.connect_timeout = Some(77);
849
850 let config = MergedConfig::build(&cli, &env, cqlshrc, default_cqlshrc_path());
851
852 assert_eq!(config.host, "env-host");
853 assert_eq!(config.port, 8888);
854 assert_eq!(config.connect_timeout, 88);
855 }
856
857 #[test]
858 fn cqlshrc_overrides_defaults() {
859 let cli = default_cli();
860 let env = EnvConfig::default();
861 let mut cqlshrc = CqlshrcConfig::default();
862 cqlshrc.connection.hostname = Some("cqlshrc-host".to_string());
863 cqlshrc.connection.port = Some(7777);
864 cqlshrc.connection.connect_timeout = Some(77);
865 cqlshrc.connection.request_timeout = Some(99);
866 cqlshrc.authentication.username = Some("cqlshrc-user".to_string());
867 cqlshrc.authentication.keyspace = Some("cqlshrc-ks".to_string());
868
869 let config = MergedConfig::build(&cli, &env, cqlshrc, default_cqlshrc_path());
870
871 assert_eq!(config.host, "cqlshrc-host");
872 assert_eq!(config.port, 7777);
873 assert_eq!(config.connect_timeout, 77);
874 assert_eq!(config.request_timeout, 99);
875 assert_eq!(config.username.as_deref(), Some("cqlshrc-user"));
876 assert_eq!(config.keyspace.as_deref(), Some("cqlshrc-ks"));
877 }
878
879 #[test]
880 fn color_mode_cli_on() {
881 let cli = CliArgs {
882 color: true,
883 ..default_cli()
884 };
885 let config = MergedConfig::build(
886 &cli,
887 &EnvConfig::default(),
888 CqlshrcConfig::default(),
889 default_cqlshrc_path(),
890 );
891 assert_eq!(config.color, ColorMode::On);
892 }
893
894 #[test]
895 fn color_mode_cli_off() {
896 let cli = CliArgs {
897 no_color: true,
898 ..default_cli()
899 };
900 let config = MergedConfig::build(
901 &cli,
902 &EnvConfig::default(),
903 CqlshrcConfig::default(),
904 default_cqlshrc_path(),
905 );
906 assert_eq!(config.color, ColorMode::Off);
907 }
908
909 #[test]
910 fn color_mode_cqlshrc_on() {
911 let mut cqlshrc = CqlshrcConfig::default();
912 cqlshrc.ui.color = Some(true);
913 let config = MergedConfig::build(
914 &default_cli(),
915 &EnvConfig::default(),
916 cqlshrc,
917 default_cqlshrc_path(),
918 );
919 assert_eq!(config.color, ColorMode::On);
920 }
921
922 #[test]
923 fn color_mode_cqlshrc_off() {
924 let mut cqlshrc = CqlshrcConfig::default();
925 cqlshrc.ui.color = Some(false);
926 let config = MergedConfig::build(
927 &default_cli(),
928 &EnvConfig::default(),
929 cqlshrc,
930 default_cqlshrc_path(),
931 );
932 assert_eq!(config.color, ColorMode::Off);
933 }
934
935 #[test]
936 fn color_mode_auto_when_unset() {
937 let config = MergedConfig::build(
938 &default_cli(),
939 &EnvConfig::default(),
940 CqlshrcConfig::default(),
941 default_cqlshrc_path(),
942 );
943 assert_eq!(config.color, ColorMode::Auto);
944 }
945
946 #[test]
947 fn encoding_precedence() {
948 let mut cqlshrc = CqlshrcConfig::default();
950 cqlshrc.ui.encoding = Some("latin-1".to_string());
951
952 let config = MergedConfig::build(
954 &default_cli(),
955 &EnvConfig::default(),
956 cqlshrc.clone(),
957 default_cqlshrc_path(),
958 );
959 assert_eq!(config.encoding, "latin-1");
960
961 let cli = CliArgs {
963 encoding: Some("utf-16".to_string()),
964 ..default_cli()
965 };
966 let config =
967 MergedConfig::build(&cli, &EnvConfig::default(), cqlshrc, default_cqlshrc_path());
968 assert_eq!(config.encoding, "utf-16");
969 }
970
971 #[test]
972 fn cqlversion_precedence() {
973 let mut cqlshrc = CqlshrcConfig::default();
974 cqlshrc.cql.version = Some("3.4.5".to_string());
975
976 let config = MergedConfig::build(
977 &default_cli(),
978 &EnvConfig::default(),
979 cqlshrc.clone(),
980 default_cqlshrc_path(),
981 );
982 assert_eq!(config.cqlversion.as_deref(), Some("3.4.5"));
983
984 let cli = CliArgs {
985 cqlversion: Some("3.4.7".to_string()),
986 ..default_cli()
987 };
988 let config =
989 MergedConfig::build(&cli, &EnvConfig::default(), cqlshrc, default_cqlshrc_path());
990 assert_eq!(config.cqlversion.as_deref(), Some("3.4.7"));
991 }
992
993 #[test]
994 fn resolve_cqlshrc_path_custom() {
995 let path = resolve_cqlshrc_path(Some("/etc/custom/cqlshrc"));
996 assert_eq!(path, PathBuf::from("/etc/custom/cqlshrc"));
997 }
998
999 #[test]
1000 fn resolve_cqlshrc_path_default() {
1001 let path = resolve_cqlshrc_path(None);
1002 assert!(path.ends_with(".cassandra/cqlshrc"));
1003 }
1004
1005 #[test]
1008 fn load_config_from_tempfile() {
1009 let dir = tempfile::tempdir().unwrap();
1010 let cqlshrc_path = dir.path().join("cqlshrc");
1011 std::fs::write(
1012 &cqlshrc_path,
1013 "[authentication]\nusername = file_user\n[connection]\nport = 9999\n",
1014 )
1015 .unwrap();
1016
1017 let config = CqlshrcConfig::load(&cqlshrc_path).unwrap();
1018 assert_eq!(config.authentication.username.as_deref(), Some("file_user"));
1019 assert_eq!(config.connection.port, Some(9999));
1020 }
1021
1022 #[test]
1023 fn ssl_validate_false() {
1024 let config = CqlshrcConfig::parse("[ssl]\nvalidate = false\n").unwrap();
1025 assert_eq!(config.ssl.validate, Some(false));
1026 }
1027
1028 #[test]
1029 fn copy_from_preparedstatements_false() {
1030 let config = CqlshrcConfig::parse("[copy-from]\npreparedstatements = false\n").unwrap();
1031 assert_eq!(config.copy_from.preparedstatements, Some(false));
1032 }
1033
1034 #[test]
1035 fn invalid_numeric_ignored() {
1036 let config = CqlshrcConfig::parse("[connection]\nport = not_a_number\n").unwrap();
1037 assert!(config.connection.port.is_none());
1038 }
1039
1040 #[test]
1041 fn copy_to_empty_begintoken() {
1042 let config = CqlshrcConfig::parse("[copy-to]\nbegintoken = \n").unwrap();
1043 assert!(config.copy_to.begintoken.is_none());
1044 }
1045}