Skip to main content

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}