preset_env_base/
query.rs

1//! Module for `browserslist` queries.
2#![deny(clippy::all)]
3
4use std::{collections::HashMap, sync::Arc};
5
6use anyhow::{Context, Error};
7use dashmap::DashMap;
8use from_variant::FromVariant;
9use once_cell::sync::Lazy;
10use rustc_hash::FxBuildHasher;
11use serde::Deserialize;
12
13use crate::{version::Version, BrowserData, Versions};
14
15#[derive(Debug, Clone, Deserialize, FromVariant)]
16#[serde(untagged)]
17#[allow(clippy::large_enum_variant)]
18pub enum Targets {
19    Query(Query),
20    EsModules(EsModules),
21    Versions(Versions),
22    HashMap(HashMap<String, QueryOrVersion, FxBuildHasher>),
23}
24
25#[derive(Debug, Clone, Copy, Deserialize)]
26pub struct EsModules {
27    #[allow(dead_code)]
28    esmodules: bool,
29}
30
31#[derive(Debug, Clone, Deserialize, FromVariant)]
32#[serde(untagged)]
33pub enum QueryOrVersion {
34    Query(Query),
35    Version(Version),
36}
37
38#[derive(Debug, Clone, Deserialize, FromVariant, Eq, PartialEq, PartialOrd, Ord, Hash)]
39#[serde(untagged)]
40pub enum Query {
41    Single(String),
42    Multiple(Vec<String>),
43}
44
45type QueryResult = Result<Arc<Versions>, Error>;
46
47impl Query {
48    fn exec(&self) -> QueryResult {
49        fn query<T>(s: &[T]) -> QueryResult
50        where
51            T: AsRef<str>,
52        {
53            let distribs = browserslist::resolve(
54                s,
55                &browserslist::Opts {
56                    mobile_to_desktop: true,
57                    ignore_unknown_versions: true,
58                    ..Default::default()
59                },
60            )
61            .with_context(|| {
62                format!(
63                    "failed to resolve browserslist query: {:?}",
64                    s.iter().map(|v| v.as_ref()).collect::<Vec<_>>()
65                )
66            })?;
67
68            let versions =
69                BrowserData::parse_versions(distribs).expect("failed to parse browser version");
70
71            Ok(Arc::new(versions))
72        }
73
74        static CACHE: Lazy<DashMap<Query, Arc<Versions>, FxBuildHasher>> =
75            Lazy::new(Default::default);
76
77        if let Some(v) = CACHE.get(self) {
78            return Ok(v.clone());
79        }
80
81        let result = match *self {
82            Query::Single(ref s) => {
83                if s.is_empty() {
84                    query(&["defaults"])
85                } else {
86                    query(&[s])
87                }
88            }
89            Query::Multiple(ref s) => query(s),
90        }
91        .context("failed to execute query")?;
92
93        CACHE.insert(self.clone(), result.clone());
94
95        Ok(result)
96    }
97}
98
99pub fn targets_to_versions(v: Option<Targets>) -> Result<Arc<Versions>, Error> {
100    match v {
101        None => Ok(Default::default()),
102        Some(Targets::Versions(v)) => Ok(Arc::new(v)),
103        Some(Targets::Query(q)) => q
104            .exec()
105            .context("failed to convert target query to version data"),
106        Some(Targets::HashMap(mut map)) => {
107            let q = map.remove("browsers").map(|q| match q {
108                QueryOrVersion::Query(q) => q.exec().expect("failed to run query"),
109                _ => unreachable!(),
110            });
111
112            let node = map.remove("node").map(|q| match q {
113                QueryOrVersion::Version(v) => v,
114                QueryOrVersion::Query(..) => unreachable!(),
115            });
116
117            if map.is_empty() {
118                if let Some(q) = q {
119                    let mut q = *q;
120                    q.node = node;
121                    return Ok(Arc::new(q));
122                }
123            }
124
125            let mut result = Versions::default();
126            for (k, v) in map.iter() {
127                match v {
128                    QueryOrVersion::Query(q) => {
129                        let v = q.exec().context("failed to run query")?;
130
131                        for (k, v) in v.iter() {
132                            result.insert(k, *v);
133                        }
134                    }
135                    QueryOrVersion::Version(v) => {
136                        result.insert(k, Some(*v));
137                    }
138                }
139            }
140
141            unimplemented!("Targets: {:?}", map)
142        }
143        _ => unimplemented!("Option<Targets>: {:?}", v),
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::Query;
150
151    #[test]
152    fn test_empty() {
153        let res = Query::Single("".into()).exec().unwrap();
154        assert!(
155            !res.is_any_target(),
156            "empty query should return non-empty result"
157        );
158    }
159}