Skip to content

Commit e7b4b25

Browse files
authored
Implement dict and list-of-scalar parsing for options sources. (#20428)
1 parent cccf8ee commit e7b4b25

File tree

6 files changed

+241
-77
lines changed

6 files changed

+241
-77
lines changed

src/rust/engine/options/src/args.rs

+48-19
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
use std::env;
55

66
use super::id::{NameTransform, OptionId, Scope};
7-
use super::parse::parse_bool;
8-
use super::OptionsSource;
7+
use super::parse::{
8+
parse_bool, parse_bool_list, parse_dict, parse_float_list, parse_int_list, ParseError,
9+
};
10+
use super::{DictEdit, OptionsSource};
911
use crate::parse::parse_string_list;
1012
use crate::ListEdit;
1113
use std::collections::HashMap;
@@ -77,6 +79,31 @@ impl Args {
7779
}
7880
Ok(None)
7981
}
82+
83+
fn get_list<T>(
84+
&self,
85+
id: &OptionId,
86+
parse_list: fn(&str) -> Result<Vec<ListEdit<T>>, ParseError>,
87+
) -> Result<Option<Vec<ListEdit<T>>>, String> {
88+
let arg_names = Self::arg_names(id, Negate::False);
89+
let mut edits = vec![];
90+
for arg in &self.args {
91+
let mut components = arg.as_str().splitn(2, '=');
92+
if let Some(name) = components.next() {
93+
if arg_names.contains_key(name) {
94+
let value = components.next().ok_or_else(|| {
95+
format!("Expected string list option {name} to have a value.")
96+
})?;
97+
edits.extend(parse_list(value).map_err(|e| e.render(name))?)
98+
}
99+
}
100+
}
101+
if edits.is_empty() {
102+
Ok(None)
103+
} else {
104+
Ok(Some(edits))
105+
}
106+
}
80107
}
81108

82109
impl OptionsSource for Args {
@@ -100,24 +127,26 @@ impl OptionsSource for Args {
100127
}
101128
}
102129

130+
fn get_bool_list(&self, id: &OptionId) -> Result<Option<Vec<ListEdit<bool>>>, String> {
131+
self.get_list(id, parse_bool_list)
132+
}
133+
134+
fn get_int_list(&self, id: &OptionId) -> Result<Option<Vec<ListEdit<i64>>>, String> {
135+
self.get_list(id, parse_int_list)
136+
}
137+
138+
fn get_float_list(&self, id: &OptionId) -> Result<Option<Vec<ListEdit<f64>>>, String> {
139+
self.get_list(id, parse_float_list)
140+
}
141+
103142
fn get_string_list(&self, id: &OptionId) -> Result<Option<Vec<ListEdit<String>>>, String> {
104-
let arg_names = Self::arg_names(id, Negate::False);
105-
let mut edits = vec![];
106-
for arg in &self.args {
107-
let mut components = arg.as_str().splitn(2, '=');
108-
if let Some(name) = components.next() {
109-
if arg_names.contains_key(name) {
110-
let value = components.next().ok_or_else(|| {
111-
format!("Expected string list option {name} to have a value.")
112-
})?;
113-
edits.extend(parse_string_list(value).map_err(|e| e.render(name))?)
114-
}
115-
}
116-
}
117-
if edits.is_empty() {
118-
Ok(None)
119-
} else {
120-
Ok(Some(edits))
143+
self.get_list(id, parse_string_list)
144+
}
145+
146+
fn get_dict(&self, id: &OptionId) -> Result<Option<DictEdit>, String> {
147+
match self.find_flag(Self::arg_names(id, Negate::False))? {
148+
Some((name, ref value, _)) => parse_dict(value).map(Some).map_err(|e| e.render(name)),
149+
None => Ok(None),
121150
}
122151
}
123152
}

src/rust/engine/options/src/args_tests.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@ fn test_float() {
9494
assert!(args.get_float(&option_id!("dne")).unwrap().is_none());
9595

9696
assert_eq!(
97-
"Problem parsing --bad value swallow as a float value: invalid float literal".to_owned(),
97+
"Problem parsing --bad float value:\n1:swallow\n ^\n\
98+
Expected \"+\", \"-\" or ['0' ..= '9'] at line 1 column 1"
99+
.to_owned(),
98100
args.get_float(&option_id!("bad")).unwrap_err()
99101
);
100102
}

src/rust/engine/options/src/config.rs

+115-35
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ use toml::value::Table;
1212
use toml::Value;
1313

1414
use super::id::{NameTransform, OptionId};
15-
use super::parse::parse_string_list;
16-
use super::{ListEdit, ListEditAction, OptionsSource};
15+
use super::parse::{
16+
parse_bool_list, parse_dict, parse_float_list, parse_int_list, parse_string_list, ParseError,
17+
};
18+
use super::{DictEdit, DictEditAction, ListEdit, ListEditAction, OptionsSource, Val};
1719

1820
type InterpolationMap = HashMap<String, String>;
1921

@@ -186,6 +188,34 @@ impl FromValue for f64 {
186188
}
187189
}
188190

191+
fn toml_value_to_val(value: &Value) -> Val {
192+
match value {
193+
Value::String(s) => Val::String(s.to_owned()),
194+
Value::Integer(i) => Val::Int(*i),
195+
Value::Float(f) => Val::Float(*f),
196+
Value::Boolean(b) => Val::Bool(*b),
197+
Value::Datetime(d) => Val::String(d.to_string()),
198+
Value::Array(a) => Val::List(a.iter().map(toml_value_to_val).collect()),
199+
Value::Table(t) => Val::Dict(
200+
t.iter()
201+
.map(|(k, v)| (k.to_string(), toml_value_to_val(v)))
202+
.collect(),
203+
),
204+
}
205+
}
206+
207+
// Helper function. Only call if you know that the arg is a Value::Table.
208+
fn toml_table_to_dict(table: &Value) -> HashMap<String, Val> {
209+
if !table.is_table() {
210+
panic!("Expected a TOML table but received: {table}");
211+
}
212+
if let Val::Dict(hm) = toml_value_to_val(table) {
213+
hm
214+
} else {
215+
panic!("toml_value_to_val() on a Value::Table must return a Val::Dict");
216+
}
217+
}
218+
189219
#[derive(Clone)]
190220
pub(crate) struct Config {
191221
config: Value,
@@ -305,6 +335,57 @@ impl Config {
305335
.and_then(|table| table.get(Self::option_name(id)))
306336
}
307337

338+
fn get_list<T: FromValue>(
339+
&self,
340+
id: &OptionId,
341+
parse_list: fn(&str) -> Result<Vec<ListEdit<T>>, ParseError>,
342+
) -> Result<Option<Vec<ListEdit<T>>>, String> {
343+
if let Some(table) = self.config.get(id.scope()) {
344+
let option_name = Self::option_name(id);
345+
let mut list_edits = vec![];
346+
if let Some(value) = table.get(&option_name) {
347+
match value {
348+
Value::Table(sub_table) => {
349+
if sub_table.is_empty()
350+
|| !sub_table.keys().collect::<HashSet<_>>().is_subset(
351+
&["add".to_owned(), "remove".to_owned()]
352+
.iter()
353+
.collect::<HashSet<_>>(),
354+
)
355+
{
356+
return Err(format!(
357+
"Expected {option_name} to contain an 'add' element, a 'remove' element or both but found: {sub_table:?}"
358+
));
359+
}
360+
if let Some(add) = sub_table.get("add") {
361+
list_edits.push(ListEdit {
362+
action: ListEditAction::Add,
363+
items: T::extract_list(&format!("{option_name}.add"), add)?,
364+
})
365+
}
366+
if let Some(remove) = sub_table.get("remove") {
367+
list_edits.push(ListEdit {
368+
action: ListEditAction::Remove,
369+
items: T::extract_list(&format!("{option_name}.remove"), remove)?,
370+
})
371+
}
372+
}
373+
Value::String(v) => {
374+
list_edits.extend(parse_list(v).map_err(|e| e.render(option_name))?);
375+
}
376+
value => list_edits.push(ListEdit {
377+
action: ListEditAction::Replace,
378+
items: T::extract_list(&option_name, value)?,
379+
}),
380+
}
381+
}
382+
if !list_edits.is_empty() {
383+
return Ok(Some(list_edits));
384+
}
385+
}
386+
Ok(None)
387+
}
388+
308389
pub(crate) fn merge(mut self, mut other: Config) -> Config {
309390
let mut map = mem::take(self.config.as_table_mut().unwrap());
310391
let mut other = mem::take(other.config.as_table_mut().unwrap());
@@ -346,52 +427,51 @@ impl OptionsSource for Config {
346427
f64::from_config(self, id)
347428
}
348429

430+
fn get_bool_list(&self, id: &OptionId) -> Result<Option<Vec<ListEdit<bool>>>, String> {
431+
self.get_list(id, parse_bool_list)
432+
}
433+
434+
fn get_int_list(&self, id: &OptionId) -> Result<Option<Vec<ListEdit<i64>>>, String> {
435+
self.get_list(id, parse_int_list)
436+
}
437+
438+
fn get_float_list(&self, id: &OptionId) -> Result<Option<Vec<ListEdit<f64>>>, String> {
439+
self.get_list(id, parse_float_list)
440+
}
441+
349442
fn get_string_list(&self, id: &OptionId) -> Result<Option<Vec<ListEdit<String>>>, String> {
443+
self.get_list(id, parse_string_list)
444+
}
445+
446+
fn get_dict(&self, id: &OptionId) -> Result<Option<DictEdit>, String> {
350447
if let Some(table) = self.config.get(id.scope()) {
351448
let option_name = Self::option_name(id);
352-
let mut list_edits = vec![];
353449
if let Some(value) = table.get(&option_name) {
354450
match value {
355451
Value::Table(sub_table) => {
356-
if sub_table.is_empty()
357-
|| !sub_table.keys().collect::<HashSet<_>>().is_subset(
358-
&["add".to_owned(), "remove".to_owned()]
359-
.iter()
360-
.collect::<HashSet<_>>(),
361-
)
362-
{
363-
return Err(format!(
364-
"Expected {option_name} to contain an 'add' element, a 'remove' element or both but found: {sub_table:?}"
365-
));
366-
}
367452
if let Some(add) = sub_table.get("add") {
368-
list_edits.push(ListEdit {
369-
action: ListEditAction::Add,
370-
items: String::extract_list(&format!("{option_name}.add"), add)?,
371-
})
372-
}
373-
if let Some(remove) = sub_table.get("remove") {
374-
list_edits.push(ListEdit {
375-
action: ListEditAction::Remove,
376-
items: String::extract_list(
377-
&format!("{option_name}.remove"),
378-
remove,
379-
)?,
380-
})
453+
if sub_table.len() == 1 && add.is_table() {
454+
return Ok(Some(DictEdit {
455+
action: DictEditAction::Add,
456+
items: toml_table_to_dict(add),
457+
}));
458+
}
381459
}
460+
return Ok(Some(DictEdit {
461+
action: DictEditAction::Replace,
462+
items: toml_table_to_dict(value),
463+
}));
382464
}
383465
Value::String(v) => {
384-
list_edits.extend(parse_string_list(v).map_err(|e| e.render(option_name))?);
466+
return Ok(Some(parse_dict(v).map_err(|e| e.render(option_name))?));
467+
}
468+
_ => {
469+
return Err(format!(
470+
"Expected {option_name} to be a toml table or Python dict, but given {value}."
471+
));
385472
}
386-
value => list_edits.push(ListEdit {
387-
action: ListEditAction::Replace,
388-
items: String::extract_list(&option_name, value)?,
389-
}),
390473
}
391474
}
392-
if !list_edits.is_empty() {
393-
return Ok(Some(list_edits));
394-
}
395475
}
396476
Ok(None)
397477
}

src/rust/engine/options/src/env.rs

+36-3
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ use std::env;
66
use std::ffi::OsString;
77

88
use super::id::{NameTransform, OptionId, Scope};
9-
use super::OptionsSource;
10-
use crate::parse::{parse_bool, parse_string_list};
9+
use super::{DictEdit, OptionsSource};
10+
use crate::parse::{
11+
parse_bool, parse_bool_list, parse_dict, parse_float_list, parse_int_list, parse_string_list,
12+
ParseError,
13+
};
1114
use crate::ListEdit;
1215

1316
#[derive(Debug)]
@@ -66,6 +69,20 @@ impl Env {
6669
}
6770
names
6871
}
72+
73+
fn get_list<T>(
74+
&self,
75+
id: &OptionId,
76+
parse_list: fn(&str) -> Result<Vec<ListEdit<T>>, ParseError>,
77+
) -> Result<Option<Vec<ListEdit<T>>>, String> {
78+
if let Some(value) = self.get_string(id)? {
79+
parse_list(&value)
80+
.map(Some)
81+
.map_err(|e| e.render(self.display(id)))
82+
} else {
83+
Ok(None)
84+
}
85+
}
6986
}
7087

7188
impl From<&Env> for Vec<(String, String)> {
@@ -102,9 +119,25 @@ impl OptionsSource for Env {
102119
}
103120
}
104121

122+
fn get_bool_list(&self, id: &OptionId) -> Result<Option<Vec<ListEdit<bool>>>, String> {
123+
self.get_list(id, parse_bool_list)
124+
}
125+
126+
fn get_int_list(&self, id: &OptionId) -> Result<Option<Vec<ListEdit<i64>>>, String> {
127+
self.get_list(id, parse_int_list)
128+
}
129+
130+
fn get_float_list(&self, id: &OptionId) -> Result<Option<Vec<ListEdit<f64>>>, String> {
131+
self.get_list(id, parse_float_list)
132+
}
133+
105134
fn get_string_list(&self, id: &OptionId) -> Result<Option<Vec<ListEdit<String>>>, String> {
135+
self.get_list(id, parse_string_list)
136+
}
137+
138+
fn get_dict(&self, id: &OptionId) -> Result<Option<DictEdit>, String> {
106139
if let Some(value) = self.get_string(id)? {
107-
parse_string_list(&value)
140+
parse_dict(&value)
108141
.map(Some)
109142
.map_err(|e| e.render(self.display(id)))
110143
} else {

src/rust/engine/options/src/env_tests.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,8 @@ fn test_float() {
137137
assert!(env.get_float(&option_id!("dne")).unwrap().is_none());
138138

139139
assert_eq!(
140-
"Problem parsing PANTS_BAD value swallow as a float value: invalid float literal"
140+
"Problem parsing PANTS_BAD float value:\n1:swallow\n ^\n\
141+
Expected \"+\", \"-\" or ['0' ..= '9'] at line 1 column 1"
141142
.to_owned(),
142143
env.get_float(&option_id!("pants", "bad")).unwrap_err()
143144
);

0 commit comments

Comments
 (0)