Skip to content

Commit 3c940a9

Browse files
authored
Inline multiple config files into options sources. (#20549)
Previously the options sources list could have only one source per type (flag/env/config/default). Since we support multiple config files, we had the concept of a "merged config" that encompassed multiple "config sources". This led to duplication of the logic that traverses and merges sources. This change gets rid of the concept of merged config. Instead, the list of options sources we consult can now include multiple config files. This is achieved by enhancing the Source::Config enum to include an ordinal (to ensure that configs are applied in the right order) and the path to the config file (purely informational). The natural `#[derive(Ord)]` will order these augmented configs appropriately within all the sources. To make this easier to achieve we change the order of the enum to be from lowest to highest priority, so that we can add config files naturally in the order in which we encounter them. This also allows us to simplify some types. In particular, we no longer need to traffic in `Vec<DictEdit>` - a single source can only return a single `DictEdit`. More importantly, this supports derivation history that includes the specific config file each value comes from.
1 parent decfe2d commit 3c940a9

File tree

10 files changed

+270
-309
lines changed

10 files changed

+270
-309
lines changed

src/rust/engine/client/src/client_tests.rs

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ async fn test_client_fingerprint_mismatch() {
4141
None,
4242
true,
4343
false,
44+
None,
4445
)
4546
.unwrap();
4647
let error = pantsd::find_pantsd(&build_root, &options_parser)

src/rust/engine/client/src/main.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ async fn execute(start: SystemTime) -> Result<i32, String> {
3232
let (env, dropped) = Env::capture_lossy();
3333
let env_items = (&env).into();
3434
let argv = env::args().collect::<Vec<_>>();
35-
let options_parser = OptionParser::new(Args::argv(), env, None, true, false)?;
35+
let options_parser = OptionParser::new(Args::argv(), env, None, true, false, None)?;
3636

3737
let use_pantsd = options_parser.parse_bool(&option_id!("pantsd"), true)?;
3838
if !use_pantsd.value {

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

+2-4
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,9 @@ impl OptionsSource for Args {
143143
self.get_list(id, parse_string_list)
144144
}
145145

146-
fn get_dict(&self, id: &OptionId) -> Result<Option<Vec<DictEdit>>, String> {
146+
fn get_dict(&self, id: &OptionId) -> Result<Option<DictEdit>, String> {
147147
match self.find_flag(Self::arg_names(id, Negate::False))? {
148-
Some((name, ref value, _)) => parse_dict(value)
149-
.map(|e| Some(vec![e]))
150-
.map_err(|e| e.render(name)),
148+
Some((name, ref value, _)) => parse_dict(value).map(Some).map_err(|e| e.render(name)),
151149
None => Ok(None),
152150
}
153151
}

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

+80-137
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the Apache License, Version 2.0 (see LICENSE).
33

44
use std::collections::{HashMap, HashSet};
5-
use std::ffi::OsString;
65
use std::fs;
76
use std::path::Path;
87

@@ -216,133 +215,16 @@ fn toml_table_to_dict(table: &Value) -> HashMap<String, Val> {
216215
}
217216
}
218217

219-
#[derive(Clone)]
220-
struct ConfigSource {
221-
#[allow(dead_code)]
222-
path: Option<OsString>,
223-
config: Value,
224-
}
225-
226-
impl ConfigSource {
227-
fn option_name(id: &OptionId) -> String {
228-
id.name("_", NameTransform::None)
229-
}
230-
231-
fn get_value(&self, id: &OptionId) -> Option<&Value> {
232-
self.config
233-
.get(id.scope())
234-
.and_then(|table| table.get(Self::option_name(id)))
235-
}
236-
237-
fn get_list<T: FromValue>(
238-
&self,
239-
id: &OptionId,
240-
parse_list: fn(&str) -> Result<Vec<ListEdit<T>>, ParseError>,
241-
) -> Result<Vec<ListEdit<T>>, String> {
242-
let mut list_edits = vec![];
243-
if let Some(table) = self.config.get(id.scope()) {
244-
let option_name = Self::option_name(id);
245-
if let Some(value) = table.get(&option_name) {
246-
match value {
247-
Value::Table(sub_table) => {
248-
if sub_table.is_empty()
249-
|| !sub_table.keys().collect::<HashSet<_>>().is_subset(
250-
&["add".to_owned(), "remove".to_owned()]
251-
.iter()
252-
.collect::<HashSet<_>>(),
253-
)
254-
{
255-
return Err(format!(
256-
"Expected {option_name} to contain an 'add' element, a 'remove' element or both but found: {sub_table:?}"
257-
));
258-
}
259-
if let Some(add) = sub_table.get("add") {
260-
list_edits.push(ListEdit {
261-
action: ListEditAction::Add,
262-
items: T::extract_list(&format!("{option_name}.add"), add)?,
263-
})
264-
}
265-
if let Some(remove) = sub_table.get("remove") {
266-
list_edits.push(ListEdit {
267-
action: ListEditAction::Remove,
268-
items: T::extract_list(&format!("{option_name}.remove"), remove)?,
269-
})
270-
}
271-
}
272-
Value::String(v) => {
273-
list_edits.extend(parse_list(v).map_err(|e| e.render(option_name))?);
274-
}
275-
value => list_edits.push(ListEdit {
276-
action: ListEditAction::Replace,
277-
items: T::extract_list(&option_name, value)?,
278-
}),
279-
}
280-
}
281-
}
282-
Ok(list_edits)
283-
}
284-
285-
fn get_dict(&self, id: &OptionId) -> Result<Option<DictEdit>, String> {
286-
if let Some(table) = self.config.get(id.scope()) {
287-
let option_name = Self::option_name(id);
288-
if let Some(value) = table.get(&option_name) {
289-
match value {
290-
Value::Table(sub_table) => {
291-
if let Some(add) = sub_table.get("add") {
292-
if sub_table.len() == 1 && add.is_table() {
293-
return Ok(Some(DictEdit {
294-
action: DictEditAction::Add,
295-
items: toml_table_to_dict(add),
296-
}));
297-
}
298-
}
299-
return Ok(Some(DictEdit {
300-
action: DictEditAction::Replace,
301-
items: toml_table_to_dict(value),
302-
}));
303-
}
304-
Value::String(v) => {
305-
return Ok(Some(parse_dict(v).map_err(|e| e.render(option_name))?));
306-
}
307-
_ => {
308-
return Err(format!(
309-
"Expected {option_name} to be a toml table or Python dict, but given {value}."
310-
));
311-
}
312-
}
313-
}
314-
}
315-
Ok(None)
316-
}
317-
}
318-
319218
#[derive(Clone)]
320219
pub(crate) struct Config {
321-
sources: Vec<ConfigSource>,
220+
value: Value,
322221
}
323222

324223
impl Config {
325224
pub(crate) fn parse<P: AsRef<Path>>(
326-
files: &[P],
327-
seed_values: &InterpolationMap,
328-
) -> Result<Config, String> {
329-
let mut sources = vec![];
330-
for file in files {
331-
sources.push(Self::parse_source(file, seed_values)?);
332-
}
333-
Ok(Config { sources })
334-
}
335-
336-
pub(crate) fn merge(self, other: Config) -> Config {
337-
Config {
338-
sources: self.sources.into_iter().chain(other.sources).collect(),
339-
}
340-
}
341-
342-
fn parse_source<P: AsRef<Path>>(
343225
file: P,
344226
seed_values: &InterpolationMap,
345-
) -> Result<ConfigSource, String> {
227+
) -> Result<Config, String> {
346228
let config_contents = fs::read_to_string(&file).map_err(|e| {
347229
format!(
348230
"Failed to read config file {}: {}",
@@ -419,29 +301,67 @@ impl Config {
419301
};
420302

421303
let new_table = Table::from_iter(new_sections?);
422-
Ok(ConfigSource {
423-
path: Some(file.as_ref().as_os_str().into()),
424-
config: Value::Table(new_table),
304+
Ok(Self {
305+
value: Value::Table(new_table),
425306
})
426307
}
427308

309+
fn option_name(id: &OptionId) -> String {
310+
id.name("_", NameTransform::None)
311+
}
312+
428313
fn get_value(&self, id: &OptionId) -> Option<&Value> {
429-
self.sources
430-
.iter()
431-
.rev()
432-
.find_map(|source| source.get_value(id))
314+
self.value
315+
.get(id.scope())
316+
.and_then(|table| table.get(Self::option_name(id)))
433317
}
434318

435319
fn get_list<T: FromValue>(
436320
&self,
437321
id: &OptionId,
438322
parse_list: fn(&str) -> Result<Vec<ListEdit<T>>, ParseError>,
439323
) -> Result<Option<Vec<ListEdit<T>>>, String> {
440-
let mut edits: Vec<ListEdit<T>> = vec![];
441-
for source in self.sources.iter() {
442-
edits.append(&mut source.get_list(id, parse_list)?);
324+
let mut list_edits = vec![];
325+
if let Some(table) = self.value.get(id.scope()) {
326+
let option_name = Self::option_name(id);
327+
if let Some(value) = table.get(&option_name) {
328+
match value {
329+
Value::Table(sub_table) => {
330+
if sub_table.is_empty()
331+
|| !sub_table.keys().collect::<HashSet<_>>().is_subset(
332+
&["add".to_owned(), "remove".to_owned()]
333+
.iter()
334+
.collect::<HashSet<_>>(),
335+
)
336+
{
337+
return Err(format!(
338+
"Expected {option_name} to contain an 'add' element, a 'remove' element or both but found: {sub_table:?}"
339+
));
340+
}
341+
if let Some(add) = sub_table.get("add") {
342+
list_edits.push(ListEdit {
343+
action: ListEditAction::Add,
344+
items: T::extract_list(&format!("{option_name}.add"), add)?,
345+
})
346+
}
347+
if let Some(remove) = sub_table.get("remove") {
348+
list_edits.push(ListEdit {
349+
action: ListEditAction::Remove,
350+
items: T::extract_list(&format!("{option_name}.remove"), remove)?,
351+
})
352+
}
353+
}
354+
Value::String(v) => {
355+
list_edits.extend(parse_list(v).map_err(|e| e.render(option_name))?);
356+
}
357+
value => list_edits.push(ListEdit {
358+
action: ListEditAction::Replace,
359+
items: T::extract_list(&option_name, value)?,
360+
}),
361+
}
362+
}
443363
}
444-
Ok(Some(edits))
364+
Ok(Some(list_edits))
445365
}
446366
}
447367

@@ -482,13 +402,36 @@ impl OptionsSource for Config {
482402
self.get_list(id, parse_string_list)
483403
}
484404

485-
fn get_dict(&self, id: &OptionId) -> Result<Option<Vec<DictEdit>>, String> {
486-
let mut edits = vec![];
487-
for source in self.sources.iter() {
488-
if let Some(edit) = source.get_dict(id)? {
489-
edits.push(edit);
405+
fn get_dict(&self, id: &OptionId) -> Result<Option<DictEdit>, String> {
406+
if let Some(table) = self.value.get(id.scope()) {
407+
let option_name = Self::option_name(id);
408+
if let Some(value) = table.get(&option_name) {
409+
match value {
410+
Value::Table(sub_table) => {
411+
if let Some(add) = sub_table.get("add") {
412+
if sub_table.len() == 1 && add.is_table() {
413+
return Ok(Some(DictEdit {
414+
action: DictEditAction::Add,
415+
items: toml_table_to_dict(add),
416+
}));
417+
}
418+
}
419+
return Ok(Some(DictEdit {
420+
action: DictEditAction::Replace,
421+
items: toml_table_to_dict(value),
422+
}));
423+
}
424+
Value::String(v) => {
425+
return Ok(Some(parse_dict(v).map_err(|e| e.render(option_name))?));
426+
}
427+
_ => {
428+
return Err(format!(
429+
"Expected {option_name} to be a toml table or Python dict, but given {value}."
430+
));
431+
}
432+
}
490433
}
491434
}
492-
Ok(if edits.is_empty() { None } else { Some(edits) })
435+
Ok(None)
493436
}
494437
}

0 commit comments

Comments
 (0)