use std::collections::BTreeSet;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Features {
names: BTreeSet<String>,
}
pub const BUILTINS: [(&str, Option<&str>); 10] = [
("string", None),
("number", None),
("integer", None),
("offset", None),
("percent", Some("fn-number")),
("currency", Some("fn-number")),
("unit", Some("fn-number")),
("datetime", Some("fn-datetime")),
("date", Some("fn-datetime")),
("time", Some("fn-datetime")),
];
pub const OPTIONS: [(&str, &[&str]); 10] = [
("string", &[]),
(
"number",
&[
"select",
"signDisplay",
"useGrouping",
"minimumIntegerDigits",
"minimumFractionDigits",
"maximumFractionDigits",
"minimumSignificantDigits",
"maximumSignificantDigits",
"trailingZeroDisplay",
"roundingPriority",
"roundingIncrement",
"roundingMode",
],
),
(
"integer",
&[
"select",
"signDisplay",
"useGrouping",
"minimumIntegerDigits",
"maximumSignificantDigits",
],
),
("offset", &["add", "subtract"]),
(
"percent",
&[
"signDisplay",
"useGrouping",
"minimumFractionDigits",
"maximumFractionDigits",
"minimumSignificantDigits",
"maximumSignificantDigits",
"trailingZeroDisplay",
"roundingPriority",
"roundingMode",
],
),
(
"currency",
&[
"currency",
"currencyDisplay",
"currencySign",
"fractionDigits",
"useGrouping",
"minimumIntegerDigits",
"minimumSignificantDigits",
"maximumSignificantDigits",
"trailingZeroDisplay",
"roundingPriority",
"roundingIncrement",
"roundingMode",
],
),
(
"unit",
&[
"unit",
"unitDisplay",
"usage",
"signDisplay",
"useGrouping",
"minimumIntegerDigits",
"minimumFractionDigits",
"maximumFractionDigits",
"minimumSignificantDigits",
"maximumSignificantDigits",
"roundingPriority",
"roundingIncrement",
"roundingMode",
],
),
(
"datetime",
&[
"dateFields",
"dateLength",
"timePrecision",
"timeZoneStyle",
"timeZone",
"hour12",
"calendar",
],
),
("date", &["fields", "length", "timeZone", "calendar"]),
(
"time",
&[
"precision",
"timeZoneStyle",
"timeZone",
"hour12",
"calendar",
],
),
];
pub fn defines_option(function: &str, option: &str) -> Option<bool> {
if option.contains(':') {
return None;
}
let (_, names) = OPTIONS.iter().find(|(name, _)| *name == function)?;
Some(names.contains(&option))
}
pub const CATALOG_FEATURES: [&str; 3] = ["fn-number", "fn-datetime", "datetime-icu"];
impl Features {
pub fn from_env() -> Features {
Features::from_vars(std::env::vars())
}
pub fn from_vars(vars: impl IntoIterator<Item = (String, String)>) -> Features {
let names = vars
.into_iter()
.filter_map(|(key, _)| {
key.strip_prefix("CARGO_FEATURE_")
.map(|f| f.to_ascii_lowercase().replace('_', "-"))
})
.collect();
Features { names }
}
pub fn parse(list: &str) -> Features {
let names = list
.split([',', ' '])
.map(str::trim)
.filter(|f| !f.is_empty())
.map(|f| f.rsplit_once('/').map_or(f, |(_, name)| name).to_owned())
.collect();
Features { names }
}
pub fn from_names<S: Into<String>>(names: impl IntoIterator<Item = S>) -> Features {
Features {
names: names.into_iter().map(Into::into).collect(),
}
}
#[must_use]
pub fn for_catalogs(&self) -> Features {
Features::from_names(CATALOG_FEATURES.into_iter().filter(|f| self.has(f)))
}
pub fn has(&self, name: &str) -> bool {
self.names.contains(name)
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.names.iter().map(String::as_str)
}
pub fn fn_number(&self) -> bool {
self.has("fn-number")
}
pub fn fn_datetime(&self) -> bool {
self.has("fn-datetime")
}
pub fn datetime_icu(&self) -> bool {
self.has("datetime-icu")
}
pub fn datetime_intl(&self) -> bool {
self.has("datetime-intl")
}
pub fn intl(&self) -> bool {
self.has("intl")
}
pub fn provides(&self, identifier: &str) -> Option<bool> {
let (_, gate) = BUILTINS.iter().find(|(name, _)| *name == identifier)?;
Some(match gate {
None => true,
Some(feature) => self.has(feature),
})
}
pub fn missing_feature(&self, identifier: &str) -> Option<&'static str> {
let (_, gate) = BUILTINS.iter().find(|(name, _)| *name == identifier)?;
gate.filter(|feature| !self.has(feature))
}
}
#[cfg(test)]
mod tests {
use super::Features;
#[test]
fn cargo_feature_variables_become_feature_names() {
let features = Features::from_vars([
("CARGO_FEATURE_FN_NUMBER".to_owned(), "1".to_owned()),
("CARGO_FEATURE_SSR".to_owned(), "1".to_owned()),
("PATH".to_owned(), "/usr/bin".to_owned()),
]);
assert!(features.fn_number());
assert!(features.has("ssr"));
assert!(!features.fn_datetime());
assert_eq!(features.names().collect::<Vec<_>>(), ["fn-number", "ssr"]);
}
#[test]
fn a_features_argument_reads_like_cargos() {
let features = Features::parse("fn-number, mf2/fn-datetime datetime-icu");
assert!(features.fn_number());
assert!(features.fn_datetime());
assert!(features.datetime_icu());
}
#[test]
fn only_some_features_change_a_catalog() {
let features = Features::parse("default,ssr,intl,datetime-intl,fn-datetime,fn-number");
assert_eq!(
features.for_catalogs().names().collect::<Vec<_>>(),
["fn-datetime", "fn-number"]
);
}
#[test]
fn gated_functions_need_their_feature() {
let none = Features::default();
assert_eq!(none.provides("number"), Some(true));
assert_eq!(none.provides("percent"), Some(false));
assert_eq!(none.provides("date"), Some(false));
assert_eq!(none.provides("app:emoji"), None);
assert_eq!(none.missing_feature("currency"), Some("fn-number"));
assert_eq!(none.missing_feature("number"), None);
let both = Features::parse("fn-number,fn-datetime");
assert_eq!(both.provides("percent"), Some(true));
assert_eq!(both.provides("time"), Some(true));
assert_eq!(both.missing_feature("unit"), None);
}
}