| use std::borrow::Cow; | |
| use std::fmt; | |
| /// Key for a configuration variable. | |
| /// | |
| /// This type represents a configuration variable that we're looking up in | |
| /// Cargo's configuration. This structure simultaneously keeps track of a | |
| /// corresponding environment variable name as well as a TOML config name. The | |
| /// intention here is that this is built up and torn down over time efficiently, | |
| /// avoiding clones and such as possible. | |
| #[derive(Debug, Clone)] | |
| pub struct ConfigKey { | |
| // The current environment variable this configuration key maps to. This is | |
| // updated with `push` methods and looks like `CARGO_FOO_BAR` for pushing | |
| // `foo` and then `bar`. | |
| env: String, | |
| // This is used to keep track of how many sub-keys have been pushed on | |
| // this `ConfigKey`. Each element of this vector is a new sub-key pushed | |
| // onto this `ConfigKey`. Each element is a pair where the first item is | |
| // the key part as a string, and the second item is an index into `env`. | |
| // The `env` index is used on `pop` to truncate `env` to rewind back to | |
| // the previous `ConfigKey` state before a `push`. | |
| parts: Vec<(String, usize)>, | |
| } | |
| impl ConfigKey { | |
| /// Creates a new blank configuration key which is ready to get built up by | |
| /// using `push` and `push_sensitive`. | |
| pub fn new() -> ConfigKey { | |
| ConfigKey { | |
| env: "CARGO".to_string(), | |
| parts: Vec::new(), | |
| } | |
| } | |
| /// Creates a `ConfigKey` from the `key` specified. | |
| /// | |
| /// The `key` specified is expected to be a period-separated toml | |
| /// configuration key. | |
| pub fn from_str(key: &str) -> ConfigKey { | |
| let mut cfg = ConfigKey::new(); | |
| for part in key.split('.') { | |
| cfg.push(part); | |
| } | |
| cfg | |
| } | |
| /// Creates a `ConfigKey` from the `key` specified. | |
| /// | |
| /// The `key` specified is expected to be a period-separated toml | |
| /// configuration key. | |
| pub fn from_parts(parts: impl IntoIterator<Item = impl AsRef<str>>) -> ConfigKey { | |
| let mut cfg = ConfigKey::new(); | |
| for part in parts { | |
| cfg.push(part.as_ref()); | |
| } | |
| cfg | |
| } | |
| /// Pushes a new sub-key on this `ConfigKey`. This sub-key should be | |
| /// equivalent to accessing a sub-table in TOML. | |
| /// | |
| /// Note that this considers `name` to be case-insensitive, meaning that the | |
| /// corresponding toml key is appended with this `name` as-is and the | |
| /// corresponding env key is appended with `name` after transforming it to | |
| /// uppercase characters. | |
| pub fn push(&mut self, name: &str) { | |
| let env = name.replace(['-', '.'], "_").to_uppercase(); | |
| self._push(&env, name); | |
| } | |
| /// Performs the same function as `push` except that the corresponding | |
| /// environment variable does not get the uppercase letters of `name` but | |
| /// instead `name` is pushed raw onto the corresponding environment | |
| /// variable. | |
| pub fn push_sensitive(&mut self, name: &str) { | |
| self._push(name, name); | |
| } | |
| fn _push(&mut self, env: &str, config: &str) { | |
| self.parts.push((config.to_string(), self.env.len())); | |
| self.env.push('_'); | |
| self.env.push_str(env); | |
| } | |
| /// Rewinds this `ConfigKey` back to the state it was at before the last | |
| /// `push` method being called. | |
| pub fn pop(&mut self) { | |
| let (_part, env) = self.parts.pop().unwrap(); | |
| self.env.truncate(env); | |
| } | |
| /// Returns the corresponding environment variable key for this | |
| /// configuration value. | |
| pub fn as_env_key(&self) -> &str { | |
| &self.env | |
| } | |
| /// Returns an iterator of the key parts as strings. | |
| pub(crate) fn parts(&self) -> impl Iterator<Item = &str> { | |
| self.parts.iter().map(|p| p.0.as_ref()) | |
| } | |
| /// Returns whether or not this is a key for the root table. | |
| pub fn is_root(&self) -> bool { | |
| self.parts.is_empty() | |
| } | |
| /// Returns whether or not the given key string matches this key. | |
| /// Use * to match any key part. | |
| pub fn matches(&self, pattern: &str) -> bool { | |
| let mut parts = self.parts(); | |
| pattern | |
| .split('.') | |
| .all(|pat| parts.next() == Some(pat) || pat == "*") | |
| && parts.next().is_none() | |
| } | |
| } | |
| impl fmt::Display for ConfigKey { | |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
| let parts: Vec<_> = self.parts().map(|part| escape_key_part(part)).collect(); | |
| parts.join(".").fmt(f) | |
| } | |
| } | |
| #[derive(Debug, Clone)] | |
| pub enum KeyOrIdx { | |
| Key(String), | |
| Idx(usize), | |
| } | |
| /// Tracks the key path to an item in an array for detailed error context. | |
| #[derive(Debug, Clone)] | |
| pub struct ArrayItemKeyPath { | |
| base: ConfigKey, | |
| /// This is stored in reverse, and is pushed only when erroring out. | |
| /// The first element is the innermost key. | |
| path: Vec<KeyOrIdx>, | |
| } | |
| impl ArrayItemKeyPath { | |
| pub fn new(base: ConfigKey) -> Self { | |
| Self { | |
| base, | |
| path: Vec::new(), | |
| } | |
| } | |
| pub fn push_key(&mut self, k: String) { | |
| self.path.push(KeyOrIdx::Key(k)) | |
| } | |
| pub fn push_index(&mut self, i: usize) { | |
| self.path.push(KeyOrIdx::Idx(i)) | |
| } | |
| } | |
| impl fmt::Display for ArrayItemKeyPath { | |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
| write!(f, "{}", self.base)?; | |
| for k in self.path.iter().rev() { | |
| match k { | |
| KeyOrIdx::Key(s) => { | |
| f.write_str(".")?; | |
| f.write_str(&escape_key_part(&s))?; | |
| } | |
| KeyOrIdx::Idx(i) => { | |
| write!(f, "[{i}]")?; | |
| } | |
| } | |
| } | |
| Ok(()) | |
| } | |
| } | |
| pub(super) fn escape_key_part<'a>(part: &'a str) -> Cow<'a, str> { | |
| let ok = part.chars().all(|c| { | |
| matches!(c, | |
| 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_') | |
| }); | |
| if ok { | |
| Cow::Borrowed(part) | |
| } else { | |
| // This is a bit messy, but toml doesn't expose a function to do this. | |
| Cow::Owned(toml::Value::from(part).to_string()) | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use snapbox::assert_data_eq; | |
| use snapbox::str; | |
| use super::*; | |
| #[test] | |
| fn array_item_key_path_display() { | |
| let base = ConfigKey::from_str("array"); | |
| let mut key_path = ArrayItemKeyPath::new(base); | |
| // These are pushed in reverse order. | |
| key_path.push_index(1); | |
| key_path.push_key("rustflags".into()); | |
| key_path.push_key("thumbv8m.base-none-eabi".into()); | |
| key_path.push_index(2); | |
| key_path.push_index(3); | |
| assert_data_eq!( | |
| key_path.to_string(), | |
| str![[r#"array[3][2]."thumbv8m.base-none-eabi".rustflags[1]"#]] | |
| ); | |
| } | |
| } |