docling_core/env.rs
1//! Environment-variable helpers shared by every crate in the workspace.
2//!
3//! The knobs documented in the README (`DOCLING_RS_*`, `DOCLING_*`) grew one
4//! hand-rolled `std::env::var` dance per call site — presence checks that
5//! treated `FOO=0` as *on*, three copies of the same truthiness predicate,
6//! a dozen `.ok().and_then(|v| v.parse().ok())` chains. This module is the
7//! single vocabulary for all of them:
8//!
9//! - [`flag`] — boolean knobs (`DOCLING_RS_FP32=1`);
10//! - [`nonempty`] — string knobs where a blank value means "unset";
11//! - [`parse`] — numeric knobs with a coded default;
12//! - [`debug_enabled`] / [`crate::debug_log!`] — the `DOCLING_RS_DEBUG`
13//! diagnostics channel.
14//!
15//! On targets without an environment (wasm32-unknown-unknown) `std::env::var`
16//! reports "not present", so every helper falls back to its default — the
17//! wasm builds keep compiling with no cfg noise at the call sites.
18
19/// True when `key` is set to a truthy value. Truthy is anything except the
20/// explicit "off" spellings — empty, `0`, `false`, `no`, `off` (trimmed,
21/// ASCII case-insensitive) — so both `FOO=1` and `FOO=yes` enable, and
22/// `FOO=0` actually disables instead of counting as "present, therefore on"
23/// (the trap the old `env::var(..).is_ok()` checks all shared).
24pub fn flag(key: &str) -> bool {
25 match std::env::var(key) {
26 Ok(v) => {
27 let v = v.trim();
28 !(v.is_empty()
29 || v == "0"
30 || v.eq_ignore_ascii_case("false")
31 || v.eq_ignore_ascii_case("no")
32 || v.eq_ignore_ascii_case("off"))
33 }
34 Err(_) => false,
35 }
36}
37
38/// The trimmed value of `key`, if set and non-blank. The `Option` shape makes
39/// "env override, else default" read as `nonempty(K).unwrap_or_else(..)` and
40/// composes with `.or_else` chains for multi-variable fallbacks.
41pub fn nonempty(key: &str) -> Option<String> {
42 match std::env::var(key) {
43 Ok(v) => {
44 let v = v.trim();
45 (!v.is_empty()).then(|| v.to_string())
46 }
47 Err(_) => None,
48 }
49}
50
51/// `key` parsed as `T`, if set and parseable. Unparseable values fall back to
52/// the coded default silently — tuning knobs degrade, they don't error.
53pub fn parse<T: std::str::FromStr>(key: &str) -> Option<T> {
54 std::env::var(key).ok().and_then(|v| v.trim().parse().ok())
55}
56
57/// Whether `DOCLING_RS_DEBUG` diagnostics are on. Cached on first use: the
58/// callers sit inside per-page pipeline loops, and a process does not
59/// meaningfully flip its own debug env mid-run.
60pub fn debug_enabled() -> bool {
61 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
62 *ON.get_or_init(|| flag("DOCLING_RS_DEBUG"))
63}
64
65/// `eprintln!` gated on [`env::debug_enabled`](debug_enabled) — the quiet
66/// diagnostics channel (`DOCLING_RS_DEBUG=1`). Callers keep their own
67/// `docling-<crate>:` message prefixes; the macro only owns the gate.
68#[macro_export]
69macro_rules! debug_log {
70 ($($arg:tt)*) => {
71 if $crate::env::debug_enabled() {
72 eprintln!($($arg)*);
73 }
74 };
75}
76
77#[cfg(test)]
78mod tests {
79 // Env mutation is process-global and the test harness is parallel, so
80 // every test owns uniquely-named variables and nothing else reads them.
81 use super::*;
82
83 #[test]
84 fn flag_spellings() {
85 for on in ["1", "true", "yes", "on", "anything", " 1 ", "TRUE"] {
86 std::env::set_var("DOCLING_TEST_FLAG_ON", on);
87 assert!(flag("DOCLING_TEST_FLAG_ON"), "{on:?} should enable");
88 }
89 for off in ["", "0", "false", "no", "off", " OFF ", "No"] {
90 std::env::set_var("DOCLING_TEST_FLAG_OFF", off);
91 assert!(!flag("DOCLING_TEST_FLAG_OFF"), "{off:?} should disable");
92 }
93 assert!(!flag("DOCLING_TEST_FLAG_UNSET"));
94 }
95
96 #[test]
97 fn nonempty_trims_and_drops_blank() {
98 std::env::set_var("DOCLING_TEST_NONEMPTY", " x ");
99 assert_eq!(nonempty("DOCLING_TEST_NONEMPTY").as_deref(), Some("x"));
100 std::env::set_var("DOCLING_TEST_NONEMPTY_BLANK", " ");
101 assert_eq!(nonempty("DOCLING_TEST_NONEMPTY_BLANK"), None);
102 assert_eq!(nonempty("DOCLING_TEST_NONEMPTY_UNSET"), None);
103 }
104
105 #[test]
106 fn parse_trims_and_ignores_garbage() {
107 std::env::set_var("DOCLING_TEST_PARSE", " 42 ");
108 assert_eq!(parse::<usize>("DOCLING_TEST_PARSE"), Some(42));
109 std::env::set_var("DOCLING_TEST_PARSE_BAD", "many");
110 assert_eq!(parse::<usize>("DOCLING_TEST_PARSE_BAD"), None);
111 assert_eq!(parse::<usize>("DOCLING_TEST_PARSE_UNSET"), None);
112 }
113}