#[non_exhaustive]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CompatibilityMode {
#[default]
Default,
Strict,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExplicitChoice {
Strict,
Default,
}
pub fn resolve(
explicit_flag: Option<ExplicitChoice>,
env_strict: Option<&str>,
argv0_basename: Option<&str>,
) -> CompatibilityMode {
match explicit_flag {
Some(ExplicitChoice::Default) => return CompatibilityMode::Default,
Some(ExplicitChoice::Strict) => return CompatibilityMode::Strict,
None => {}
}
if let Some(value) = env_strict {
if env_var_is_truthy(value) {
return CompatibilityMode::Strict;
}
}
if let Some(name) = argv0_basename {
if name.eq_ignore_ascii_case("ts") {
return CompatibilityMode::Strict;
}
}
CompatibilityMode::Default
}
fn env_var_is_truthy(value: &str) -> bool {
matches!(
value.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
)
}
pub fn argv0_basename(argv0: &std::ffi::OsStr) -> Option<String> {
use std::path::Path;
Path::new(argv0)
.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.to_owned())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn explicit_no_strict_wins_over_everything() {
let mode = resolve(Some(ExplicitChoice::Default), Some("1"), Some("ts"));
assert_eq!(mode, CompatibilityMode::Default);
}
#[test]
fn explicit_strict_wins_over_env_and_argv() {
let mode = resolve(Some(ExplicitChoice::Strict), Some("0"), Some("rusty-ts"));
assert_eq!(mode, CompatibilityMode::Strict);
}
#[test]
fn env_truthy_enables_strict() {
for truthy in ["1", "true", "TRUE", "yes", "Yes", "on", " 1 "] {
assert_eq!(
resolve(None, Some(truthy), Some("rusty-ts")),
CompatibilityMode::Strict,
"env {truthy:?} should enable Strict",
);
}
}
#[test]
fn env_falsy_or_unset_falls_through() {
for falsy in ["0", "false", "no", "off", "", " "] {
assert_eq!(
resolve(None, Some(falsy), Some("rusty-ts")),
CompatibilityMode::Default,
"env {falsy:?} should not enable Strict",
);
}
assert_eq!(
resolve(None, None, Some("rusty-ts")),
CompatibilityMode::Default,
);
}
#[test]
fn argv0_ts_enables_strict() {
assert_eq!(resolve(None, None, Some("ts")), CompatibilityMode::Strict);
}
#[test]
fn argv0_ts_case_insensitive() {
assert_eq!(resolve(None, None, Some("TS")), CompatibilityMode::Strict);
assert_eq!(resolve(None, None, Some("Ts")), CompatibilityMode::Strict);
}
#[test]
fn argv0_rusty_ts_stays_default() {
assert_eq!(
resolve(None, None, Some("rusty-ts")),
CompatibilityMode::Default,
);
}
#[test]
fn argv0_basename_strips_exe() {
use std::ffi::OsStr;
assert_eq!(argv0_basename(OsStr::new("ts.exe")).as_deref(), Some("ts"));
assert_eq!(argv0_basename(OsStr::new("ts")).as_deref(), Some("ts"));
assert_eq!(argv0_basename(OsStr::new("./ts")).as_deref(), Some("ts"),);
}
#[test]
fn argv0_basename_handles_path_components() {
use std::ffi::OsStr;
assert_eq!(
argv0_basename(OsStr::new("/usr/local/bin/ts")).as_deref(),
Some("ts"),
);
}
#[test]
fn precedence_table() {
type Row = (
Option<ExplicitChoice>,
Option<&'static str>,
Option<&'static str>,
CompatibilityMode,
);
let cases: &[Row] = &[
(None, None, None, CompatibilityMode::Default),
(None, None, Some("rusty-ts"), CompatibilityMode::Default),
(None, None, Some("ts"), CompatibilityMode::Strict),
(None, Some("1"), Some("rusty-ts"), CompatibilityMode::Strict),
(None, Some("true"), None, CompatibilityMode::Strict),
(None, Some("0"), Some("ts"), CompatibilityMode::Strict), (
None,
Some("0"),
Some("rusty-ts"),
CompatibilityMode::Default,
),
(
Some(ExplicitChoice::Strict),
Some("0"),
Some("rusty-ts"),
CompatibilityMode::Strict,
),
(
Some(ExplicitChoice::Strict),
None,
Some("ts"),
CompatibilityMode::Strict,
),
(
Some(ExplicitChoice::Default),
Some("1"),
Some("ts"),
CompatibilityMode::Default,
),
(
Some(ExplicitChoice::Default),
None,
Some("ts"),
CompatibilityMode::Default,
),
];
for (i, (explicit, env, argv, expected)) in cases.iter().enumerate() {
let actual = resolve(*explicit, *env, *argv);
assert_eq!(
actual, *expected,
"case {i}: explicit={explicit:?} env={env:?} argv={argv:?} expected {expected:?} got {actual:?}",
);
}
}
}