1use std::collections::BTreeSet;
11
12#[derive(Clone, Debug, Default, PartialEq, Eq)]
15pub struct Features {
16 names: BTreeSet<String>,
17}
18
19pub const BUILTINS: [(&str, Option<&str>); 10] = [
25 ("string", None),
26 ("number", None),
27 ("integer", None),
28 ("offset", None),
29 ("percent", Some("fn-number")),
30 ("currency", Some("fn-number")),
31 ("unit", Some("fn-number")),
32 ("datetime", Some("fn-datetime")),
33 ("date", Some("fn-datetime")),
34 ("time", Some("fn-datetime")),
35];
36
37pub const OPTIONS: [(&str, &[&str]); 10] = [
48 ("string", &[]),
49 (
50 "number",
51 &[
52 "select",
53 "signDisplay",
54 "useGrouping",
55 "minimumIntegerDigits",
56 "minimumFractionDigits",
57 "maximumFractionDigits",
58 "minimumSignificantDigits",
59 "maximumSignificantDigits",
60 "trailingZeroDisplay",
61 "roundingPriority",
62 "roundingIncrement",
63 "roundingMode",
64 ],
65 ),
66 (
67 "integer",
68 &[
69 "select",
70 "signDisplay",
71 "useGrouping",
72 "minimumIntegerDigits",
73 "maximumSignificantDigits",
74 ],
75 ),
76 ("offset", &["add", "subtract"]),
77 (
78 "percent",
79 &[
80 "signDisplay",
81 "useGrouping",
82 "minimumFractionDigits",
83 "maximumFractionDigits",
84 "minimumSignificantDigits",
85 "maximumSignificantDigits",
86 "trailingZeroDisplay",
87 "roundingPriority",
88 "roundingMode",
89 ],
90 ),
91 (
92 "currency",
93 &[
94 "currency",
95 "currencyDisplay",
96 "currencySign",
97 "fractionDigits",
98 "useGrouping",
99 "minimumIntegerDigits",
100 "minimumSignificantDigits",
101 "maximumSignificantDigits",
102 "trailingZeroDisplay",
103 "roundingPriority",
104 "roundingIncrement",
105 "roundingMode",
106 ],
107 ),
108 (
109 "unit",
110 &[
111 "unit",
112 "unitDisplay",
113 "usage",
114 "signDisplay",
115 "useGrouping",
116 "minimumIntegerDigits",
117 "minimumFractionDigits",
118 "maximumFractionDigits",
119 "minimumSignificantDigits",
120 "maximumSignificantDigits",
121 "roundingPriority",
122 "roundingIncrement",
123 "roundingMode",
124 ],
125 ),
126 (
127 "datetime",
128 &[
129 "dateFields",
130 "dateLength",
131 "timePrecision",
132 "timeZoneStyle",
133 "timeZone",
134 "hour12",
135 "calendar",
136 ],
137 ),
138 ("date", &["fields", "length", "timeZone", "calendar"]),
139 (
140 "time",
141 &[
142 "precision",
143 "timeZoneStyle",
144 "timeZone",
145 "hour12",
146 "calendar",
147 ],
148 ),
149];
150
151pub fn defines_option(function: &str, option: &str) -> Option<bool> {
154 if option.contains(':') {
155 return None;
156 }
157 let (_, names) = OPTIONS.iter().find(|(name, _)| *name == function)?;
158 Some(names.contains(&option))
159}
160
161pub const CATALOG_FEATURES: [&str; 3] = ["fn-number", "fn-datetime", "datetime-icu"];
166
167impl Features {
168 pub fn from_env() -> Features {
175 Features::from_vars(std::env::vars())
176 }
177
178 pub fn from_vars(vars: impl IntoIterator<Item = (String, String)>) -> Features {
180 let names = vars
181 .into_iter()
182 .filter_map(|(key, _)| {
183 key.strip_prefix("CARGO_FEATURE_")
184 .map(|f| f.to_ascii_lowercase().replace('_', "-"))
185 })
186 .collect();
187 Features { names }
188 }
189
190 pub fn parse(list: &str) -> Features {
193 let names = list
194 .split([',', ' '])
195 .map(str::trim)
196 .filter(|f| !f.is_empty())
197 .map(|f| f.rsplit_once('/').map_or(f, |(_, name)| name).to_owned())
198 .collect();
199 Features { names }
200 }
201
202 pub fn from_names<S: Into<String>>(names: impl IntoIterator<Item = S>) -> Features {
204 Features {
205 names: names.into_iter().map(Into::into).collect(),
206 }
207 }
208
209 #[must_use]
212 pub fn for_catalogs(&self) -> Features {
213 Features::from_names(CATALOG_FEATURES.into_iter().filter(|f| self.has(f)))
214 }
215
216 pub fn has(&self, name: &str) -> bool {
218 self.names.contains(name)
219 }
220
221 pub fn names(&self) -> impl Iterator<Item = &str> {
223 self.names.iter().map(String::as_str)
224 }
225
226 pub fn fn_number(&self) -> bool {
229 self.has("fn-number")
230 }
231
232 pub fn fn_datetime(&self) -> bool {
234 self.has("fn-datetime")
235 }
236
237 pub fn datetime_icu(&self) -> bool {
239 self.has("datetime-icu")
240 }
241
242 pub fn datetime_intl(&self) -> bool {
244 self.has("datetime-intl")
245 }
246
247 pub fn intl(&self) -> bool {
251 self.has("intl")
252 }
253
254 pub fn provides(&self, identifier: &str) -> Option<bool> {
258 let (_, gate) = BUILTINS.iter().find(|(name, _)| *name == identifier)?;
259 Some(match gate {
260 None => true,
261 Some(feature) => self.has(feature),
262 })
263 }
264
265 pub fn missing_feature(&self, identifier: &str) -> Option<&'static str> {
267 let (_, gate) = BUILTINS.iter().find(|(name, _)| *name == identifier)?;
268 gate.filter(|feature| !self.has(feature))
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::Features;
275
276 #[test]
277 fn cargo_feature_variables_become_feature_names() {
278 let features = Features::from_vars([
279 ("CARGO_FEATURE_FN_NUMBER".to_owned(), "1".to_owned()),
280 ("CARGO_FEATURE_SSR".to_owned(), "1".to_owned()),
281 ("PATH".to_owned(), "/usr/bin".to_owned()),
282 ]);
283 assert!(features.fn_number());
284 assert!(features.has("ssr"));
285 assert!(!features.fn_datetime());
286 assert_eq!(features.names().collect::<Vec<_>>(), ["fn-number", "ssr"]);
287 }
288
289 #[test]
290 fn a_features_argument_reads_like_cargos() {
291 let features = Features::parse("fn-number, mf2/fn-datetime datetime-icu");
292 assert!(features.fn_number());
293 assert!(features.fn_datetime());
294 assert!(features.datetime_icu());
295 }
296
297 #[test]
298 fn only_some_features_change_a_catalog() {
299 let features = Features::parse("default,ssr,intl,datetime-intl,fn-datetime,fn-number");
300 assert_eq!(
301 features.for_catalogs().names().collect::<Vec<_>>(),
302 ["fn-datetime", "fn-number"]
303 );
304 }
305
306 #[test]
307 fn gated_functions_need_their_feature() {
308 let none = Features::default();
309 assert_eq!(none.provides("number"), Some(true));
310 assert_eq!(none.provides("percent"), Some(false));
311 assert_eq!(none.provides("date"), Some(false));
312 assert_eq!(none.provides("app:emoji"), None);
313 assert_eq!(none.missing_feature("currency"), Some("fn-number"));
314 assert_eq!(none.missing_feature("number"), None);
315
316 let both = Features::parse("fn-number,fn-datetime");
317 assert_eq!(both.provides("percent"), Some(true));
318 assert_eq!(both.provides("time"), Some(true));
319 assert_eq!(both.missing_feature("unit"), None);
320 }
321}