1use std::{
2 borrow::Cow,
3 collections::HashMap,
4 fmt,
5 str::FromStr,
6 sync::{Arc, Mutex},
7 time::Duration,
8};
9
10use crate::errors::{Error, Result, UrlError};
11use percent_encoding::percent_decode;
12use url::Url;
13
14const DEFAULT_MIN_CONNS: usize = 10;
15
16const DEFAULT_MAX_CONNS: usize = 20;
17
18#[derive(Debug)]
19#[allow(clippy::large_enum_variant)]
20enum State {
21 Raw(Options),
22 Url(String),
23}
24
25#[derive(Clone)]
26pub struct OptionsSource {
27 state: Arc<Mutex<State>>,
28}
29
30impl fmt::Debug for OptionsSource {
31 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
32 let guard = self.state.lock().unwrap();
33 match *guard {
34 State::Url(ref url) => write!(f, "Url({url})"),
35 State::Raw(ref options) => write!(f, "{options:?}"),
36 }
37 }
38}
39
40impl OptionsSource {
41 pub(crate) fn get(&self) -> Result<Cow<Options>> {
42 let mut state = self.state.lock().unwrap();
43 loop {
44 *state = match &*state {
45 State::Raw(ref options) => {
46 let ptr = options as *const Options;
47 return unsafe { Ok(Cow::Borrowed(ptr.as_ref().unwrap())) };
48 }
49 State::Url(url) => {
50 let options = from_url(url)?;
51 State::Raw(options)
52 }
53 };
54 }
55 }
56}
57
58impl Default for OptionsSource {
59 fn default() -> Self {
60 Self {
61 state: Arc::new(Mutex::new(State::Raw(Options::default()))),
62 }
63 }
64}
65
66pub trait IntoOptions {
67 fn into_options_src(self) -> OptionsSource;
68}
69
70impl IntoOptions for Options {
71 fn into_options_src(self) -> OptionsSource {
72 OptionsSource {
73 state: Arc::new(Mutex::new(State::Raw(self))),
74 }
75 }
76}
77
78impl IntoOptions for &str {
79 fn into_options_src(self) -> OptionsSource {
80 OptionsSource {
81 state: Arc::new(Mutex::new(State::Url(self.into()))),
82 }
83 }
84}
85
86impl IntoOptions for String {
87 fn into_options_src(self) -> OptionsSource {
88 OptionsSource {
89 state: Arc::new(Mutex::new(State::Url(self))),
90 }
91 }
92}
93
94#[cfg(feature = "tls-native-tls")]
96#[derive(Clone)]
97pub struct Certificate(Arc<native_tls::Certificate>);
98#[cfg(feature = "tls-native-tls")]
99impl Certificate {
100 pub fn from_der(der: &[u8]) -> Result<Certificate> {
102 let inner = match native_tls::Certificate::from_der(der) {
103 Ok(certificate) => certificate,
104 Err(err) => return Err(Error::Other(err.to_string().into())),
105 };
106 Ok(Certificate(Arc::new(inner)))
107 }
108
109 pub fn from_pem(der: &[u8]) -> Result<Certificate> {
111 let inner = match native_tls::Certificate::from_pem(der) {
112 Ok(certificate) => certificate,
113 Err(err) => return Err(Error::Other(err.to_string().into())),
114 };
115 Ok(Certificate(Arc::new(inner)))
116 }
117}
118#[cfg(feature = "tls-native-tls")]
119impl From<Certificate> for native_tls::Certificate {
120 fn from(value: Certificate) -> Self {
121 value.0.as_ref().clone()
122 }
123}
124
125#[cfg(feature = "tls-rustls")]
127#[derive(Clone)]
128pub struct Certificate(Arc<Vec<rustls::pki_types::CertificateDer<'static>>>);
129#[cfg(feature = "tls-rustls")]
130impl Certificate {
131 pub fn from_der(der: &[u8]) -> Result<Certificate> {
133 let der = der.to_vec();
134 let inner = match rustls::pki_types::CertificateDer::try_from(der) {
135 Ok(certificate) => certificate,
136 Err(err) => return Err(Error::Other(err.to_string().into())),
137 };
138 Ok(Certificate(Arc::new(vec![inner])))
139 }
140
141 pub fn from_pem(der: &[u8]) -> Result<Certificate> {
143 let certs = rustls_pemfile::certs(&mut der.as_ref())
144 .map(|result| result.unwrap())
145 .collect();
146 Ok(Certificate(Arc::new(certs)))
147 }
148}
149#[cfg(feature = "tls-rustls")]
150impl From<Certificate> for Vec<rustls::pki_types::CertificateDer<'static>> {
151 fn from(value: Certificate) -> Self {
152 value.0.as_ref().clone()
153 }
154}
155
156#[cfg(feature = "_tls")]
157impl fmt::Debug for Certificate {
158 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
159 write!(f, "[Certificate]")
160 }
161}
162#[cfg(feature = "_tls")]
163impl PartialEq for Certificate {
164 fn eq(&self, _other: &Self) -> bool {
165 true
166 }
167}
168
169#[derive(Clone, PartialEq, Debug)]
170pub enum SettingType {
171 String(String),
172 Bool(bool),
173 UInt64(u64),
174 Float64(f64),
175}
176
177impl fmt::Display for SettingType {
178 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179 match &self {
180 SettingType::Bool(val) => write!(f, "{val}"),
181 SettingType::UInt64(val) => write!(f, "{val}"),
182 SettingType::Float64(val) => write!(f, "{val}"),
183 SettingType::String(val) => write!(f, "{val}"),
184 }
185 }
186}
187
188impl From<&str> for SettingType {
189 fn from(val: &str) -> Self {
190 SettingType::String(val.into())
191 }
192}
193
194impl From<bool> for SettingType {
195 fn from(val: bool) -> Self {
196 SettingType::Bool(val)
197 }
198}
199
200impl From<u64> for SettingType {
201 fn from(val: u64) -> Self {
202 SettingType::UInt64(val)
203 }
204}
205
206impl From<i32> for SettingType {
207 fn from(val: i32) -> Self {
208 SettingType::UInt64(val as u64)
209 }
210}
211
212impl From<i64> for SettingType {
213 fn from(val: i64) -> Self {
214 SettingType::UInt64(val as u64)
215 }
216}
217
218impl From<f64> for SettingType {
219 fn from(val: f64) -> Self {
220 SettingType::Float64(val)
221 }
222}
223
224#[derive(Clone, PartialEq, Debug)]
225pub struct SettingValue {
226 pub(crate) value: SettingType,
227 pub(crate) is_important: bool,
228}
229impl fmt::Display for SettingValue {
230 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231 self.value.fmt(f)
232 }
233}
234
235#[derive(Clone, PartialEq)]
237pub struct Options {
238 pub(crate) addr: Url,
240
241 pub(crate) database: String,
243 pub(crate) username: String,
245 pub(crate) password: String,
247
248 pub(crate) compression: bool,
250
251 pub(crate) pool_min: usize,
253 pub(crate) pool_max: usize,
255
256 pub(crate) nodelay: bool,
258 pub(crate) keepalive: Option<Duration>,
260
261 pub(crate) ping_before_query: bool,
263 pub(crate) send_retries: usize,
265 pub(crate) retry_timeout: Duration,
267 pub(crate) ping_timeout: Duration,
269
270 pub(crate) connection_timeout: Duration,
272
273 pub(crate) query_timeout: Duration,
275
276 pub(crate) insert_timeout: Option<Duration>,
278
279 pub(crate) execute_timeout: Option<Duration>,
281
282 #[cfg(feature = "_tls")]
284 pub(crate) secure: bool,
285
286 #[cfg(feature = "_tls")]
288 pub(crate) skip_verify: bool,
289
290 #[cfg(feature = "_tls")]
292 pub(crate) certificate: Option<Certificate>,
293
294 pub(crate) settings: HashMap<String, SettingValue>,
296
297 pub(crate) alt_hosts: Vec<Url>,
299}
300
301impl fmt::Debug for Options {
302 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
303 f.debug_struct("Options")
304 .field("addr", &self.addr)
305 .field("database", &self.database)
306 .field("compression", &self.compression)
307 .field("pool_min", &self.pool_min)
308 .field("pool_max", &self.pool_max)
309 .field("nodelay", &self.nodelay)
310 .field("keepalive", &self.keepalive)
311 .field("ping_before_query", &self.ping_before_query)
312 .field("send_retries", &self.send_retries)
313 .field("retry_timeout", &self.retry_timeout)
314 .field("ping_timeout", &self.ping_timeout)
315 .field("connection_timeout", &self.connection_timeout)
316 .field("settings", &self.settings)
317 .field("alt_hosts", &self.alt_hosts)
318 .finish()
319 }
320}
321
322impl Default for Options {
323 fn default() -> Self {
324 Self {
325 addr: Url::parse("tcp://default@127.0.0.1:9000").unwrap(),
326 database: "default".into(),
327 username: "default".into(),
328 password: "".into(),
329 compression: false,
330 pool_min: DEFAULT_MIN_CONNS,
331 pool_max: DEFAULT_MAX_CONNS,
332 nodelay: true,
333 keepalive: None,
334 ping_before_query: true,
335 send_retries: 3,
336 retry_timeout: Duration::from_secs(5),
337 ping_timeout: Duration::from_millis(500),
338 connection_timeout: Duration::from_millis(500),
339 query_timeout: Duration::from_secs(180),
340 insert_timeout: Some(Duration::from_secs(180)),
341 execute_timeout: Some(Duration::from_secs(180)),
342 #[cfg(feature = "_tls")]
343 secure: false,
344 #[cfg(feature = "_tls")]
345 skip_verify: false,
346 #[cfg(feature = "_tls")]
347 certificate: None,
348 settings: HashMap::new(),
349 alt_hosts: Vec::new(),
350 }
351 }
352}
353
354macro_rules! property {
355 ( $k:ident: $t:ty ) => {
356 pub fn $k(self, $k: $t) -> Self {
357 Self {
358 $k: $k.into(),
359 ..self
360 }
361 }
362 };
363 ( $(#[$attr:meta])* => $k:ident: $t:ty ) => {
364 $(#[$attr])*
365 pub fn $k(self, $k: $t) -> Self {
366 Self {
367 $k: $k.into(),
368 ..self
369 }
370 }
371 }
372}
373
374impl Options {
375 pub fn new<A>(addr: A) -> Self
377 where
378 A: Into<Url>,
379 {
380 Self {
381 addr: addr.into(),
382 ..Self::default()
383 }
384 }
385
386 pub fn with_setting<V>(mut self, name: &str, value: V, is_important: bool) -> Self
387 where
388 V: Into<SettingType>,
389 {
390 let value: SettingType = value.into();
391 self.settings.insert(
392 name.into(),
393 SettingValue {
394 value,
395 is_important,
396 },
397 );
398 self
399 }
400
401 property! {
402 => database: &str
404 }
405
406 property! {
407 => username: &str
409 }
410
411 property! {
412 => password: &str
414 }
415
416 pub fn with_compression(self) -> Self {
418 Self {
419 compression: true,
420 ..self
421 }
422 }
423
424 property! {
425 => pool_min: usize
427 }
428
429 property! {
430 => pool_max: usize
432 }
433
434 property! {
435 => nodelay: bool
437 }
438
439 property! {
440 => keepalive: Option<Duration>
442 }
443
444 property! {
445 => ping_before_query: bool
447 }
448
449 property! {
450 => send_retries: usize
452 }
453
454 property! {
455 => retry_timeout: Duration
457 }
458
459 property! {
460 => ping_timeout: Duration
462 }
463
464 property! {
465 => connection_timeout: Duration
467 }
468
469 property! {
470 => query_timeout: Duration
472 }
473
474 property! {
475 => insert_timeout: Option<Duration>
477 }
478
479 property! {
480 => execute_timeout: Option<Duration>
482 }
483
484 #[cfg(feature = "_tls")]
485 property! {
486 => secure: bool
488 }
489
490 #[cfg(feature = "_tls")]
491 property! {
492 => skip_verify: bool
494 }
495
496 #[cfg(feature = "_tls")]
497 property! {
498 => certificate: Option<Certificate>
500 }
501
502 property! {
503 => settings: HashMap<String, SettingValue>
505 }
506
507 property! {
508 => alt_hosts: Vec<Url>
510 }
511}
512
513impl FromStr for Options {
514 type Err = Error;
515
516 fn from_str(url: &str) -> Result<Self> {
517 from_url(url)
518 }
519}
520
521fn from_url(url_str: &str) -> Result<Options> {
522 let url = Url::parse(url_str)?;
523
524 if url.scheme() != "tcp" {
525 return Err(UrlError::UnsupportedScheme {
526 scheme: url.scheme().to_string(),
527 }
528 .into());
529 }
530
531 if url.cannot_be_a_base() || !url.has_host() {
532 return Err(UrlError::Invalid.into());
533 }
534
535 let mut options = Options::default();
536
537 if let Some(username) = get_username_from_url(&url) {
538 options.username = username.into();
539 }
540
541 if let Some(password) = get_password_from_url(&url) {
542 options.password = password.into()
543 }
544
545 let mut addr = url.clone();
546 addr.set_path("");
547 addr.set_query(None);
548
549 let port = url.port().or(Some(9000));
550 addr.set_port(port).map_err(|_| UrlError::Invalid)?;
551 options.addr = addr;
552
553 if let Some(database) = get_database_from_url(&url)? {
554 options.database = database.into();
555 }
556
557 set_params(&mut options, url.query_pairs())?;
558
559 Ok(options)
560}
561
562fn set_params<'a, I>(options: &mut Options, iter: I) -> std::result::Result<(), UrlError>
563where
564 I: Iterator<Item = (Cow<'a, str>, Cow<'a, str>)>,
565{
566 for (key, value) in iter {
567 match key.as_ref() {
568 "pool_min" => options.pool_min = parse_param(key, value, usize::from_str)?,
569 "pool_max" => options.pool_max = parse_param(key, value, usize::from_str)?,
570 "nodelay" => options.nodelay = parse_param(key, value, bool::from_str)?,
571 "keepalive" => options.keepalive = parse_param(key, value, parse_opt_duration)?,
572 "ping_before_query" => {
573 options.ping_before_query = parse_param(key, value, bool::from_str)?
574 }
575 "send_retries" => options.send_retries = parse_param(key, value, usize::from_str)?,
576 "retry_timeout" => options.retry_timeout = parse_param(key, value, parse_duration)?,
577 "ping_timeout" => options.ping_timeout = parse_param(key, value, parse_duration)?,
578 "connection_timeout" => {
579 options.connection_timeout = parse_param(key, value, parse_duration)?
580 }
581 "query_timeout" => options.query_timeout = parse_param(key, value, parse_duration)?,
582 "insert_timeout" => {
583 options.insert_timeout = parse_param(key, value, parse_opt_duration)?
584 }
585 "execute_timeout" => {
586 options.execute_timeout = parse_param(key, value, parse_opt_duration)?
587 }
588 "compression" => options.compression = parse_param(key, value, parse_compression)?,
589 #[cfg(feature = "_tls")]
590 "secure" => options.secure = parse_param(key, value, bool::from_str)?,
591 #[cfg(feature = "_tls")]
592 "skip_verify" => options.skip_verify = parse_param(key, value, bool::from_str)?,
593 "alt_hosts" => options.alt_hosts = parse_param(key, value, parse_hosts)?,
594 _ => {
595 let value = SettingType::String(value.to_string());
596 options.settings.insert(
597 key.to_string(),
598 SettingValue {
599 value,
600 is_important: true,
601 },
602 );
603 }
604 };
605 }
606
607 Ok(())
608}
609
610fn parse_param<'a, F, T, E>(
611 param: Cow<'a, str>,
612 value: Cow<'a, str>,
613 parse: F,
614) -> std::result::Result<T, UrlError>
615where
616 F: Fn(&str) -> std::result::Result<T, E>,
617{
618 let source = percent_decode(value.as_bytes()).decode_utf8_lossy();
619 match parse(source.as_ref()) {
620 Ok(value) => Ok(value),
621 Err(_) => Err(UrlError::InvalidParamValue {
622 param: param.into(),
623 value: value.into(),
624 }),
625 }
626}
627
628fn get_username_from_url(url: &Url) -> Option<Cow<'_, str>> {
629 let user = url.username();
630 if user.is_empty() {
631 return None;
632 }
633 Some(percent_decode(user.as_bytes()).decode_utf8_lossy())
634}
635
636fn get_password_from_url(url: &Url) -> Option<Cow<'_, str>> {
637 let password = url.password()?;
638 Some(percent_decode(password.as_bytes()).decode_utf8_lossy())
639}
640
641fn get_database_from_url(url: &Url) -> Result<Option<&str>> {
642 match url.path_segments() {
643 None => Ok(None),
644 Some(mut segments) => {
645 let head = segments.next();
646
647 if segments.next().is_some() {
648 return Err(Error::Url(UrlError::Invalid));
649 }
650
651 match head {
652 Some(database) if !database.is_empty() => Ok(Some(database)),
653 _ => Ok(None),
654 }
655 }
656 }
657}
658
659fn parse_duration(source: &str) -> std::result::Result<Duration, ()> {
660 let digits_count = source.chars().take_while(|c| c.is_ascii_digit()).count();
661
662 let left: String = source.chars().take(digits_count).collect();
663 let right: String = source.chars().skip(digits_count).collect();
664
665 let num = match u64::from_str(&left) {
666 Ok(value) => value,
667 Err(_) => return Err(()),
668 };
669
670 match right.as_str() {
671 "s" => Ok(Duration::from_secs(num)),
672 "ms" => Ok(Duration::from_millis(num)),
673 _ => Err(()),
674 }
675}
676
677fn parse_opt_duration(source: &str) -> std::result::Result<Option<Duration>, ()> {
678 if source == "none" {
679 return Ok(None);
680 }
681
682 let duration = parse_duration(source)?;
683 Ok(Some(duration))
684}
685
686fn parse_compression(source: &str) -> std::result::Result<bool, ()> {
687 match source {
688 "none" => Ok(false),
689 "lz4" => Ok(true),
690 _ => Err(()),
691 }
692}
693
694fn parse_hosts(source: &str) -> std::result::Result<Vec<Url>, ()> {
695 let mut result = Vec::new();
696 for host in source.split(',') {
697 match Url::from_str(&format!("tcp://{host}")) {
698 Ok(url) => result.push(url),
699 Err(_) => return Err(()),
700 }
701 }
702 Ok(result)
703}
704
705#[cfg(test)]
706mod test {
707 use super::*;
708
709 #[test]
710 fn test_parse_hosts() {
711 let source = "host2:9000,host3:9000";
712 let expected = vec![
713 Url::from_str("tcp://host2:9000").unwrap(),
714 Url::from_str("tcp://host3:9000").unwrap(),
715 ];
716 let actual = parse_hosts(source).unwrap();
717 assert_eq!(actual, expected)
718 }
719
720 #[test]
721 fn test_parse_default() {
722 let url = "tcp://host1";
723 let options = from_url(url).unwrap();
724 assert_eq!(options.database, "default");
725 assert_eq!(options.username, "default");
726 assert_eq!(options.password, "");
727 }
728
729 #[test]
730 #[cfg(feature = "_tls")]
731 fn test_parse_secure_options() {
732 let url = "tcp://username:password@host1:9001/database?ping_timeout=42ms&keepalive=99s&compression=lz4&connection_timeout=10s&secure=true&skip_verify=true";
733 assert_eq!(
734 Options {
735 username: "username".into(),
736 password: "password".into(),
737 addr: Url::parse("tcp://username:password@host1:9001").unwrap(),
738 database: "database".into(),
739 keepalive: Some(Duration::from_secs(99)),
740 ping_timeout: Duration::from_millis(42),
741 connection_timeout: Duration::from_secs(10),
742 compression: true,
743 secure: true,
744 skip_verify: true,
745 ..Options::default()
746 },
747 from_url(url).unwrap(),
748 );
749 }
750
751 #[test]
752 fn test_parse_encoded_creds() {
753 let url = "tcp://user%20%3Cbar%3E:password%20%3Cbar%3E@host1:9001/database?ping_timeout=42ms&keepalive=99s&compression=lz4&connection_timeout=10s";
754 assert_eq!(
755 Options {
756 username: "user <bar>".into(),
757 password: "password <bar>".into(),
758 addr: Url::parse("tcp://user%20%3Cbar%3E:password%20%3Cbar%3E@host1:9001").unwrap(),
759 database: "database".into(),
760 keepalive: Some(Duration::from_secs(99)),
761 ping_timeout: Duration::from_millis(42),
762 connection_timeout: Duration::from_secs(10),
763 compression: true,
764 ..Options::default()
765 },
766 from_url(url).unwrap(),
767 );
768 }
769
770 #[test]
771 fn test_parse_options() {
772 let url = "tcp://username:password@host1:9001/database?ping_timeout=42ms&keepalive=99s&compression=lz4&connection_timeout=10s";
773 assert_eq!(
774 Options {
775 username: "username".into(),
776 password: "password".into(),
777 addr: Url::parse("tcp://username:password@host1:9001").unwrap(),
778 database: "database".into(),
779 keepalive: Some(Duration::from_secs(99)),
780 ping_timeout: Duration::from_millis(42),
781 connection_timeout: Duration::from_secs(10),
782 compression: true,
783 ..Options::default()
784 },
785 from_url(url).unwrap(),
786 );
787 }
788
789 #[test]
790 #[should_panic]
791 fn test_parse_invalid_url() {
792 let url = "ʘ_ʘ";
793 from_url(url).unwrap();
794 }
795
796 #[test]
797 fn test_parse_with_unknown_setting() {
798 let url = "tcp://localhost:9000/foo?bar=baz";
799 assert_eq!(
800 Options {
801 addr: Url::parse("tcp://localhost:9000").unwrap(),
802 database: "foo".into(),
803 settings: HashMap::from([(
804 "bar".into(),
805 SettingValue {
806 value: SettingType::String("baz".into()),
807 is_important: true,
808 }
809 ),]),
810 ..Options::default()
811 },
812 from_url(url).unwrap(),
813 );
814 }
816
817 #[test]
818 fn test_with_setting() {
819 {
820 let opts = Options::from_str("tcp://localhost:9000")
821 .unwrap()
822 .with_setting("foo", "bar", true);
823 assert_eq!(
824 opts.settings,
825 HashMap::from([(
826 "foo".into(),
827 SettingValue {
828 value: SettingType::String("bar".into()),
829 is_important: true,
830 }
831 )])
832 );
833 }
834
835 {
836 let opts = Options::from_str("tcp://localhost:9000")
837 .unwrap()
838 .with_setting("foo", "bar", false);
839 assert_eq!(
840 opts.settings,
841 HashMap::from([(
842 "foo".into(),
843 SettingValue {
844 value: SettingType::String("bar".into()),
845 is_important: false,
846 }
847 )])
848 );
849 }
850
851 {
852 let opts = Options::from_str("tcp://localhost:9000")
853 .unwrap()
854 .with_setting("foo", 1, true);
855 assert_eq!(
856 opts.settings,
857 HashMap::from([(
858 "foo".into(),
859 SettingValue {
860 value: SettingType::UInt64(1u64),
861 is_important: true,
862 }
863 )])
864 );
865 }
866
867 {
868 let opts = Options::from_str("tcp://localhost:9000")
869 .unwrap()
870 .with_setting("foo", true, true);
871 assert_eq!(
872 opts.settings,
873 HashMap::from([(
874 "foo".into(),
875 SettingValue {
876 value: SettingType::Bool(true),
877 is_important: true,
878 }
879 )])
880 );
881 }
882
883 {
884 let opts = Options::from_str("tcp://localhost:9000")
885 .unwrap()
886 .with_setting("foo", 1., true);
887 assert_eq!(
888 opts.settings,
889 HashMap::from([(
890 "foo".into(),
891 SettingValue {
892 value: SettingType::Float64(1.),
893 is_important: true,
894 }
895 )])
896 );
897 }
898 }
899
900 #[test]
901 #[should_panic]
902 fn test_parse_with_multi_databases() {
903 let url = "tcp://localhost:9000/foo/bar";
904 from_url(url).unwrap();
905 }
906
907 #[test]
908 fn test_parse_duration() {
909 assert_eq!(parse_duration("3s").unwrap(), Duration::from_secs(3));
910 assert_eq!(parse_duration("123ms").unwrap(), Duration::from_millis(123));
911
912 parse_duration("ms").unwrap_err();
913 parse_duration("1ss").unwrap_err();
914 }
915
916 #[test]
917 fn test_parse_opt_duration() {
918 assert_eq!(
919 parse_opt_duration("3s").unwrap(),
920 Some(Duration::from_secs(3))
921 );
922 assert_eq!(parse_opt_duration("none").unwrap(), None::<Duration>);
923 }
924
925 #[test]
926 fn test_parse_compression() {
927 assert!(!parse_compression("none").unwrap());
928 assert!(parse_compression("lz4").unwrap());
929 parse_compression("?").unwrap_err();
930 }
931}