Skip to content

Commit 5944b04

Browse files
authored
Split fromfile support into its own submodule (#20807)
Background: Option @fromfile paths are intended to be relative to the buildroot, not the cwd. This will require us to plumb buildroot information through in several places. This change is prep work for that change, and is straightforward and technical, despite the diff stats: It just splits fromfile support out into its own submodule. This will make the subsequent changes easier to grok. The new fromfile.rs and fromfile_tests.rs contain code moved from parser.rs and parser_tests.rs, with no functionality changes. The other diffs are just import changes.
1 parent 7a11f54 commit 5944b04

11 files changed

+456
-433
lines changed

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ use std::env;
55

66
use super::id::{is_valid_scope_name, NameTransform, OptionId, Scope};
77
use super::{DictEdit, OptionsSource};
8-
use crate::parse::{expand, expand_to_dict, expand_to_list, ParseError, Parseable};
8+
use crate::fromfile::{expand, expand_to_dict, expand_to_list};
9+
use crate::parse::{ParseError, Parseable};
910
use crate::ListEdit;
1011
use core::iter::once;
1112
use itertools::{chain, Itertools};

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use core::fmt::Debug;
55
use maplit::hashmap;
66

77
use crate::args::Args;
8-
use crate::parse::test_util::write_fromfile;
8+
use crate::fromfile::test_util::write_fromfile;
99
use crate::{option_id, DictEdit, DictEditAction, Val};
1010
use crate::{ListEdit, ListEditAction, OptionId, OptionsSource};
1111

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ use regex::Regex;
1010
use toml::value::Table;
1111
use toml::Value;
1212

13-
use super::id::{NameTransform, OptionId};
14-
use super::parse::{expand, expand_to_dict, expand_to_list, Parseable};
1513
use super::{DictEdit, DictEditAction, ListEdit, ListEditAction, OptionsSource, Val};
14+
use crate::fromfile::{expand, expand_to_dict, expand_to_list};
15+
use crate::id::{NameTransform, OptionId};
16+
use crate::parse::Parseable;
1617

1718
type InterpolationMap = HashMap<String, String>;
1819

src/rust/engine/options/src/config_tests.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use crate::{
1414
};
1515

1616
use crate::config::Config;
17-
use crate::parse::test_util::write_fromfile;
17+
use crate::fromfile::test_util::write_fromfile;
1818
use tempfile::TempDir;
1919

2020
fn maybe_config(file_content: &str) -> Result<Config, String> {

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ use std::ffi::OsString;
77

88
use super::id::{NameTransform, OptionId, Scope};
99
use super::{DictEdit, OptionsSource};
10-
use crate::parse::{expand, expand_to_dict, expand_to_list, Parseable};
10+
use crate::fromfile::{expand, expand_to_dict, expand_to_list};
11+
use crate::parse::Parseable;
1112
use crate::ListEdit;
1213

1314
#[derive(Debug)]

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

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

44
use crate::env::Env;
5-
use crate::parse::test_util::write_fromfile;
5+
use crate::fromfile::test_util::write_fromfile;
66
use crate::{option_id, DictEdit, DictEditAction};
77
use crate::{ListEdit, ListEditAction, OptionId, OptionsSource, Val};
88
use maplit::hashmap;
+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright 2024 Pants project contributors (see CONTRIBUTORS.md).
2+
// Licensed under the Apache License, Version 2.0 (see LICENSE).
3+
4+
use super::{DictEdit, DictEditAction, ListEdit, ListEditAction};
5+
6+
use crate::parse::{mk_parse_err, parse_dict, ParseError, Parseable};
7+
use log::warn;
8+
use serde::de::Deserialize;
9+
use std::path::{Path, PathBuf};
10+
use std::{fs, io};
11+
12+
// If the corresponding unexpanded value points to a @fromfile, then the
13+
// first component is the path to that file, and the second is the value from the file,
14+
// or None if the file doesn't exist and the @?fromfile syntax was used.
15+
//
16+
// Otherwise, the first component is None and the second is the original value.
17+
type ExpandedValue = (Option<PathBuf>, Option<String>);
18+
19+
fn maybe_expand(value: String) -> Result<ExpandedValue, ParseError> {
20+
if let Some(suffix) = value.strip_prefix('@') {
21+
if suffix.starts_with('@') {
22+
// @@ escapes the initial @.
23+
Ok((None, Some(suffix.to_owned())))
24+
} else {
25+
match suffix.strip_prefix('?') {
26+
Some(subsuffix) => {
27+
// @? means the path is allowed to not exist.
28+
let path = PathBuf::from(subsuffix);
29+
match fs::read_to_string(&path) {
30+
Ok(content) => Ok((Some(path), Some(content))),
31+
Err(err) if err.kind() == io::ErrorKind::NotFound => {
32+
warn!("Optional file config '{}' does not exist.", path.display());
33+
Ok((Some(path), None))
34+
}
35+
Err(err) => Err(mk_parse_err(err, &path)),
36+
}
37+
}
38+
_ => {
39+
let path = PathBuf::from(suffix);
40+
let content = fs::read_to_string(&path).map_err(|e| mk_parse_err(e, &path))?;
41+
Ok((Some(path), Some(content)))
42+
}
43+
}
44+
}
45+
} else {
46+
Ok((None, Some(value)))
47+
}
48+
}
49+
50+
pub(crate) fn expand(value: String) -> Result<Option<String>, ParseError> {
51+
let (_, expanded_value) = maybe_expand(value)?;
52+
Ok(expanded_value)
53+
}
54+
55+
#[derive(Debug)]
56+
enum FromfileType {
57+
Json,
58+
Yaml,
59+
Unknown,
60+
}
61+
62+
impl FromfileType {
63+
fn detect(path: &Path) -> FromfileType {
64+
if let Some(ext) = path.extension() {
65+
if ext == "json" {
66+
return FromfileType::Json;
67+
} else if ext == "yml" || ext == "yaml" {
68+
return FromfileType::Yaml;
69+
};
70+
}
71+
FromfileType::Unknown
72+
}
73+
}
74+
75+
fn try_deserialize<'a, DE: Deserialize<'a>>(
76+
value: &'a str,
77+
path_opt: Option<PathBuf>,
78+
) -> Result<Option<DE>, ParseError> {
79+
if let Some(path) = path_opt {
80+
match FromfileType::detect(&path) {
81+
FromfileType::Json => serde_json::from_str(value).map_err(|e| mk_parse_err(e, &path)),
82+
FromfileType::Yaml => serde_yaml::from_str(value).map_err(|e| mk_parse_err(e, &path)),
83+
_ => Ok(None),
84+
}
85+
} else {
86+
Ok(None)
87+
}
88+
}
89+
90+
pub(crate) fn expand_to_list<T: Parseable>(
91+
value: String,
92+
) -> Result<Option<Vec<ListEdit<T>>>, ParseError> {
93+
let (path_opt, value_opt) = maybe_expand(value)?;
94+
if let Some(value) = value_opt {
95+
if let Some(items) = try_deserialize(&value, path_opt)? {
96+
Ok(Some(vec![ListEdit {
97+
action: ListEditAction::Replace,
98+
items,
99+
}]))
100+
} else {
101+
T::parse_list(&value).map(Some)
102+
}
103+
} else {
104+
Ok(None)
105+
}
106+
}
107+
108+
pub(crate) fn expand_to_dict(value: String) -> Result<Option<Vec<DictEdit>>, ParseError> {
109+
let (path_opt, value_opt) = maybe_expand(value)?;
110+
if let Some(value) = value_opt {
111+
if let Some(items) = try_deserialize(&value, path_opt)? {
112+
Ok(Some(vec![DictEdit {
113+
action: DictEditAction::Replace,
114+
items,
115+
}]))
116+
} else {
117+
parse_dict(&value).map(|x| Some(vec![x]))
118+
}
119+
} else {
120+
Ok(None)
121+
}
122+
}
123+
124+
#[cfg(test)]
125+
pub(crate) mod test_util {
126+
use std::fs::File;
127+
use std::io::Write;
128+
use std::path::PathBuf;
129+
use tempfile::{tempdir, TempDir};
130+
131+
pub(crate) fn write_fromfile(filename: &str, content: &str) -> (TempDir, PathBuf) {
132+
let tmpdir = tempdir().unwrap();
133+
let fromfile_path = tmpdir.path().join(filename);
134+
let mut fromfile = File::create(&fromfile_path).unwrap();
135+
fromfile.write_all(content.as_bytes()).unwrap();
136+
fromfile.flush().unwrap();
137+
(tmpdir, fromfile_path)
138+
}
139+
}

0 commit comments

Comments
 (0)