From 5dd6fe1be32c319c45f778e20762874ab9df25bd Mon Sep 17 00:00:00 2001 From: Jun Wu Date: Tue, 17 Sep 2019 15:41:41 -0700 Subject: [PATCH] configparser: add type conversions Summary: This is to match APIs like `ui.configbool` in the Python land. The new APIs use string types to make them easier to use. Reviewed By: xavierd Differential Revision: D17423798 fbshipit-source-id: 747b094c4cc6afef2e818a1862e820647328c4c2 --- lib/configparser/src/error.rs | 4 ++ lib/configparser/src/hg.rs | 107 ++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/lib/configparser/src/error.rs b/lib/configparser/src/error.rs index 4e2c5c45d5..4962163b54 100644 --- a/lib/configparser/src/error.rs +++ b/lib/configparser/src/error.rs @@ -12,6 +12,10 @@ use failure::Fail; /// The error type for parsing config files. #[derive(Fail, Debug)] pub enum Error { + /// Unable to convert to a type. + #[fail(display = "{}", _0)] + Convert(String), + /// Unable to parse a file due to syntax. #[fail(display = "{:?}:\n{}", _0, _1)] Parse(PathBuf, String), diff --git a/lib/configparser/src/hg.rs b/lib/configparser/src/hg.rs index f8dc12746e..4d4c8a8a30 100644 --- a/lib/configparser/src/hg.rs +++ b/lib/configparser/src/hg.rs @@ -56,6 +56,31 @@ pub trait ConfigSetHgExt { /// Load a specified config file. Respect HGPLAIN environment variables. /// Return errors parsing files. fn load_hgrc(&mut self, path: impl AsRef, source: &'static str) -> Vec; + + /// Get a config item. Convert to type `T`. + /// + /// If the config item is not set, calculate it using `default_func`. + fn get_or( + &self, + section: &str, + name: &str, + default_func: impl Fn() -> T, + ) -> Fallible; + + /// Get a config item. Convert to type `T`. + /// + /// If the config item is not set, return `T::default()`. + fn get_or_default( + &self, + section: &str, + name: &str, + ) -> Fallible { + self.get_or(section, name, || Default::default()) + } +} + +pub trait FromConfigValue: Sized { + fn try_from_bytes(bytes: &[u8]) -> Fallible; } /// Load system, user config files. @@ -292,6 +317,50 @@ impl ConfigSetHgExt for ConfigSet { let opts = Options::new().source(source).process_hgplain(); self.load_path(path, &opts) } + + fn get_or( + &self, + section: &str, + name: &str, + default_func: impl Fn() -> T, + ) -> Fallible { + match ConfigSet::get(self, section, name) { + None => Ok(default_func()), + Some(bytes) => Ok(T::try_from_bytes(&bytes)?), + } + } +} + +impl FromConfigValue for bool { + fn try_from_bytes(bytes: &[u8]) -> Fallible { + let value = std::str::from_utf8(bytes)?.to_lowercase(); + match value.as_ref() { + "1" | "yes" | "true" | "on" | "always" => Ok(true), + "0" | "no" | "false" | "off" | "never" => Ok(false), + _ => Err(Error::Convert(format!("invalid bool: {}", value)).into()), + } + } +} + +impl FromConfigValue for i64 { + fn try_from_bytes(bytes: &[u8]) -> Fallible { + let value = std::str::from_utf8(bytes)?.parse()?; + Ok(value) + } +} + +impl FromConfigValue for String { + fn try_from_bytes(bytes: &[u8]) -> Fallible { + String::from_utf8(bytes.to_vec()) + .map_err(|_| Error::Convert(format!("{:?} is not utf8 encoded", bytes)).into()) + } +} + +impl FromConfigValue for Vec { + fn try_from_bytes(bytes: &[u8]) -> Fallible { + let items = parse_list(bytes); + items.into_iter().map(|s| T::try_from_bytes(&s)).collect() + } } /// Parse a configuration value as a list of comma/space separated strings. @@ -786,4 +855,42 @@ mod tests { vec![b("a"), b(" c"), b("\""), b("d")] ); } + + #[test] + fn test_get_or() { + let mut cfg = ConfigSet::new(); + cfg.parse( + "[foo]\n\ + bool1 = yes\n\ + bool2 = unknown\n\ + bools = 1, TRUE, On, aLwAys, 0, false, oFF, never\n\ + int1 = -33\n\ + list1 = x y z\n\ + list3 = 2, 3, 1", + &"test".into(), + ); + + assert_eq!(cfg.get_or("foo", "bar", || 3).unwrap(), 3); + assert_eq!(cfg.get_or("foo", "bool1", || false).unwrap(), true); + assert_eq!( + format!("{}", cfg.get_or("foo", "bool2", || true).unwrap_err()), + "invalid bool: unknown" + ); + assert_eq!(cfg.get_or("foo", "int1", || 42).unwrap(), -33); + assert_eq!( + cfg.get_or("foo", "list1", || vec!["x".to_string()]) + .unwrap(), + vec!["x", "y", "z"] + ); + assert_eq!( + cfg.get_or("foo", "list3", || vec![0]).unwrap(), + vec![2, 3, 1] + ); + + assert_eq!(cfg.get_or_default::("foo", "bool1").unwrap(), true); + assert_eq!( + cfg.get_or_default::>("foo", "bools").unwrap(), + vec![true, true, true, true, false, false, false, false] + ); + } }