use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct Variant {
#[serde(default, deserialize_with = "deserialize_one_or_many")]
pub os: Vec<String>,
#[serde(default)]
pub profile: Option<String>,
#[serde(default)]
pub default: bool,
}
impl Variant {
pub(crate) fn name(&self) -> String {
let mut parts = vec![];
for os in &self.os {
parts.push(os.replace('/', "-"));
}
let mut name = parts.join("-");
if let Some(profile) = &self.profile {
if name.is_empty() {
name = profile.clone();
} else {
name = format!("{name}+{profile}");
}
}
if name.is_empty() {
"default".to_string()
} else {
name
}
}
fn matches(&self, environments: &[String]) -> bool {
let os_ok = self.os.is_empty()
|| self
.os
.iter()
.any(|entry| crate::cli::version::os_selector_matches(entry));
let profile_ok = self
.profile
.as_ref()
.is_none_or(|profile| environments.iter().any(|env| env == profile));
os_ok && profile_ok
}
fn specificity(&self) -> u8 {
let mut score = 0;
if self.profile.is_some() {
score += 2;
}
if !self.os.is_empty() {
score += 1;
if self.os.iter().any(|entry| entry.contains('/')) {
score += 1;
}
}
score
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Selection {
Single,
Variant(Variant),
NoMatch,
Ambiguous(Vec<Variant>),
}
pub(crate) fn validate(variants: &[Variant]) -> eyre::Result<()> {
let mut default_seen = false;
for variant in variants {
if variant.default {
if default_seen || !variant.os.is_empty() || variant.profile.is_some() {
eyre::bail!("variants allow only one default, without os or profile selectors");
}
default_seen = true;
}
}
Ok(())
}
pub(crate) fn select(variants: &[Variant], environments: &[String]) -> Selection {
if variants.is_empty() {
return Selection::Single;
}
let matching: Vec<&Variant> = variants
.iter()
.filter(|variant| !variant.default && variant.matches(environments))
.collect();
let best = matching.iter().map(|variant| variant.specificity()).max();
match best {
Some(best) => {
let winners: Vec<&Variant> = matching
.into_iter()
.filter(|variant| variant.specificity() == best)
.collect();
match winners.as_slice() {
[one] => Selection::Variant((*one).clone()),
many => Selection::Ambiguous(many.iter().map(|v| (*v).clone()).collect()),
}
}
None => match variants.iter().find(|variant| variant.default) {
Some(fallback) => Selection::Variant(fallback.clone()),
None => Selection::NoMatch,
},
}
}
pub(crate) fn active_environments() -> Vec<String> {
crate::env::MISE_ENV_WITH_AUTO.clone()
}
fn deserialize_one_or_many<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum OneOrMany {
One(String),
Many(Vec<String>),
}
Ok(match OneOrMany::deserialize(deserializer)? {
OneOrMany::One(one) => vec![one],
OneOrMany::Many(many) => many,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_must_be_unique_and_unqualified() {
let fallback = Variant {
default: true,
..Variant::default()
};
assert!(validate(std::slice::from_ref(&fallback)).is_ok());
assert!(validate(&[fallback.clone(), fallback.clone()]).is_err());
assert!(
validate(&[Variant {
os: vec!["macos".into()],
..fallback.clone()
}])
.is_err()
);
assert!(
validate(&[Variant {
profile: Some("work".into()),
..fallback
}])
.is_err()
);
}
fn v(os: &[&str], profile: Option<&str>, default: bool) -> Variant {
Variant {
os: os.iter().map(|s| s.to_string()).collect(),
profile: profile.map(str::to_string),
default,
}
}
#[test]
fn selection_prefers_the_most_specific_match() {
let this_os = crate::cli::version::OS.to_string();
let other_os = if this_os == "linux" { "macos" } else { "linux" };
let variants = vec![v(&[&this_os], None, false), v(&[other_os], None, false)];
assert_eq!(
select(&variants, &[]),
Selection::Variant(v(&[&this_os], None, false))
);
let variants = vec![v(&[&this_os], None, false), v(&[], Some("work"), false)];
assert_eq!(
select(&variants, &["work".into()]),
Selection::Variant(v(&[], Some("work"), false))
);
let variants = vec![v(&[other_os], None, false)];
assert_eq!(select(&variants, &[]), Selection::NoMatch);
let variants = vec![v(&[other_os], None, false), v(&[], None, true)];
assert_eq!(
select(&variants, &[]),
Selection::Variant(v(&[], None, true))
);
let variants = vec![v(&[&this_os], None, false), v(&[&this_os], None, false)];
assert!(matches!(select(&variants, &[]), Selection::Ambiguous(_)));
assert_eq!(select(&[], &[]), Selection::Single);
}
#[test]
fn stream_names() {
assert_ne!(
v(&["macos", "linux"], None, false).name(),
v(&["macos", "windows"], None, false).name()
);
assert_eq!(v(&["macos"], None, false).name(), "macos");
assert_eq!(
v(&["linux/arm64"], Some("work"), false).name(),
"linux-arm64+work"
);
assert_eq!(v(&[], Some("work"), false).name(), "work");
assert_eq!(v(&[], None, true).name(), "default");
}
}