Skip to content

Commit 8cffebd

Browse files
committed
feat(config): support array of any types
Only add the support to TOML-based Cargo config. Environment variables still only supports array of strings. `-Zadvanced-env` is not yet supported. There is a regression in `cargo_alias_config::alias_malformed_config_list` that the key path error report is missing. This may require a revamp over ConfigKey.
1 parent e0d0443 commit 8cffebd

File tree

7 files changed

+498
-323
lines changed

7 files changed

+498
-323
lines changed

src/cargo/ops/cargo_config.rs

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,21 @@ fn print_toml(gctx: &GlobalContext, opts: &GetOptions<'_>, key: &ConfigKey, cv:
115115
}
116116
format!(" # {}", def)
117117
};
118+
119+
fn cv_to_toml(cv: &CV) -> toml_edit::Value {
120+
match cv {
121+
CV::String(s, _) => toml_edit::Value::from(s.as_str()),
122+
CV::Integer(i, _) => toml_edit::Value::from(*i),
123+
CV::Boolean(b, _) => toml_edit::Value::from(*b),
124+
CV::List(l, _) => toml_edit::Value::from_iter(l.iter().map(cv_to_toml)),
125+
CV::Table(t, _) => toml_edit::Value::from_iter({
126+
let mut t: Vec<_> = t.iter().collect();
127+
t.sort_by_key(|t| t.0);
128+
t.into_iter().map(|(k, v)| (k, cv_to_toml(v)))
129+
}),
130+
}
131+
}
132+
118133
match cv {
119134
CV::Boolean(val, def) => drop_println!(gctx, "{} = {}{}", key, val, origin(def)),
120135
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:
129144
if opts.show_origin {
130145
drop_println!(gctx, "{} = [", key);
131146
for cv in vals {
132-
let (val, def) = match cv {
133-
CV::String(s, def) => (s.as_str(), def),
134-
// This is actually unreachable until we start supporting list of different types.
135-
// It should be validated already during the deserialization.
136-
v => todo!("support {} type ", v.desc()),
137-
};
138-
drop_println!(
139-
gctx,
140-
" {}, # {}",
141-
serde::Serialize::serialize(val, toml_edit::ser::ValueSerializer::new())
142-
.unwrap(),
143-
def
144-
);
147+
let val = cv_to_toml(cv);
148+
let def = cv.definition();
149+
drop_println!(gctx, " {val}, # {def}");
145150
}
146151
drop_println!(gctx, "]");
147152
} else {
148-
let vals: toml_edit::Array = vals
149-
.iter()
150-
.map(|cv| match cv {
151-
CV::String(s, _) => toml_edit::Value::from(s.as_str()),
152-
// This is actually unreachable until we start supporting list of different types.
153-
// It should be validated already during the deserialization.
154-
v => todo!("support {} type ", v.desc()),
155-
})
156-
.collect();
153+
let vals: toml_edit::Array = vals.iter().map(cv_to_toml).collect();
157154
drop_println!(gctx, "{} = {}", key, vals);
158155
}
159156
}

src/cargo/util/context/de.rs

Lines changed: 125 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -425,8 +425,6 @@ impl<'de, 'gctx> de::MapAccess<'de> for ConfigMapAccess<'gctx> {
425425
}
426426

427427
struct ConfigSeqAccess {
428-
/// The config key to the sequence.
429-
key: ConfigKey,
430428
list_iter: vec::IntoIter<CV>,
431429
}
432430

@@ -447,7 +445,6 @@ impl ConfigSeqAccess {
447445
de.gctx.get_env_list(&de.key, &mut res)?;
448446

449447
Ok(ConfigSeqAccess {
450-
key: de.key,
451448
list_iter: res.into_iter(),
452449
})
453450
}
@@ -461,13 +458,7 @@ impl<'de> de::SeqAccess<'de> for ConfigSeqAccess {
461458
T: de::DeserializeSeed<'de>,
462459
{
463460
match self.list_iter.next() {
464-
// TODO: add `def` to error?
465-
Some(val @ CV::String(..)) => {
466-
// This might be a String or a Value<String>.
467-
// ArrayItemDeserializer will handle figuring out which one it is.
468-
seed.deserialize(ArrayItemDeserializer::new(val)).map(Some)
469-
}
470-
Some(val) => Err(ConfigError::expected(&self.key, "list of string", &val)),
461+
Some(val) => seed.deserialize(ArrayItemDeserializer::new(val)).map(Some),
471462
None => Ok(None),
472463
}
473464
}
@@ -482,11 +473,10 @@ enum ValueSource<'gctx> {
482473
},
483474
/// A [`ConfigValue`](CV).
484475
///
485-
/// This is used for situations where you can't address a string via a TOML key,
486-
/// such as a string inside an array.
487-
/// The [`ConfigSeqAccess`] doesn't know if the type it should deserialize to
488-
/// is a `String` or `Value<String>`,
489-
/// so [`ArrayItemDeserializer`] needs to be able to handle both.
476+
/// This is used for situations where you can't address type via a TOML key,
477+
/// such as a value inside an array.
478+
/// The [`ConfigSeqAccess`] doesn't know what type it should deserialize to
479+
/// so [`ArrayItemDeserializer`] needs to be able to handle all of them.
490480
ConfigValue(CV),
491481
}
492482

@@ -605,10 +595,8 @@ impl<'de, 'gctx> de::MapAccess<'de> for ValueDeserializer<'gctx> {
605595

606596
/// A deserializer for individual [`ConfigValue`](CV) items in arrays
607597
///
608-
/// This deserializer is only implemented to handle deserializing a String
609-
/// inside a sequence (like `Vec<String>` or `Vec<Value<String>>`).
610-
/// `Value<String>` is handled by `deserialize_struct` in [`ValueDeserializer`],
611-
/// and the plain `String` is handled by all the other functions here.
598+
/// It is implemented to handle any types inside a sequence, like `Vec<String>`,
599+
/// `Vec<Value<i32>>`, or even `Vev<HashMap<String, Vec<bool>>>`.
612600
#[derive(Clone)]
613601
struct ArrayItemDeserializer {
614602
cv: CV,
@@ -618,32 +606,11 @@ impl ArrayItemDeserializer {
618606
fn new(cv: CV) -> Self {
619607
Self { cv }
620608
}
621-
622-
fn into_inner(self) -> String {
623-
match self.cv {
624-
CV::String(s, _def) => s,
625-
_ => unreachable!("string expected"),
626-
}
627-
}
628609
}
629610

630611
impl<'de> de::Deserializer<'de> for ArrayItemDeserializer {
631612
type Error = ConfigError;
632613

633-
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
634-
where
635-
V: de::Visitor<'de>,
636-
{
637-
visitor.visit_str(&self.into_inner())
638-
}
639-
640-
fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
641-
where
642-
V: de::Visitor<'de>,
643-
{
644-
visitor.visit_string(self.into_inner())
645-
}
646-
647614
fn deserialize_struct<V>(
648615
self,
649616
name: &'static str,
@@ -660,31 +627,136 @@ impl<'de> de::Deserializer<'de> for ArrayItemDeserializer {
660627
if name == value::NAME && fields == value::FIELDS {
661628
return visitor.visit_map(ValueDeserializer::with_cv(self.cv));
662629
}
663-
unimplemented!("only strings and Value can be deserialized from a sequence");
630+
visitor.visit_map(ArrayItemMapAccess::with_struct(self.cv, fields))
664631
}
665632

666633
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
667634
where
668635
V: de::Visitor<'de>,
669636
{
670-
visitor.visit_string(self.into_inner())
637+
match self.cv {
638+
CV::String(s, _) => visitor.visit_string(s),
639+
CV::Integer(i, _) => visitor.visit_i64(i),
640+
CV::Boolean(b, _) => visitor.visit_bool(b),
641+
l @ CV::List(_, _) => visitor.visit_seq(ArrayItemSeqAccess::new(l)),
642+
t @ CV::Table(_, _) => visitor.visit_map(ArrayItemMapAccess::new(t)),
643+
}
644+
}
645+
646+
// Forward everything to deserialize_any
647+
serde::forward_to_deserialize_any! {
648+
bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string seq
649+
bytes byte_buf map option unit newtype_struct
650+
ignored_any unit_struct tuple_struct tuple enum identifier
651+
}
652+
}
653+
654+
/// Sequence access for nested arrays within [`ArrayItemDeserializer`]
655+
struct ArrayItemSeqAccess {
656+
items: vec::IntoIter<CV>,
657+
}
658+
659+
impl ArrayItemSeqAccess {
660+
fn new(cv: CV) -> Self {
661+
let items = match cv {
662+
CV::List(list, _) => list.into_iter(),
663+
_ => unreachable!("must be a list"),
664+
};
665+
Self { items }
671666
}
667+
}
668+
669+
impl<'de> de::SeqAccess<'de> for ArrayItemSeqAccess {
670+
type Error = ConfigError;
672671

673-
fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
672+
fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Self::Error>
674673
where
675-
V: de::Visitor<'de>,
674+
T: de::DeserializeSeed<'de>,
676675
{
677-
visitor.visit_unit()
676+
match self.items.next() {
677+
Some(cv) => {
678+
let deserializer = ArrayItemDeserializer::new(cv);
679+
seed.deserialize(deserializer).map(Some)
680+
}
681+
None => Ok(None),
682+
}
678683
}
684+
}
679685

680-
serde::forward_to_deserialize_any! {
681-
i8 i16 i32 i64
682-
u8 u16 u32 u64
683-
option
684-
newtype_struct seq tuple tuple_struct map enum bool
685-
f32 f64 char bytes
686-
byte_buf unit unit_struct
687-
identifier
686+
/// Map access for nested tables within [`ArrayItemDeserializer`]
687+
struct ArrayItemMapAccess {
688+
cv: CV,
689+
keys: vec::IntoIter<String>,
690+
current_key: Option<String>,
691+
}
692+
693+
impl ArrayItemMapAccess {
694+
fn new(cv: CV) -> Self {
695+
let keys = match &cv {
696+
CV::Table(map, _) => map.keys().cloned().collect::<Vec<_>>().into_iter(),
697+
_ => unreachable!("must be a map"),
698+
};
699+
Self {
700+
cv,
701+
keys,
702+
current_key: None,
703+
}
704+
}
705+
706+
fn with_struct(cv: CV, given_fields: &[&str]) -> Self {
707+
// TODO: We might want to warn unused fields,
708+
// like what we did in ConfigMapAccess::new_struct
709+
let keys = given_fields
710+
.into_iter()
711+
.map(|s| s.to_string())
712+
.collect::<Vec<_>>()
713+
.into_iter();
714+
Self {
715+
cv,
716+
keys,
717+
current_key: None,
718+
}
719+
}
720+
}
721+
722+
impl<'de> de::MapAccess<'de> for ArrayItemMapAccess {
723+
type Error = ConfigError;
724+
725+
fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
726+
where
727+
K: de::DeserializeSeed<'de>,
728+
{
729+
match self.keys.next() {
730+
Some(key) => {
731+
self.current_key = Some(key.clone());
732+
seed.deserialize(key.into_deserializer()).map(Some)
733+
}
734+
None => Ok(None),
735+
}
736+
}
737+
738+
fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
739+
where
740+
V: de::DeserializeSeed<'de>,
741+
{
742+
let key = self.current_key.take().unwrap();
743+
match &self.cv {
744+
CV::Table(map, _) => {
745+
if let Some(cv) = map.get(&key) {
746+
let deserializer = ArrayItemDeserializer::new(cv.clone());
747+
seed.deserialize(deserializer)
748+
} else {
749+
Err(ConfigError::new(
750+
format!("missing config key `{key}`"),
751+
self.cv.definition().clone(),
752+
))
753+
}
754+
}
755+
_ => Err(ConfigError::new(
756+
"expected table".to_string(),
757+
self.cv.definition().clone(),
758+
)),
759+
}
688760
}
689761
}
690762

src/cargo/util/context/mod.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,7 +1025,8 @@ impl GlobalContext {
10251025
})?;
10261026
let values = toml_v.as_array().expect("env var was not array");
10271027
for value in values {
1028-
// TODO: support other types.
1028+
// Until we figure out how to deal with it through `-Zadvanced-env`,
1029+
// complex array types are unsupported.
10291030
let s = value.as_str().ok_or_else(|| {
10301031
ConfigError::new(
10311032
format!("expected string, found {}", value.type_str()),
@@ -2131,12 +2132,9 @@ impl ConfigValue {
21312132
toml::Value::Array(val) => Ok(CV::List(
21322133
val.into_iter()
21332134
.enumerate()
2134-
.map(|(i, toml)| match toml {
2135-
toml::Value::String(val) => Ok(CV::String(val, def.clone())),
2136-
v => {
2137-
path.push(KeyOrIdx::Idx(i));
2138-
bail!("expected string but found {} at index {i}", v.type_str())
2139-
}
2135+
.map(|(i, toml)| {
2136+
CV::from_toml_inner(def.clone(), toml, path)
2137+
.inspect_err(|_| path.push(KeyOrIdx::Idx(i)))
21402138
})
21412139
.collect::<CargoResult<_>>()?,
21422140
def,

tests/testsuite/bad_config.rs

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -86,36 +86,6 @@ Caused by:
8686
.run();
8787
}
8888

89-
#[cargo_test]
90-
fn unsupported_int_array() {
91-
let p = project()
92-
.file("src/lib.rs", "")
93-
.file(
94-
".cargo/config.toml",
95-
r#"
96-
[alias]
97-
ints = [1, 2]
98-
"#,
99-
)
100-
.build();
101-
p.cargo("check")
102-
.with_status(101)
103-
.with_stderr_data(str![[r#"
104-
[ERROR] could not load Cargo configuration
105-
106-
Caused by:
107-
failed to load TOML configuration from `[ROOT]/foo/.cargo/config.toml`
108-
109-
Caused by:
110-
failed to parse config at `alias.ints[0]`
111-
112-
Caused by:
113-
expected string but found integer at index 0
114-
115-
"#]])
116-
.run();
117-
}
118-
11989
#[cargo_test]
12090
fn unsupported_float_array() {
12191
let p = project()
@@ -140,7 +110,7 @@ Caused by:
140110
failed to parse config at `alias.floats[0]`
141111
142112
Caused by:
143-
expected string but found float at index 0
113+
unsupported TOML configuration type `float`
144114
145115
"#]])
146116
.run();
@@ -170,7 +140,7 @@ Caused by:
170140
failed to parse config at `alias.datetimes[0]`
171141
172142
Caused by:
173-
expected string but found datetime at index 0
143+
unsupported TOML configuration type `datetime`
174144
175145
"#]])
176146
.run();

0 commit comments

Comments
 (0)