Skip to content

Commit 2afad43

Browse files
committed
Add error handling to env config handling
This adds several changes to how environment variables are handled to more closely align with how configs are handled, and to fix an issue with replacing entire tables. The changes are: - Top-level tables like `MDBOOK_BOOK` now *replace* the contents of the `book` table instead of merging it. This adds consistency with how all the other environment objects work. - Fixed allowing top-level replacement of `MDBOOK_BOOK` and `MDBOOK_OUTPUT`. This was inadvertently recently broken. - Added ability to replace top-level `MDBOOK_RUST`. I don't recall why that wasn't included. - Reject invalid keys like `MDBOOK_FOO`. - Reject unknown keys, like `MDBOOK_BOOK='{"xyz": 123}'` - Reject invalid types, like `MDBOOK_BOOK='{"title": 123}'`
1 parent 5445458 commit 2afad43

File tree

5 files changed

+68
-62
lines changed

5 files changed

+68
-62
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: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -216,10 +216,7 @@ fn env_invalid_config_key() {
216216
.expect_failure()
217217
.expect_stdout(str![[""]])
218218
.expect_stderr(str![[r#"
219-
220-
thread 'main' ([..]) panicked at [..]
221-
unreachable: invalid key `foo`
222-
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
219+
ERROR invalid key `foo`
223220
224221
"#]]);
225222
});
@@ -231,25 +228,26 @@ fn env_invalid_value() {
231228
BookTest::from_dir("config/empty")
232229
.run("build", |cmd| {
233230
cmd.env("MDBOOK_BOOK", r#"{"titlez": "typo"}"#)
231+
.expect_failure()
234232
.expect_stdout(str![[""]])
235233
.expect_stderr(str![[r#"
236-
INFO Book building has started
237-
INFO Running the html backend
238-
INFO HTML book written to `[ROOT]/book`
234+
ERROR unknown field `titlez`, expected one of `title`, `authors`, `description`, `src`, `language`, `text-direction`
235+
239236
240237
"#]]);
241238
})
242239
.run("build", |cmd| {
243240
cmd.env("MDBOOK_BOOK__TITLE", r#"{"looks like obj": "abc"}"#)
241+
.expect_failure()
244242
.expect_stdout(str![[""]])
245243
.expect_stderr(str![[r#"
246-
INFO Book building has started
247-
INFO Running the html backend
248-
INFO HTML book written to `[ROOT]/book`
244+
ERROR invalid type: map, expected a string
245+
in `title`
246+
249247
250248
"#]]);
251249
})
252-
.check_file_contains("book/index.html", "<title>Chapter 1</title>")
250+
// This is not valid JSON, so falls back to be interpreted as a string.
253251
.run("build", |cmd| {
254252
cmd.env("MDBOOK_BOOK__TITLE", r#"{braces}"#)
255253
.expect_stdout(str![[""]])
@@ -276,7 +274,8 @@ fn env_entire_book_table() {
276274
.run("build", |cmd| {
277275
cmd.env("MDBOOK_BOOK", r#"{"description": "custom description"}"#);
278276
})
279-
.check_file_contains("book/index.html", "<title>Chapter 1 - config title</title>")
277+
// The book.toml title is removed.
278+
.check_file_contains("book/index.html", "<title>Chapter 1</title>")
280279
.check_file_contains(
281280
"book/index.html",
282281
r#"<meta name="description" content="custom description">"#,
@@ -311,6 +310,7 @@ fn env_entire_output_preprocessor_table() {
311310
let mut s = String::new();
312311
std::io::stdin().read_to_string(&mut s).unwrap();
313312
assert!(s.contains("custom output config"));
313+
eprintln!("preprocessor saw custom config");
314314
}
315315
"#,
316316
)
@@ -329,14 +329,15 @@ fn env_entire_output_preprocessor_table() {
329329
r#"{"my-preprocessor": {"foo": "custom preprocessor config"}}"#,
330330
)
331331
.env("PATH", path)
332-
.expect_failure()
333332
.expect_stdout(str![[""]])
334333
.expect_stderr(str![[r#"
335-
336-
thread 'main' ([..]) panicked at [..]
337-
unreachable: invalid key `output`
338-
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
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
339338
340339
"#]]);
341-
});
340+
})
341+
// No HTML output
342+
.check_file_list("book", str![[""]]);
342343
}

0 commit comments

Comments
 (0)