dynamo_truthy/lib.rs
1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Canonical truthy/falsy flag parsing for Dynamo.
5//!
6//! Single owner of the boolean vocabulary accepted from user-supplied
7//! configuration (environment variables, HTTP headers, config values):
8//! truthy = `1 | true | on | yes`, falsy = `0 | false | off | no` or empty,
9//! case-insensitive, surrounding whitespace ignored.
10//!
11//! `dynamo_runtime::config` re-exports these helpers and is the canonical
12//! import path for crates that already depend on `dynamo-runtime`. Crates that
13//! cannot (e.g. `dynamo-memory`, `dynamo-kv-router`, `dynamo-mocker`) depend on
14//! this crate directly. Do not hand-roll new bool parsers — a divergent
15//! accepted set means `SOMEFLAG=on` works for one flag and silently not
16//! another. `tests/no_bool_parse_forks.rs` greps the workspace for forks.
17
18/// Check if a string is truthy: `1 | true | on | yes`, case-insensitive,
19/// surrounding whitespace ignored. Everything else is not truthy.
20pub fn is_truthy(val: &str) -> bool {
21 matches!(
22 val.trim().to_lowercase().as_str(),
23 "1" | "true" | "on" | "yes"
24 )
25}
26
27/// Check if a string is falsey: `0 | false | off | no` or empty,
28/// case-insensitive, surrounding whitespace ignored.
29pub fn is_falsey(val: &str) -> bool {
30 matches!(
31 val.trim().to_lowercase().as_str(),
32 "" | "0" | "false" | "off" | "no"
33 )
34}
35
36/// Parse a string as a boolean value, returning an error if it is neither
37/// truthy nor falsey.
38///
39/// # Returns
40/// * `Ok(true)` - for truthy values ([`is_truthy`])
41/// * `Ok(false)` - for falsey values ([`is_falsey`]), including empty
42/// * `Err(_)` - for any other value
43pub fn parse_bool(val: &str) -> anyhow::Result<bool> {
44 if is_truthy(val) {
45 Ok(true)
46 } else if is_falsey(val) {
47 Ok(false)
48 } else {
49 anyhow::bail!(
50 "Invalid boolean value: '{}'. Expected one of: true/false, 1/0, on/off, yes/no",
51 val
52 )
53 }
54}
55
56/// Tri-state parse for call sites that preserve their own default unless the
57/// value is a deliberate boolean choice.
58///
59/// Unlike [`parse_bool`], an empty (or whitespace-only) value yields `None` —
60/// a variable declared without a value (common in Kubernetes manifests and
61/// Docker Compose files) must not override a default — and so does any
62/// unrecognized value.
63///
64/// # Returns
65/// * `Some(true)` - for truthy values ([`is_truthy`])
66/// * `Some(false)` - for `0 | false | off | no`
67/// * `None` - for empty or unrecognized values
68pub fn parse_bool_opt(val: &str) -> Option<bool> {
69 if is_truthy(val) {
70 Some(true)
71 } else if val.trim().is_empty() {
72 None
73 } else if is_falsey(val) {
74 Some(false)
75 } else {
76 None
77 }
78}
79
80/// Check if an environment variable is set to a truthy value.
81/// Unset (or non-unicode) variables are not truthy.
82pub fn env_is_truthy(env: &str) -> bool {
83 match std::env::var(env) {
84 Ok(val) => is_truthy(val.as_str()),
85 Err(_) => false,
86 }
87}
88
89/// Check if an environment variable is set to a falsey value (including set
90/// but empty). Unset (or non-unicode) variables are not falsey.
91pub fn env_is_falsey(env: &str) -> bool {
92 match std::env::var(env) {
93 Ok(val) => is_falsey(val.as_str()),
94 Err(_) => false,
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 const TRUTHY: &[&str] = &[
103 "1", "true", "TRUE", "True", "on", "ON", "On", "yes", "YES", "Yes", " true ", "\tyes\n",
104 ];
105 const FALSEY: &[&str] = &[
106 "0", "false", "FALSE", "False", "off", "OFF", "Off", "no", "NO", "No", "", " ", " false ",
107 ];
108 const NEITHER: &[&str] = &[
109 "2", "enabled", "disabled", "maybe", "y", "n", "t", "f", "-1",
110 ];
111
112 #[test]
113 fn truthy_spellings() {
114 for val in TRUTHY {
115 assert!(is_truthy(val), "value={val:?}");
116 assert!(!is_falsey(val), "value={val:?}");
117 assert!(parse_bool(val).unwrap(), "value={val:?}");
118 }
119 }
120
121 #[test]
122 fn falsey_spellings() {
123 for val in FALSEY {
124 assert!(is_falsey(val), "value={val:?}");
125 assert!(!is_truthy(val), "value={val:?}");
126 assert!(!parse_bool(val).unwrap(), "value={val:?}");
127 }
128 }
129
130 #[test]
131 fn invalid_spellings() {
132 for val in NEITHER {
133 assert!(!is_truthy(val), "value={val:?}");
134 assert!(!is_falsey(val), "value={val:?}");
135 assert!(parse_bool(val).is_err(), "value={val:?}");
136 }
137 }
138
139 #[test]
140 fn parse_bool_opt_spellings() {
141 for val in TRUTHY {
142 assert_eq!(parse_bool_opt(val), Some(true), "value={val:?}");
143 }
144 for val in FALSEY {
145 let expected = if val.trim().is_empty() {
146 None // declared-but-empty is not a deliberate choice
147 } else {
148 Some(false)
149 };
150 assert_eq!(parse_bool_opt(val), expected, "value={val:?}");
151 }
152 for val in NEITHER {
153 assert_eq!(parse_bool_opt(val), None, "value={val:?}");
154 }
155 }
156
157 #[test]
158 fn env_helpers() {
159 // Each test uses its own variable name: tests run concurrently and
160 // the process environment is shared.
161 const UNSET: &str = "DYN_TRUTHY_TEST_UNSET";
162 assert!(!env_is_truthy(UNSET));
163 assert!(!env_is_falsey(UNSET));
164
165 const SET: &str = "DYN_TRUTHY_TEST_SET";
166 for (val, truthy, falsey) in [("on", true, false), ("off", false, true), ("", false, true)]
167 {
168 // SAFETY: single-threaded with respect to this variable name.
169 unsafe { std::env::set_var(SET, val) };
170 assert_eq!(env_is_truthy(SET), truthy, "value={val:?}");
171 assert_eq!(env_is_falsey(SET), falsey, "value={val:?}");
172 }
173 }
174}