diff --git a/src/cargo/ops/cargo_config.rs b/src/cargo/ops/cargo_config.rs index 95a1e3c879d..8d65da43bf0 100644 --- a/src/cargo/ops/cargo_config.rs +++ b/src/cargo/ops/cargo_config.rs @@ -115,6 +115,21 @@ fn print_toml(gctx: &GlobalContext, opts: &GetOptions<'_>, key: &ConfigKey, cv: } format!(" # {}", def) }; + + fn cv_to_toml(cv: &CV) -> toml_edit::Value { + match cv { + CV::String(s, _) => toml_edit::Value::from(s.as_str()), + CV::Integer(i, _) => toml_edit::Value::from(*i), + CV::Boolean(b, _) => toml_edit::Value::from(*b), + CV::List(l, _) => toml_edit::Value::from_iter(l.iter().map(cv_to_toml)), + CV::Table(t, _) => toml_edit::Value::from_iter({ + let mut t: Vec<_> = t.iter().collect(); + t.sort_by_key(|t| t.0); + t.into_iter().map(|(k, v)| (k, cv_to_toml(v))) + }), + } + } + match cv { CV::Boolean(val, def) => drop_println!(gctx, "{} = {}{}", key, val, origin(def)), CV::Integer(val, def) => drop_println!(gctx, "{} = {}{}", key, val, origin(def)), @@ -129,31 +144,13 @@ fn print_toml(gctx: &GlobalContext, opts: &GetOptions<'_>, key: &ConfigKey, cv: if opts.show_origin { drop_println!(gctx, "{} = [", key); for cv in vals { - let (val, def) = match cv { - CV::String(s, def) => (s.as_str(), def), - // This is actually unreachable until we start supporting list of different types. - // It should be validated already during the deserialization. - v => todo!("support {} type ", v.desc()), - }; - drop_println!( - gctx, - " {}, # {}", - serde::Serialize::serialize(val, toml_edit::ser::ValueSerializer::new()) - .unwrap(), - def - ); + let val = cv_to_toml(cv); + let def = cv.definition(); + drop_println!(gctx, " {val}, # {def}"); } drop_println!(gctx, "]"); } else { - let vals: toml_edit::Array = vals - .iter() - .map(|cv| match cv { - CV::String(s, _) => toml_edit::Value::from(s.as_str()), - // This is actually unreachable until we start supporting list of different types. - // It should be validated already during the deserialization. - v => todo!("support {} type ", v.desc()), - }) - .collect(); + let vals: toml_edit::Array = vals.iter().map(cv_to_toml).collect(); drop_println!(gctx, "{} = {}", key, vals); } } diff --git a/src/cargo/util/context/de.rs b/src/cargo/util/context/de.rs index 71af17c0516..a3567ff2271 100644 --- a/src/cargo/util/context/de.rs +++ b/src/cargo/util/context/de.rs @@ -21,6 +21,7 @@ //! //! [`ConfigValue`]: CV +use crate::util::context::key::ArrayItemKeyPath; use crate::util::context::value; use crate::util::context::{ConfigError, ConfigKey, GlobalContext}; use crate::util::context::{ConfigValue as CV, Definition, Value}; @@ -129,7 +130,8 @@ impl<'de, 'gctx> de::Deserializer<'de> for Deserializer<'gctx> { // // See more comments in `value.rs` for the protocol used here. if name == value::NAME && fields == value::FIELDS { - return visitor.visit_map(ValueDeserializer::new(self)?); + let source = ValueSource::with_deserializer(self)?; + return visitor.visit_map(ValueDeserializer::new(source)); } visitor.visit_map(ConfigMapAccess::new_struct(self, fields)?) } @@ -424,14 +426,13 @@ impl<'de, 'gctx> de::MapAccess<'de> for ConfigMapAccess<'gctx> { } } -struct ConfigSeqAccess { - /// The config key to the sequence. - key: ConfigKey, - list_iter: vec::IntoIter, +struct ConfigSeqAccess<'gctx> { + de: Deserializer<'gctx>, + list_iter: std::iter::Enumerate>, } -impl ConfigSeqAccess { - fn new(de: Deserializer<'_>) -> Result { +impl ConfigSeqAccess<'_> { + fn new(de: Deserializer<'_>) -> Result, ConfigError> { let mut res = Vec::new(); match de.gctx.get_cv(&de.key)? { @@ -447,34 +448,43 @@ impl ConfigSeqAccess { de.gctx.get_env_list(&de.key, &mut res)?; Ok(ConfigSeqAccess { - key: de.key, - list_iter: res.into_iter(), + de, + list_iter: res.into_iter().enumerate(), }) } } -impl<'de> de::SeqAccess<'de> for ConfigSeqAccess { +impl<'de, 'gctx> de::SeqAccess<'de> for ConfigSeqAccess<'gctx> { type Error = ConfigError; fn next_element_seed(&mut self, seed: T) -> Result, Self::Error> where T: de::DeserializeSeed<'de>, { - match self.list_iter.next() { - // TODO: add `def` to error? - Some(val @ CV::String(..)) => { - // This might be a String or a Value. - // ArrayItemDeserializer will handle figuring out which one it is. - seed.deserialize(ArrayItemDeserializer::new(val)).map(Some) - } - Some(val) => Err(ConfigError::expected(&self.key, "list of string", &val)), - None => Ok(None), - } + let Some((i, cv)) = self.list_iter.next() else { + return Ok(None); + }; + + let mut key_path = ArrayItemKeyPath::new(self.de.key.clone()); + let definition = Some(cv.definition().clone()); + let de = ArrayItemDeserializer { + cv, + key_path: &mut key_path, + }; + seed.deserialize(de) + .map_err(|e| { + // This along with ArrayItemKeyPath provide a better error context of the + // ConfigValue definition + the key path within an array item that native + // TOML key path can't express. For example, `foo.bar[3].baz`. + key_path.push_index(i); + e.with_array_item_key_context(&key_path, definition) + }) + .map(Some) } } /// Source of data for [`ValueDeserializer`] -enum ValueSource<'gctx> { +enum ValueSource<'gctx, 'err> { /// The deserializer used to actually deserialize a Value struct. Deserializer { de: Deserializer<'gctx>, @@ -482,28 +492,18 @@ enum ValueSource<'gctx> { }, /// A [`ConfigValue`](CV). /// - /// This is used for situations where you can't address a string via a TOML key, - /// such as a string inside an array. - /// The [`ConfigSeqAccess`] doesn't know if the type it should deserialize to - /// is a `String` or `Value`, - /// so [`ArrayItemDeserializer`] needs to be able to handle both. - ConfigValue(CV), -} - -/// This is a deserializer that deserializes into a `Value` for -/// configuration. -/// -/// This is a special deserializer because it deserializes one of its struct -/// fields into the location that this configuration value was defined in. -/// -/// See more comments in `value.rs` for the protocol used here. -struct ValueDeserializer<'gctx> { - hits: u32, - source: ValueSource<'gctx>, + /// This is used for situations where you can't address type via a TOML key, + /// such as a value inside an array. + /// The [`ConfigSeqAccess`] doesn't know what type it should deserialize to + /// so [`ArrayItemDeserializer`] needs to be able to handle all of them. + ConfigValue { + cv: CV, + key_path: &'err mut ArrayItemKeyPath, + }, } -impl<'gctx> ValueDeserializer<'gctx> { - fn new(de: Deserializer<'gctx>) -> Result, ConfigError> { +impl<'gctx, 'err> ValueSource<'gctx, 'err> { + fn with_deserializer(de: Deserializer<'gctx>) -> Result, ConfigError> { // Figure out where this key is defined. let definition = { let env = de.key.as_env_key(); @@ -525,28 +525,40 @@ impl<'gctx> ValueDeserializer<'gctx> { } }; - Ok(ValueDeserializer { - hits: 0, - source: ValueSource::Deserializer { de, definition }, - }) + Ok(Self::Deserializer { de, definition }) } - fn with_cv(cv: CV) -> ValueDeserializer<'gctx> { - ValueDeserializer { - hits: 0, - source: ValueSource::ConfigValue(cv), - } + fn with_cv(cv: CV, key_path: &'err mut ArrayItemKeyPath) -> ValueSource<'gctx, 'err> { + ValueSource::ConfigValue { cv, key_path } + } +} + +/// This is a deserializer that deserializes into a `Value` for +/// configuration. +/// +/// This is a special deserializer because it deserializes one of its struct +/// fields into the location that this configuration value was defined in. +/// +/// See more comments in `value.rs` for the protocol used here. +struct ValueDeserializer<'gctx, 'err> { + hits: u32, + source: ValueSource<'gctx, 'err>, +} + +impl<'gctx, 'err> ValueDeserializer<'gctx, 'err> { + fn new(source: ValueSource<'gctx, 'err>) -> ValueDeserializer<'gctx, 'err> { + Self { hits: 0, source } } fn definition(&self) -> &Definition { match &self.source { ValueSource::Deserializer { definition, .. } => definition, - ValueSource::ConfigValue(cv) => cv.definition(), + ValueSource::ConfigValue { cv, .. } => cv.definition(), } } } -impl<'de, 'gctx> de::MapAccess<'de> for ValueDeserializer<'gctx> { +impl<'de, 'gctx, 'err> de::MapAccess<'de> for ValueDeserializer<'gctx, 'err> { type Error = ConfigError; fn next_key_seed(&mut self, seed: K) -> Result, Self::Error> @@ -572,12 +584,16 @@ impl<'de, 'gctx> de::MapAccess<'de> for ValueDeserializer<'gctx> { // If this is the first time around we deserialize the `value` field // which is the actual deserializer if self.hits == 1 { - return match &self.source { + return match &mut self.source { ValueSource::Deserializer { de, definition } => seed .deserialize(de.clone()) .map_err(|e| e.with_key_context(&de.key, Some(definition.clone()))), - ValueSource::ConfigValue(cv) => { - seed.deserialize(ArrayItemDeserializer::new(cv.clone())) + ValueSource::ConfigValue { cv, key_path } => { + let de = ArrayItemDeserializer { + cv: cv.clone(), + key_path, + }; + seed.deserialize(de) } }; } @@ -605,45 +621,16 @@ impl<'de, 'gctx> de::MapAccess<'de> for ValueDeserializer<'gctx> { /// A deserializer for individual [`ConfigValue`](CV) items in arrays /// -/// This deserializer is only implemented to handle deserializing a String -/// inside a sequence (like `Vec` or `Vec>`). -/// `Value` is handled by `deserialize_struct` in [`ValueDeserializer`], -/// and the plain `String` is handled by all the other functions here. -#[derive(Clone)] -struct ArrayItemDeserializer { +/// It is implemented to handle any types inside a sequence, like `Vec`, +/// `Vec>`, or even `Vev>>`. +struct ArrayItemDeserializer<'err> { cv: CV, + key_path: &'err mut ArrayItemKeyPath, } -impl ArrayItemDeserializer { - fn new(cv: CV) -> Self { - Self { cv } - } - - fn into_inner(self) -> String { - match self.cv { - CV::String(s, _def) => s, - _ => unreachable!("string expected"), - } - } -} - -impl<'de> de::Deserializer<'de> for ArrayItemDeserializer { +impl<'de, 'err> de::Deserializer<'de> for ArrayItemDeserializer<'err> { type Error = ConfigError; - fn deserialize_str(self, visitor: V) -> Result - where - V: de::Visitor<'de>, - { - visitor.visit_str(&self.into_inner()) - } - - fn deserialize_string(self, visitor: V) -> Result - where - V: de::Visitor<'de>, - { - visitor.visit_string(self.into_inner()) - } - fn deserialize_struct( self, name: &'static str, @@ -658,33 +645,156 @@ impl<'de> de::Deserializer<'de> for ArrayItemDeserializer { // // See more comments in `value.rs` for the protocol used here. if name == value::NAME && fields == value::FIELDS { - return visitor.visit_map(ValueDeserializer::with_cv(self.cv)); + let source = ValueSource::with_cv(self.cv, self.key_path); + return visitor.visit_map(ValueDeserializer::new(source)); } - unimplemented!("only strings and Value can be deserialized from a sequence"); + visitor.visit_map(ArrayItemMapAccess::with_struct( + self.cv, + fields, + self.key_path, + )) } fn deserialize_any(self, visitor: V) -> Result where V: de::Visitor<'de>, { - visitor.visit_string(self.into_inner()) + match self.cv { + CV::String(s, _) => visitor.visit_string(s), + CV::Integer(i, _) => visitor.visit_i64(i), + CV::Boolean(b, _) => visitor.visit_bool(b), + l @ CV::List(_, _) => visitor.visit_seq(ArrayItemSeqAccess::new(l, self.key_path)), + t @ CV::Table(_, _) => visitor.visit_map(ArrayItemMapAccess::new(t, self.key_path)), + } + } + + // Forward everything to deserialize_any + serde::forward_to_deserialize_any! { + bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string seq + bytes byte_buf map option unit newtype_struct + ignored_any unit_struct tuple_struct tuple enum identifier } +} + +/// Sequence access for nested arrays within [`ArrayItemDeserializer`] +struct ArrayItemSeqAccess<'err> { + items: std::iter::Enumerate>, + key_path: &'err mut ArrayItemKeyPath, +} + +impl<'err> ArrayItemSeqAccess<'err> { + fn new(cv: CV, key_path: &'err mut ArrayItemKeyPath) -> ArrayItemSeqAccess<'err> { + let items = match cv { + CV::List(list, _) => list.into_iter().enumerate(), + _ => unreachable!("must be a list"), + }; + Self { items, key_path } + } +} - fn deserialize_ignored_any(self, visitor: V) -> Result +impl<'de, 'err> de::SeqAccess<'de> for ArrayItemSeqAccess<'err> { + type Error = ConfigError; + + fn next_element_seed(&mut self, seed: T) -> Result, Self::Error> where - V: de::Visitor<'de>, + T: de::DeserializeSeed<'de>, { - visitor.visit_unit() + match self.items.next() { + Some((i, cv)) => { + let de = ArrayItemDeserializer { + cv, + key_path: self.key_path, + }; + seed.deserialize(de) + .inspect_err(|_| self.key_path.push_index(i)) + .map(Some) + } + None => Ok(None), + } } +} - serde::forward_to_deserialize_any! { - i8 i16 i32 i64 - u8 u16 u32 u64 - option - newtype_struct seq tuple tuple_struct map enum bool - f32 f64 char bytes - byte_buf unit unit_struct - identifier +/// Map access for nested tables within [`ArrayItemDeserializer`] +struct ArrayItemMapAccess<'err> { + cv: CV, + keys: vec::IntoIter, + current_key: Option, + key_path: &'err mut ArrayItemKeyPath, +} + +impl<'err> ArrayItemMapAccess<'err> { + fn new(cv: CV, key_path: &'err mut ArrayItemKeyPath) -> Self { + let keys = match &cv { + CV::Table(map, _) => map.keys().cloned().collect::>().into_iter(), + _ => unreachable!("must be a map"), + }; + Self { + cv, + keys, + current_key: None, + key_path, + } + } + + fn with_struct(cv: CV, given_fields: &[&str], key_path: &'err mut ArrayItemKeyPath) -> Self { + // TODO: We might want to warn unused fields, + // like what we did in ConfigMapAccess::new_struct + let keys = given_fields + .into_iter() + .map(|s| s.to_string()) + .collect::>() + .into_iter(); + Self { + cv, + keys, + current_key: None, + key_path, + } + } +} + +impl<'de, 'err> de::MapAccess<'de> for ArrayItemMapAccess<'err> { + type Error = ConfigError; + + fn next_key_seed(&mut self, seed: K) -> Result, Self::Error> + where + K: de::DeserializeSeed<'de>, + { + match self.keys.next() { + Some(key) => { + self.current_key = Some(key.clone()); + seed.deserialize(key.into_deserializer()).map(Some) + } + None => Ok(None), + } + } + + fn next_value_seed(&mut self, seed: V) -> Result + where + V: de::DeserializeSeed<'de>, + { + let key = self.current_key.take().unwrap(); + match &self.cv { + CV::Table(map, _) => { + if let Some(cv) = map.get(&key) { + let de = ArrayItemDeserializer { + cv: cv.clone(), + key_path: self.key_path, + }; + seed.deserialize(de) + .inspect_err(|_| self.key_path.push_key(key)) + } else { + Err(ConfigError::new( + format!("missing config key `{key}`"), + self.cv.definition().clone(), + )) + } + } + _ => Err(ConfigError::new( + "expected table".to_string(), + self.cv.definition().clone(), + )), + } } } diff --git a/src/cargo/util/context/error.rs b/src/cargo/util/context/error.rs index fe7f3ad497b..90dd536f832 100644 --- a/src/cargo/util/context/error.rs +++ b/src/cargo/util/context/error.rs @@ -3,6 +3,7 @@ use std::fmt; use crate::util::ConfigValue; use crate::util::context::ConfigKey; use crate::util::context::Definition; +use crate::util::context::key::ArrayItemKeyPath; /// Internal error for serde errors. #[derive(Debug)] @@ -53,6 +54,17 @@ impl ConfigError { definition: definition, } } + + pub(super) fn with_array_item_key_context( + self, + key: &ArrayItemKeyPath, + definition: Option, + ) -> ConfigError { + ConfigError { + error: anyhow::Error::from(self).context(format!("failed to parse config at `{key}`")), + definition, + } + } } impl std::error::Error for ConfigError { diff --git a/src/cargo/util/context/key.rs b/src/cargo/util/context/key.rs index 048b595f22e..b4418394c89 100644 --- a/src/cargo/util/context/key.rs +++ b/src/cargo/util/context/key.rs @@ -112,6 +112,56 @@ impl fmt::Display for ConfigKey { } } +#[derive(Debug, Clone)] +pub enum KeyOrIdx { + Key(String), + Idx(usize), +} + +/// Tracks the key path to an item in an array for detailed errro 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, +} + +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, @@ -124,3 +174,28 @@ pub(super) fn escape_key_part<'a>(part: &'a str) -> Cow<'a, str> { 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]"#]] + ); + } +} diff --git a/src/cargo/util/context/mod.rs b/src/cargo/util/context/mod.rs index c90549bf371..b7124992c4d 100644 --- a/src/cargo/util/context/mod.rs +++ b/src/cargo/util/context/mod.rs @@ -112,6 +112,7 @@ pub use value::{Definition, OptValue, Value}; mod key; pub use key::ConfigKey; +use key::KeyOrIdx; mod path; pub use path::{ConfigRelativePath, PathAndArgs}; @@ -1025,7 +1026,8 @@ impl GlobalContext { })?; let values = toml_v.as_array().expect("env var was not array"); for value in values { - // TODO: support other types. + // Until we figure out how to deal with it through `-Zadvanced-env`, + // complex array types are unsupported. let s = value.as_str().ok_or_else(|| { ConfigError::new( format!("expected string, found {}", value.type_str()), @@ -2063,12 +2065,6 @@ impl GlobalContext { } } -#[derive(Debug)] -enum KeyOrIdx { - Key(String), - Idx(usize), -} - /// Similar to [`toml::Value`] but includes the source location where it is defined. #[derive(Eq, PartialEq, Clone)] pub enum ConfigValue { @@ -2131,12 +2127,9 @@ impl ConfigValue { toml::Value::Array(val) => Ok(CV::List( val.into_iter() .enumerate() - .map(|(i, toml)| match toml { - toml::Value::String(val) => Ok(CV::String(val, def.clone())), - v => { - path.push(KeyOrIdx::Idx(i)); - bail!("expected string but found {} at index {i}", v.type_str()) - } + .map(|(i, toml)| { + CV::from_toml_inner(def.clone(), toml, path) + .inspect_err(|_| path.push(KeyOrIdx::Idx(i))) }) .collect::>()?, def, diff --git a/tests/testsuite/bad_config.rs b/tests/testsuite/bad_config.rs index eea8cc82307..8b8e469dc65 100644 --- a/tests/testsuite/bad_config.rs +++ b/tests/testsuite/bad_config.rs @@ -86,36 +86,6 @@ Caused by: .run(); } -#[cargo_test] -fn unsupported_int_array() { - let p = project() - .file("src/lib.rs", "") - .file( - ".cargo/config.toml", - r#" - [alias] - ints = [1, 2] - "#, - ) - .build(); - p.cargo("check") - .with_status(101) - .with_stderr_data(str![[r#" -[ERROR] could not load Cargo configuration - -Caused by: - failed to load TOML configuration from `[ROOT]/foo/.cargo/config.toml` - -Caused by: - failed to parse config at `alias.ints[0]` - -Caused by: - expected string but found integer at index 0 - -"#]]) - .run(); -} - #[cargo_test] fn unsupported_float_array() { let p = project() @@ -140,7 +110,7 @@ Caused by: failed to parse config at `alias.floats[0]` Caused by: - expected string but found float at index 0 + unsupported TOML configuration type `float` "#]]) .run(); @@ -170,7 +140,7 @@ Caused by: failed to parse config at `alias.datetimes[0]` Caused by: - expected string but found datetime at index 0 + unsupported TOML configuration type `datetime` "#]]) .run(); diff --git a/tests/testsuite/cargo_alias_config.rs b/tests/testsuite/cargo_alias_config.rs index e7b725e092a..4a048b79ecb 100644 --- a/tests/testsuite/cargo_alias_config.rs +++ b/tests/testsuite/cargo_alias_config.rs @@ -81,16 +81,10 @@ fn alias_malformed_config_list() { p.cargo("b-cargo-test -v") .with_status(101) .with_stderr_data(str![[r#" -[ERROR] could not load Cargo configuration - -Caused by: - failed to load TOML configuration from `[ROOT]/foo/.cargo/config.toml` - -Caused by: - failed to parse config at `alias.b-cargo-test[0]` +[ERROR] error in [ROOT]/foo/.cargo/config.toml: failed to parse config at `alias.b-cargo-test[0]` Caused by: - expected string but found integer at index 0 + invalid type: integer `1`, expected a string "#]]) .run(); diff --git a/tests/testsuite/cargo_config/mod.rs b/tests/testsuite/cargo_config/mod.rs index be5cf7d35e5..f9c38487358 100644 --- a/tests/testsuite/cargo_config/mod.rs +++ b/tests/testsuite/cargo_config/mod.rs @@ -68,6 +68,48 @@ fn common_setup() -> PathBuf { sub_folder } +fn array_setup() -> PathBuf { + let home = paths::home(); + write_config_at( + home.join(".cargo/config.toml"), + r#" + ints = [1, 2, 3] + + bools = [true, false, true] + + strings = ["hello", "world", "test"] + + nested_ints = [[1, 2], [3, 4]] + nested_bools = [[true], [false, true]] + nested_strings = [["a", "b"], ["3", "4"]] + nested_tables = [ + [ + { x = "a" }, + { x = "b" }, + ], + [ + { x = "c" }, + { x = "d" }, + ], + ] + deeply_nested = [[ + { x = [[[ { x = [], y = 2 } ]]], y = 1 }, + ]] + + mixed = [{ x = 1 }, true, [false], "hello", 123] + + [[tables]] + name = "first" + value = 1 + [[tables]] + name = "second" + value = 2 + + "#, + ); + home +} + #[cargo_test] fn get_toml() { // Notes: @@ -171,6 +213,42 @@ profile.dev.opt-level = 3 .run(); } +#[cargo_test] +fn get_toml_with_array_any_types() { + let cwd = &array_setup(); + cargo_process("config get -Zunstable-options") + .cwd(cwd) + .masquerade_as_nightly_cargo(&["cargo-config"]) + .with_stdout_data(str![[r#" +bools = [true, false, true] +deeply_nested = [[{ x = [[[{ x = [], y = 2 }]]], y = 1 }]] +ints = [1, 2, 3] +mixed = [{ x = 1 }, true, [false], "hello", 123] +nested_bools = [[true], [false, true]] +nested_ints = [[1, 2], [3, 4]] +nested_strings = [["a", "b"], ["3", "4"]] +nested_tables = [[{ x = "a" }, { x = "b" }], [{ x = "c" }, { x = "d" }]] +strings = ["hello", "world", "test"] +tables = [{ name = "first", value = 1 }, { name = "second", value = 2 }] +# The following environment variables may affect the loaded values. +# CARGO_HOME=[ROOT]/home/.cargo + +"#]]) + .with_stderr_data(str![[r#""#]]) + .run(); + + // Unfortunately there is no TOML syntax to index an array item. + cargo_process("config get tables -Zunstable-options") + .cwd(cwd) + .masquerade_as_nightly_cargo(&["cargo-config"]) + .with_stdout_data(str![[r#" +tables = [{ name = "first", value = 1 }, { name = "second", value = 2 }] + +"#]]) + .with_stderr_data(str![[r#""#]]) + .run(); +} + #[cargo_test] fn get_json() { let sub_folder = common_setup(); @@ -300,6 +378,151 @@ CARGO_HOME=[ROOT]/home/.cargo .run(); } +#[cargo_test] +fn get_json_with_array_any_types() { + let cwd = &array_setup(); + cargo_process("config get --format=json -Zunstable-options") + .cwd(cwd) + .masquerade_as_nightly_cargo(&["cargo-config"]) + .with_stdout_data( + str![[r#" +{ + "bools": [ + true, + false, + true + ], + "deeply_nested": [ + [ + { + "x": [ + [ + [ + { + "x": [], + "y": 2 + } + ] + ] + ], + "y": 1 + } + ] + ], + "ints": [ + 1, + 2, + 3 + ], + "mixed": [ + { + "x": 1 + }, + true, + [ + false + ], + "hello", + 123 + ], + "nested_bools": [ + [ + true + ], + [ + false, + true + ] + ], + "nested_ints": [ + [ + 1, + 2 + ], + [ + 3, + 4 + ] + ], + "nested_strings": [ + [ + "a", + "b" + ], + [ + "3", + "4" + ] + ], + "nested_tables": [ + [ + { + "x": "a" + }, + { + "x": "b" + } + ], + [ + { + "x": "c" + }, + { + "x": "d" + } + ] + ], + "strings": [ + "hello", + "world", + "test" + ], + "tables": [ + { + "name": "first", + "value": 1 + }, + { + "name": "second", + "value": 2 + } + ] +} +"#]] + .is_json(), + ) + .with_stderr_data(str![[r#" +[NOTE] The following environment variables may affect the loaded values. +CARGO_HOME=[ROOT]/home/.cargo + +"#]]) + .run(); + + // Unfortunately there is no TOML syntax to index an array item. + cargo_process("config get tables --format=json -Zunstable-options") + .cwd(cwd) + .masquerade_as_nightly_cargo(&["cargo-config"]) + .with_stdout_data( + str![[r#" +{ + "tables": [ + { + "name": "first", + "value": 1 + }, + { + "name": "second", + "value": 2 + } + ] +} +"#]] + .is_json(), + ) + .with_stderr_data(str![""]) + .run(); +} + #[cargo_test] fn show_origin_toml() { let sub_folder = common_setup(); @@ -345,6 +568,80 @@ build.rustflags = [ .run(); } +#[cargo_test] +fn show_origin_toml_with_array_any_types() { + let cwd = &array_setup(); + cargo_process("config get --show-origin -Zunstable-options") + .cwd(cwd) + .masquerade_as_nightly_cargo(&["cargo-config"]) + .with_stdout_data(str![[r#" +bools = [ + true, # [ROOT]/home/.cargo/config.toml + false, # [ROOT]/home/.cargo/config.toml + true, # [ROOT]/home/.cargo/config.toml +] +deeply_nested = [ + [{ x = [[[{ x = [], y = 2 }]]], y = 1 }], # [ROOT]/home/.cargo/config.toml +] +ints = [ + 1, # [ROOT]/home/.cargo/config.toml + 2, # [ROOT]/home/.cargo/config.toml + 3, # [ROOT]/home/.cargo/config.toml +] +mixed = [ + { x = 1 }, # [ROOT]/home/.cargo/config.toml + true, # [ROOT]/home/.cargo/config.toml + [false], # [ROOT]/home/.cargo/config.toml + "hello", # [ROOT]/home/.cargo/config.toml + 123, # [ROOT]/home/.cargo/config.toml +] +nested_bools = [ + [true], # [ROOT]/home/.cargo/config.toml + [false, true], # [ROOT]/home/.cargo/config.toml +] +nested_ints = [ + [1, 2], # [ROOT]/home/.cargo/config.toml + [3, 4], # [ROOT]/home/.cargo/config.toml +] +nested_strings = [ + ["a", "b"], # [ROOT]/home/.cargo/config.toml + ["3", "4"], # [ROOT]/home/.cargo/config.toml +] +nested_tables = [ + [{ x = "a" }, { x = "b" }], # [ROOT]/home/.cargo/config.toml + [{ x = "c" }, { x = "d" }], # [ROOT]/home/.cargo/config.toml +] +strings = [ + "hello", # [ROOT]/home/.cargo/config.toml + "world", # [ROOT]/home/.cargo/config.toml + "test", # [ROOT]/home/.cargo/config.toml +] +tables = [ + { name = "first", value = 1 }, # [ROOT]/home/.cargo/config.toml + { name = "second", value = 2 }, # [ROOT]/home/.cargo/config.toml +] +# The following environment variables may affect the loaded values. +# CARGO_HOME=[ROOT]/home/.cargo + +"#]]) + .with_stderr_data(str![[r#""#]]) + .run(); + + // Unfortunately there is no TOML syntax to index an array item. + cargo_process("config get tables --show-origin -Zunstable-options") + .cwd(cwd) + .masquerade_as_nightly_cargo(&["cargo-config"]) + .with_stdout_data(str![[r#" +tables = [ + { name = "first", value = 1 }, # [ROOT]/home/.cargo/config.toml + { name = "second", value = 2 }, # [ROOT]/home/.cargo/config.toml +] + +"#]]) + .with_stderr_data(str![[r#""#]]) + .run(); +} + #[cargo_test] fn show_origin_toml_cli() { let sub_folder = common_setup(); @@ -401,7 +698,7 @@ fn unmerged_toml() { .masquerade_as_nightly_cargo(&["cargo-config"]) .env("CARGO_ALIAS_BAR", "cat dog") .env("CARGO_BUILD_JOBS", "100") - .with_stdout_data(str![[r#" + .with_stdout_data(str![[r##" # Environment variables # CARGO=[..] # CARGO_ALIAS_BAR=[..]cat dog[..] @@ -422,7 +719,7 @@ profile.dev.package.foo.opt-level = 1 target.'cfg(target_os = "linux")'.runner = "runme" -"#]]) +"##]]) .with_stderr_data(str![[r#""#]]) .run(); @@ -552,7 +849,7 @@ build.rustflags = [ cargo_process("config get --merged=no -Zunstable-options -Zconfig-include") .cwd(&sub_folder.parent().unwrap()) .masquerade_as_nightly_cargo(&["cargo-config", "config-include"]) - .with_stdout_data(str![[r#" + .with_stdout_data(str![[r##" # Environment variables # CARGO=[..] # CARGO_HOME=[ROOT]/home/.cargo @@ -574,7 +871,7 @@ profile.dev.package.foo.opt-level = 1 target.'cfg(target_os = "linux")'.runner = "runme" -"#]]) +"##]]) .with_stderr_data(str![[r#""#]]) .run(); } diff --git a/tests/testsuite/config.rs b/tests/testsuite/config.rs index 308dce26882..eb0b50a3cb0 100644 --- a/tests/testsuite/config.rs +++ b/tests/testsuite/config.rs @@ -12,6 +12,7 @@ use cargo::CargoResult; use cargo::core::features::{GitFeatures, GitoxideFeatures}; use cargo::core::{PackageIdSpec, Shell}; use cargo::util::auth::RegistryConfig; +use cargo::util::context::Value; use cargo::util::context::{ self, Definition, GlobalContext, JobsConfig, SslVersionConfig, StringList, }; @@ -1351,28 +1352,6 @@ Caused by: ); } -#[cargo_test] -fn non_string_in_array() { - // Currently only strings are supported. - write_config_toml("foo = [1, 2, 3]"); - let gctx = new_gctx(); - assert_error( - gctx.get::>("foo").unwrap_err(), - str![[r#" -could not load Cargo configuration - -Caused by: - failed to load TOML configuration from `[ROOT]/.cargo/config.toml` - -Caused by: - failed to parse config at `foo[0]` - -Caused by: - expected string but found integer at index 0 -"#]], - ); -} - #[cargo_test] fn struct_with_opt_inner_struct() { // Struct with a key that is Option of another struct. @@ -2309,3 +2288,259 @@ fn build_std() { ] ); } + +#[cargo_test] +fn array_of_any_types() { + write_config_toml( + r#" + ints = [1, 2, 3] + + bools = [true, false, true] + + strings = ["hello", "world", "test"] + + [[tables]] + name = "first" + value = 1 + [[tables]] + name = "second" + value = 2 + "#, + ); + + let gctx = new_gctx(); + + // Test integer array + let ints: Vec = gctx.get("ints").unwrap(); + assert_eq!(ints, vec![1, 2, 3]); + + let bools: Vec = gctx.get("bools").unwrap(); + assert_eq!(bools, vec![true, false, true]); + + #[derive(Deserialize, Debug, PartialEq)] + struct T { + name: String, + value: i32, + } + let tables: Vec = gctx.get("tables").unwrap(); + assert_eq!( + tables, + vec![ + T { + name: "first".into(), + value: 1, + }, + T { + name: "second".into(), + value: 2, + }, + ] + ); +} + +#[cargo_test] +fn array_env() { + // for environment, only strings are supported. + let gctx = GlobalContextBuilder::new() + .env("CARGO_INTS", "3 4 5") + .env("CARGO_BOOLS", "false true false") + .env("CARGO_STRINGS", "env1 env2 env3") + .build(); + + assert_error( + gctx.get::>("ints").unwrap_err(), + str![[r#" +error in environment variable `CARGO_INTS`: failed to parse config at `ints[0]` + +Caused by: + invalid type: string "3", expected i32 +"#]], + ); + + assert_error( + gctx.get::>("bools").unwrap_err(), + str![[r#" +error in environment variable `CARGO_BOOLS`: failed to parse config at `bools[0]` + +Caused by: + invalid type: string "false", expected a boolean +"#]], + ); + + assert_eq!( + gctx.get::>("strings").unwrap(), + vec!["env1".to_string(), "env2".to_string(), "env3".to_string()], + ); +} + +#[cargo_test] +fn nested_array() { + let root_path = paths::root().join(".cargo/config.toml"); + write_config_at( + &root_path, + r#" + nested_ints = [[1, 2], [3, 4]] + nested_bools = [[true], [false, true]] + nested_strings = [["a", "b"], ["3", "4"]] + nested_tables = [ + [ + { x = "a" }, + { x = "b" }, + ], + [ + { x = "c" }, + { x = "d" }, + ], + ] + deeply_nested = [[ + { x = [[[ { x = [], y = 2 } ]]], y = 1 }, + ]] + "#, + ); + + let gctx = GlobalContextBuilder::new() + .config_arg("nested_ints = [[5]]") + .build(); + + let nested = gctx.get::>>("nested_ints").unwrap(); + assert_eq!(nested, vec![vec![1, 2], vec![3, 4], vec![5]]); + + // exercising Value and Definition + let nested = gctx + .get::>>>>("nested_ints") + .unwrap(); + let def = Definition::Path(root_path); + assert_eq!( + nested, + vec![ + Value { + val: vec![ + Value { + val: 1, + definition: def.clone(), + }, + Value { + val: 2, + definition: def.clone(), + }, + ], + definition: def.clone() + }, + Value { + val: vec![ + Value { + val: 3, + definition: def.clone(), + }, + Value { + val: 4, + definition: def.clone(), + }, + ], + definition: def.clone(), + }, + Value { + val: vec![Value { + val: 5, + definition: Definition::Cli(None), + },], + definition: Definition::Cli(None), + }, + ] + ); + + let nested = gctx.get::>>("nested_bools").unwrap(); + assert_eq!(nested, vec![vec![true], vec![false, true]]); + + let nested = gctx.get::>>("nested_strings").unwrap(); + assert_eq!( + nested, + vec![ + vec!["a".to_string(), "b".to_string()], + vec!["3".to_string(), "4".to_string()] + ] + ); + + #[derive(Deserialize, Debug, PartialEq)] + struct S { + x: Vec>>, + y: i32, + } + let nested = gctx.get::>>("deeply_nested").unwrap(); + assert_eq!( + nested, + vec![vec![S { + x: vec![vec![vec![S { x: vec![], y: 2 }]]], + y: 1, + }]], + ); +} + +#[cargo_test] +fn mixed_type_array() { + let root_path = paths::root().join(".cargo/config.toml"); + write_config_at(&root_path, r#"a = [{ x = 1 }]"#); + + let foo_path = paths::root().join("foo/.cargo/config.toml"); + write_config_at(&foo_path, r#"a = [true, [false]]"#); + + let gctx = GlobalContextBuilder::new() + .cwd("foo") + .env("CARGO_A", "hello") + .config_arg("a = [123]") + .build(); + + #[derive(Deserialize, Debug, PartialEq)] + #[serde(untagged)] + enum Item { + B(bool), + I(i32), + S(String), + T { x: i32 }, + L(Vec), + } + + use Item::*; + + // Simple vector works + assert_eq!( + gctx.get::>("a").unwrap(), + vec![ + T { x: 1 }, + B(true), + L(vec![false]), + S("hello".into()), + I(123) + ], + ); + + // Value and Definition works + assert_eq!( + gctx.get::>>>("a").unwrap(), + Value { + val: vec![ + Value { + val: T { x: 1 }, + definition: Definition::Path(root_path.clone()), + }, + Value { + val: B(true), + definition: Definition::Path(foo_path.clone()), + }, + Value { + val: L(vec![false]), + definition: Definition::Path(foo_path.clone()), + }, + Value { + val: S("hello".into()), + definition: Definition::Environment("CARGO_A".into()), + }, + Value { + val: I(123), + definition: Definition::Cli(None), + }, + ], + definition: Definition::Environment("CARGO_A".into()), + } + ); +}