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