socket_patch_core/utils/env_compat.rs
1//! Legacy → new env-var compatibility shim.
2//!
3//! The v3.0 CLI surface migrated three env vars from the `SOCKET_PATCH_*`
4//! prefix to the unified `SOCKET_*` prefix:
5//!
6//! | New | Legacy |
7//! |------------------------------|-------------------------------------|
8//! | `SOCKET_PROXY_URL` | `SOCKET_PATCH_PROXY_URL` |
9//! | `SOCKET_DEBUG` | `SOCKET_PATCH_DEBUG` |
10//! | `SOCKET_TELEMETRY_DISABLED` | `SOCKET_PATCH_TELEMETRY_DISABLED` |
11//!
12//! `read_env_with_legacy` reads the new name; if absent, it falls back to the
13//! legacy name and prints a one-shot deprecation warning to stderr. The
14//! warning fires **unconditionally** — even under `--silent` / `--json` — so
15//! users see the transition signal in scripts and CI logs. The legacy names
16//! will be removed in the next major release.
17
18use std::collections::HashSet;
19use std::sync::Mutex;
20
21use once_cell::sync::Lazy;
22
23/// Names of legacy env vars that have already warned in this process. Used
24/// so each legacy var warns at most once per invocation, even when read
25/// from multiple call sites.
26static WARNED: Lazy<Mutex<HashSet<&'static str>>> = Lazy::new(|| Mutex::new(HashSet::new()));
27
28/// Read the new-style env var `new_name`. If absent, fall back to
29/// `legacy_name` and print a one-shot deprecation warning to stderr (the
30/// warning fires regardless of CLI verbosity flags so users notice the
31/// transition).
32///
33/// Returns `None` when neither name is set (or both are set to an empty
34/// string, matching the prior call sites' filtering).
35pub fn read_env_with_legacy(new_name: &'static str, legacy_name: &'static str) -> Option<String> {
36 if let Ok(v) = std::env::var(new_name) {
37 if !v.is_empty() {
38 return Some(v);
39 }
40 }
41 match std::env::var(legacy_name) {
42 Ok(v) if !v.is_empty() => {
43 warn_legacy_once(legacy_name, new_name);
44 Some(v)
45 }
46 _ => None,
47 }
48}
49
50/// Print a one-shot deprecation warning. Public so callers that read the
51/// legacy name through other code paths (e.g. clap's `env =` attribute,
52/// which reads only the new name) can still surface the deprecation when
53/// they detect the legacy name was set.
54pub fn warn_legacy_once(legacy_name: &'static str, new_name: &'static str) {
55 let mut warned = match WARNED.lock() {
56 Ok(g) => g,
57 Err(poisoned) => poisoned.into_inner(),
58 };
59 if warned.insert(legacy_name) {
60 eprintln!(
61 "[socket-patch] warning: env var `{legacy_name}` is deprecated; \
62 use `{new_name}` instead. The legacy name will be removed in a \
63 future major release."
64 );
65 }
66}
67
68/// Read the new env var; if it isn't set, also probe the legacy name and
69/// surface a deprecation warning when the legacy name is set. Returns the
70/// Renamed env vars whose legacy `SOCKET_PATCH_*` names are still honored.
71///
72/// First entry of each tuple is the new name (what clap and current code
73/// read); second is the legacy name that gets a deprecation warning.
74pub const LEGACY_ENV_RENAMES: &[(&str, &str)] = &[
75 ("SOCKET_PROXY_URL", "SOCKET_PATCH_PROXY_URL"),
76 ("SOCKET_DEBUG", "SOCKET_PATCH_DEBUG"),
77 ("SOCKET_TELEMETRY_DISABLED", "SOCKET_PATCH_TELEMETRY_DISABLED"),
78];
79
80/// Promote legacy `SOCKET_PATCH_*` env vars to their new `SOCKET_*` names
81/// in-process. When the new name is unset and the legacy name is set, copy
82/// the value over and emit a one-shot deprecation warning to stderr.
83///
84/// Call this *once*, very early in `main`, before clap parses. After
85/// promotion every downstream reader (clap `env =`, core code) only needs
86/// to know the new name.
87///
88/// The warning fires unconditionally — even under `--silent` / `--json`
89/// — so the transition signal isn't swallowed in CI logs.
90pub fn promote_legacy_env_vars() {
91 for (new_name, legacy_name) in LEGACY_ENV_RENAMES {
92 let new_already_set = std::env::var(new_name)
93 .ok()
94 .filter(|v| !v.is_empty())
95 .is_some();
96 if new_already_set {
97 continue;
98 }
99 if let Ok(value) = std::env::var(legacy_name) {
100 if !value.is_empty() {
101 warn_legacy_once(legacy_name, new_name);
102 std::env::set_var(new_name, value);
103 }
104 }
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 /// The warning bookkeeping is process-global, so any test that flips a
113 /// real env var would race with parallel tests. Exercise the dedup
114 /// path directly instead.
115 #[test]
116 fn warn_legacy_once_fires_only_once_per_name() {
117 let name = "SOCKET_TEST_LEGACY_ONCE_PATCH";
118 let new = "SOCKET_TEST_LEGACY_ONCE";
119 warn_legacy_once(name, new);
120 warn_legacy_once(name, new);
121 let warned = WARNED.lock().unwrap();
122 assert!(warned.contains(name));
123 }
124}