Skip to content

Commit 8e4bc4a

Browse files
authored
Merge pull request #2942 from ehuss/fix-env-config
Add error handling to env config handling
2 parents ef476a7 + 2afad43 commit 8e4bc4a

File tree

5 files changed

+183
-44
lines changed

5 files changed

+183
-44
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ This release also includes many new features described below.
99

1010
We have prepared a [0.5 Migration Guide](#05-migration-guide) to help existing authors switch from 0.4.
1111

12-
The final 0.5.0 release does not contain any changes since [0.5.0-beta.2](#mdbook-050-beta2).
12+
The final 0.5.0 release only contains the following changes since [0.5.0-beta.2](#mdbook-050-beta2):
13+
14+
- Added error handling to environment config handling. This checks that environment variables starting with `MDBOOK_` are correctly specified instead of silently ignoring. This also fixed being able to replace entire top-level tables like `MDBOOK_OUTPUT`.
15+
[#2942](https://github.com/rust-lang/mdBook/pull/2942)
1316

1417
## 0.5 Migration Guide
1518

@@ -56,6 +59,10 @@ The following is a summary of the changes that may require your attention when u
5659
[#2775](https://github.com/rust-lang/mdBook/pull/2775)
5760
- Removed the very old legacy config support. Warnings have been displayed in previous versions on how to migrate.
5861
[#2783](https://github.com/rust-lang/mdBook/pull/2783)
62+
- Top-level config values set from the environment like `MDBOOK_BOOK` now *replace* the contents of the top-level table instead of merging into it.
63+
[#2942](https://github.com/rust-lang/mdBook/pull/2942)
64+
- Invalid environment variables are now rejected. Previously unknown keys like `MDBOOK_FOO` would be ignored, or keys or invalid values inside objects like the `[book]` table would be ignored.
65+
[#2942](https://github.com/rust-lang/mdBook/pull/2942)
5966

6067
### Theme changes
6168

crates/mdbook-core/src/config.rs

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,10 @@ impl Config {
123123
///
124124
/// For example:
125125
///
126-
/// - `MDBOOK_foo` -> `foo`
127-
/// - `MDBOOK_FOO` -> `foo`
128-
/// - `MDBOOK_FOO__BAR` -> `foo.bar`
129-
/// - `MDBOOK_FOO_BAR` -> `foo-bar`
130-
/// - `MDBOOK_FOO_bar__baz` -> `foo-bar.baz`
126+
/// - `MDBOOK_book` -> `book`
127+
/// - `MDBOOK_BOOK` -> `book`
128+
/// - `MDBOOK_BOOK__TITLE` -> `book.title`
129+
/// - `MDBOOK_BOOK__TEXT_DIRECTION` -> `book.text-direction`
131130
///
132131
/// So by setting the `MDBOOK_BOOK__TITLE` environment variable you can
133132
/// override the book's title without needing to touch your `book.toml`.
@@ -147,7 +146,7 @@ impl Config {
147146
/// The latter case may be useful in situations where `mdbook` is invoked
148147
/// from a script or CI, where it sometimes isn't possible to update the
149148
/// `book.toml` before building.
150-
pub fn update_from_env(&mut self) {
149+
pub fn update_from_env(&mut self) -> Result<()> {
151150
debug!("Updating the config from environment variables");
152151

153152
let overrides =
@@ -162,19 +161,9 @@ impl Config {
162161
let parsed_value = serde_json::from_str(&value)
163162
.unwrap_or_else(|_| serde_json::Value::String(value.to_string()));
164163

165-
if key == "book" || key == "build" {
166-
if let serde_json::Value::Object(ref map) = parsed_value {
167-
// To `set` each `key`, we wrap them as `prefix.key`
168-
for (k, v) in map {
169-
let full_key = format!("{key}.{k}");
170-
self.set(&full_key, v).expect("unreachable");
171-
}
172-
return;
173-
}
174-
}
175-
176-
self.set(key, parsed_value).expect("unreachable");
164+
self.set(key, parsed_value)?;
177165
}
166+
Ok(())
178167
}
179168

180169
/// Get a value from the configuration.
@@ -266,24 +255,39 @@ impl Config {
266255
/// `output.html.playground` will set the "playground" in the html output
267256
/// table).
268257
///
269-
/// The only way this can fail is if we can't serialize `value` into a
270-
/// `toml::Value`.
258+
/// # Errors
259+
///
260+
/// This will fail if:
261+
///
262+
/// - The value cannot be represented as TOML.
263+
/// - The value is not a correct type.
264+
/// - The key is an unknown configuration option.
271265
pub fn set<S: Serialize, I: AsRef<str>>(&mut self, index: I, value: S) -> Result<()> {
272266
let index = index.as_ref();
273267

274268
let value = Value::try_from(value)
275269
.with_context(|| "Unable to represent the item as a JSON Value")?;
276270

277-
if let Some(key) = index.strip_prefix("book.") {
278-
self.book.update_value(key, value);
271+
if index == "book" {
272+
self.book = value.try_into()?;
273+
} else if index == "build" {
274+
self.build = value.try_into()?;
275+
} else if index == "rust" {
276+
self.rust = value.try_into()?;
277+
} else if index == "output" {
278+
self.output = value;
279+
} else if index == "preprocessor" {
280+
self.preprocessor = value;
281+
} else if let Some(key) = index.strip_prefix("book.") {
282+
self.book.update_value(key, value)?;
279283
} else if let Some(key) = index.strip_prefix("build.") {
280-
self.build.update_value(key, value);
284+
self.build.update_value(key, value)?;
281285
} else if let Some(key) = index.strip_prefix("rust.") {
282-
self.rust.update_value(key, value);
286+
self.rust.update_value(key, value)?;
283287
} else if let Some(key) = index.strip_prefix("output.") {
284-
self.output.update_value(key, value);
288+
self.output.update_value(key, value)?;
285289
} else if let Some(key) = index.strip_prefix("preprocessor.") {
286-
self.preprocessor.update_value(key, value);
290+
self.preprocessor.update_value(key, value)?;
287291
} else {
288292
bail!("invalid key `{index}`");
289293
}
@@ -703,18 +707,13 @@ pub struct SearchChapterSettings {
703707
/// This is definitely not the most performant way to do things, which means you
704708
/// should probably keep it away from tight loops...
705709
trait Updateable<'de>: Serialize + Deserialize<'de> {
706-
fn update_value<S: Serialize>(&mut self, key: &str, value: S) {
710+
fn update_value<S: Serialize>(&mut self, key: &str, value: S) -> Result<()> {
707711
let mut raw = Value::try_from(&self).expect("unreachable");
708-
709-
if let Ok(value) = Value::try_from(value) {
710-
raw.insert(key, value);
711-
} else {
712-
return;
713-
}
714-
715-
if let Ok(updated) = raw.try_into() {
716-
*self = updated;
717-
}
712+
let value = Value::try_from(value)?;
713+
raw.insert(key, value);
714+
let updated = raw.try_into()?;
715+
*self = updated;
716+
Ok(())
718717
}
719718
}
720719

crates/mdbook-driver/src/mdbook.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ impl MDBook {
5656
Config::default()
5757
};
5858

59-
config.update_from_env();
59+
config.update_from_env()?;
6060

6161
if tracing::enabled!(tracing::Level::TRACE) {
6262
for line in format!("Config: {config:#?}").lines() {

guide/src/format/configuration/environment-variables.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,10 @@ underscore (`_`) is replaced with a dash (`-`).
1212

1313
For example:
1414

15-
- `MDBOOK_foo` -> `foo`
16-
- `MDBOOK_FOO` -> `foo`
17-
- `MDBOOK_FOO__BAR` -> `foo.bar`
18-
- `MDBOOK_FOO_BAR` -> `foo-bar`
19-
- `MDBOOK_FOO_bar__baz` -> `foo-bar.baz`
15+
- `MDBOOK_book` -> `book`
16+
- `MDBOOK_BOOK` -> `book`
17+
- `MDBOOK_BOOK__TITLE` -> `book.title`
18+
- `MDBOOK_BOOK__TEXT_DIRECTION` -> `book.text-direction`
2019

2120
So by setting the `MDBOOK_BOOK__TITLE` environment variable you can override the
2221
book's title without needing to touch your `book.toml`.

tests/testsuite/config.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,3 +207,137 @@ unknown field `title`, expected `edition`
207207
"#]]);
208208
});
209209
}
210+
211+
// An invalid top-level key in the environment.
212+
#[test]
213+
fn env_invalid_config_key() {
214+
BookTest::from_dir("config/empty").run("build", |cmd| {
215+
cmd.env("MDBOOK_FOO", "testing")
216+
.expect_failure()
217+
.expect_stdout(str![[""]])
218+
.expect_stderr(str![[r#"
219+
ERROR invalid key `foo`
220+
221+
"#]]);
222+
});
223+
}
224+
225+
// An invalid value in the environment.
226+
#[test]
227+
fn env_invalid_value() {
228+
BookTest::from_dir("config/empty")
229+
.run("build", |cmd| {
230+
cmd.env("MDBOOK_BOOK", r#"{"titlez": "typo"}"#)
231+
.expect_failure()
232+
.expect_stdout(str![[""]])
233+
.expect_stderr(str![[r#"
234+
ERROR unknown field `titlez`, expected one of `title`, `authors`, `description`, `src`, `language`, `text-direction`
235+
236+
237+
"#]]);
238+
})
239+
.run("build", |cmd| {
240+
cmd.env("MDBOOK_BOOK__TITLE", r#"{"looks like obj": "abc"}"#)
241+
.expect_failure()
242+
.expect_stdout(str![[""]])
243+
.expect_stderr(str![[r#"
244+
ERROR invalid type: map, expected a string
245+
in `title`
246+
247+
248+
"#]]);
249+
})
250+
// This is not valid JSON, so falls back to be interpreted as a string.
251+
.run("build", |cmd| {
252+
cmd.env("MDBOOK_BOOK__TITLE", r#"{braces}"#)
253+
.expect_stdout(str![[""]])
254+
.expect_stderr(str![[r#"
255+
INFO Book building has started
256+
INFO Running the html backend
257+
INFO HTML book written to `[ROOT]/book`
258+
259+
"#]]);
260+
})
261+
.check_file_contains("book/index.html", "<title>Chapter 1 - {braces}</title>");
262+
}
263+
264+
// Replacing the entire book table from the environment.
265+
#[test]
266+
fn env_entire_book_table() {
267+
BookTest::init(|_| {})
268+
.change_file(
269+
"book.toml",
270+
"[book]\n\
271+
title = \"config title\"\n\
272+
",
273+
)
274+
.run("build", |cmd| {
275+
cmd.env("MDBOOK_BOOK", r#"{"description": "custom description"}"#);
276+
})
277+
// The book.toml title is removed.
278+
.check_file_contains("book/index.html", "<title>Chapter 1</title>")
279+
.check_file_contains(
280+
"book/index.html",
281+
r#"<meta name="description" content="custom description">"#,
282+
);
283+
}
284+
285+
// Replacing the entire output or preprocessor table from the environment.
286+
#[test]
287+
fn env_entire_output_preprocessor_table() {
288+
BookTest::from_dir("config/empty")
289+
.rust_program(
290+
"mdbook-my-preprocessor",
291+
r#"
292+
fn main() {
293+
let mut args = std::env::args().skip(1);
294+
if args.next().as_deref() == Some("supports") {
295+
return;
296+
}
297+
use std::io::Read;
298+
let mut s = String::new();
299+
std::io::stdin().read_to_string(&mut s).unwrap();
300+
assert!(s.contains("custom preprocessor config"));
301+
println!("{{\"items\": []}}");
302+
}
303+
"#,
304+
)
305+
.rust_program(
306+
"mdbook-my-output",
307+
r#"
308+
fn main() {
309+
use std::io::Read;
310+
let mut s = String::new();
311+
std::io::stdin().read_to_string(&mut s).unwrap();
312+
assert!(s.contains("custom output config"));
313+
eprintln!("preprocessor saw custom config");
314+
}
315+
"#,
316+
)
317+
.run("build", |cmd| {
318+
let mut paths: Vec<_> =
319+
std::env::split_paths(&std::env::var_os("PATH").unwrap_or_default()).collect();
320+
paths.push(cmd.dir.clone());
321+
let path = std::env::join_paths(paths).unwrap().into_string().unwrap();
322+
323+
cmd.env(
324+
"MDBOOK_OUTPUT",
325+
r#"{"my-output": {"foo": "custom output config"}}"#,
326+
)
327+
.env(
328+
"MDBOOK_PREPROCESSOR",
329+
r#"{"my-preprocessor": {"foo": "custom preprocessor config"}}"#,
330+
)
331+
.env("PATH", path)
332+
.expect_stdout(str![[""]])
333+
.expect_stderr(str![[r#"
334+
INFO Book building has started
335+
INFO Running the my-output backend
336+
INFO Invoking the "my-output" renderer
337+
preprocessor saw custom config
338+
339+
"#]]);
340+
})
341+
// No HTML output
342+
.check_file_list("book", str![[""]]);
343+
}

0 commit comments

Comments
 (0)