Skip to main content

writerslogic_warm/
compatibility.rs

1// Copyright 2024-2026 WritersLogic Contributors
2// SPDX-License-Identifier: Apache-2.0
3
4use std::env::VarError;
5use std::ffi::OsString;
6
7/// Reads a WARM setting, falling back to its HMS name only when the new name is absent.
8pub fn var_os(name: &str) -> Option<OsString> {
9    std::env::var_os(name).or_else(|| {
10        name.strip_prefix("WARM_")
11            .and_then(|suffix| std::env::var_os(format!("HMS_{suffix}")))
12    })
13}
14
15pub fn var(name: &str) -> Result<String, VarError> {
16    var_os(name)
17        .ok_or(VarError::NotPresent)?
18        .into_string()
19        .map_err(VarError::NotUnicode)
20}
21
22/// Selects the environment name before clap captures its value, without changing the process.
23pub fn env_name(current: &'static str, legacy: &'static str) -> &'static str {
24    if std::env::var_os(current).is_none() && std::env::var_os(legacy).is_some() {
25        legacy
26    } else {
27        current
28    }
29}
30
31#[cfg(test)]
32mod tests {
33    use super::*;
34
35    const CURRENT: &str = "WARM_COMPATIBILITY_TEST_VALUE";
36    const LEGACY: &str = "HMS_COMPATIBILITY_TEST_VALUE";
37
38    #[test]
39    fn environment_precedence() {
40        for case in ["absent", "legacy", "current", "both", "empty", "unicode"] {
41            let mut child = std::process::Command::new(std::env::current_exe().unwrap());
42            child
43                .args(["--exact", "compatibility::tests::environment_probe"])
44                .env("WARM_COMPATIBILITY_TEST_CASE", case)
45                .env_remove(CURRENT)
46                .env_remove(LEGACY);
47            match case {
48                "legacy" => {
49                    child.env(LEGACY, "17");
50                }
51                "current" => {
52                    child.env(CURRENT, "23");
53                }
54                "both" => {
55                    child.env(LEGACY, "17").env(CURRENT, "23");
56                }
57                "empty" => {
58                    child.env(LEGACY, "17").env(CURRENT, "");
59                }
60                "unicode" => {
61                    child.env(LEGACY, "17").env(CURRENT, "記憶");
62                }
63                _ => {}
64            }
65            assert!(child.status().unwrap().success(), "case {case}");
66        }
67        #[cfg(unix)]
68        {
69            use std::os::unix::ffi::OsStringExt;
70            let status = std::process::Command::new(std::env::current_exe().unwrap())
71                .args(["--exact", "compatibility::tests::environment_probe"])
72                .env("WARM_COMPATIBILITY_TEST_CASE", "nonunicode")
73                .env(LEGACY, "17")
74                .env(CURRENT, OsString::from_vec(vec![0xff]))
75                .status()
76                .unwrap();
77            assert!(status.success());
78        }
79    }
80
81    #[test]
82    fn environment_probe() {
83        let Ok(case) = std::env::var("WARM_COMPATIBILITY_TEST_CASE") else {
84            return;
85        };
86        if case == "nonunicode" {
87            assert!(matches!(var(CURRENT), Err(VarError::NotUnicode(_))));
88            assert_eq!(env_name(CURRENT, LEGACY), CURRENT);
89            return;
90        }
91        let expected = match case.as_str() {
92            "absent" => None,
93            "legacy" => Some("17"),
94            "current" | "both" => Some("23"),
95            "empty" => Some(""),
96            "unicode" => Some("記憶"),
97            _ => panic!("unknown test case"),
98        };
99        assert_eq!(var(CURRENT).ok().as_deref(), expected);
100        assert_eq!(
101            env_name(CURRENT, LEGACY),
102            if case == "legacy" { LEGACY } else { CURRENT }
103        );
104        #[cfg(feature = "cli")]
105        {
106            use clap::Parser;
107            #[derive(Parser)]
108            struct Settings {
109                #[arg(long, env = env_name(CURRENT, LEGACY))]
110                value: Option<String>,
111            }
112            assert_eq!(
113                Settings::try_parse_from(["test"]).unwrap().value.as_deref(),
114                expected
115            );
116            assert_eq!(
117                Settings::try_parse_from(["test", "--value", "31"])
118                    .unwrap()
119                    .value
120                    .as_deref(),
121                Some("31")
122            );
123        }
124    }
125}