Skip to main content

leyline/profile/
registry.rs

1use std::collections::HashMap;
2use std::fs::{read_dir, read_to_string};
3use std::path::{Path, PathBuf};
4use std::sync::{Arc, LazyLock};
5
6use crate::profile::Browser;
7use crate::profile::types::BrowserProfile;
8
9#[derive(Debug)]
10#[non_exhaustive]
11pub enum ProfileError {
12    Io {
13        path: PathBuf,
14        source: std::io::Error,
15    },
16    Parse {
17        path: Option<PathBuf>,
18        source: Box<dyn std::error::Error + Send + Sync>,
19    },
20    Empty {
21        path: PathBuf,
22    },
23}
24
25impl ProfileError {
26    pub(crate) fn parse(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
27        Self::Parse {
28            path: None,
29            source: source.into(),
30        }
31    }
32
33    fn at(self, file: &Path) -> Self {
34        match self {
35            Self::Parse { source, .. } => Self::Parse {
36                path: Some(file.to_path_buf()),
37                source,
38            },
39            other => other,
40        }
41    }
42}
43
44impl std::fmt::Display for ProfileError {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        match self {
47            Self::Io { path, source } => write!(f, "cannot read {}: {source}", path.display()),
48            Self::Parse { path, source } => match path {
49                Some(path) => write!(f, "invalid profile {}: {source}", path.display()),
50                None => write!(f, "invalid profile: {source}"),
51            },
52            Self::Empty { path } => write!(
53                f,
54                "{} holds no <family>/<version>.toml profile; a profile directory contains one \
55                 subdirectory per family, each with one TOML file per version",
56                path.display()
57            ),
58        }
59    }
60}
61
62impl std::error::Error for ProfileError {
63    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
64        match self {
65            Self::Io { source, .. } => Some(source),
66            Self::Parse { source, .. } => Some(source.as_ref()),
67            Self::Empty { .. } => None,
68        }
69    }
70}
71
72pub struct ProfileRegistry {
73    profiles: HashMap<(String, u32), Arc<BrowserProfile>>,
74}
75
76impl std::fmt::Debug for ProfileRegistry {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        let mut keys: Vec<(&str, u32)> = self
79            .profiles
80            .keys()
81            .map(|(browser, version)| (browser.as_str(), *version))
82            .collect();
83        keys.sort_unstable();
84        f.debug_struct("ProfileRegistry")
85            .field("profiles", &keys)
86            .finish_non_exhaustive()
87    }
88}
89
90static BUILTIN: LazyLock<ProfileRegistry> = LazyLock::new(ProfileRegistry::builtin);
91
92impl ProfileRegistry {
93    #[must_use]
94    pub fn global() -> &'static Self {
95        &BUILTIN
96    }
97
98    fn new() -> Self {
99        Self {
100            profiles: HashMap::new(),
101        }
102    }
103
104    bench_pub! {
105        fn builtin() -> Self {
106            let mut reg = Self::new();
107            for browser in Browser::all() {
108                reg.load_toml(browser.profile_source());
109            }
110            reg
111        }
112    }
113
114    pub fn load(dir: &Path) -> Result<Self, ProfileError> {
115        let mut reg = Self::new();
116        for family in sorted(dir)? {
117            if !family.is_dir() {
118                continue;
119            }
120            for file in sorted(&family)? {
121                if file.extension().is_none_or(|ext| ext != "toml") {
122                    continue;
123                }
124                let text = read_to_string(&file).map_err(|source| ProfileError::Io {
125                    path: file.clone(),
126                    source,
127                })?;
128                let profile = BrowserProfile::from_toml(&text).map_err(|e| e.at(&file))?;
129                let key = (profile.meta.browser.clone(), profile.meta.version);
130                drop(reg.profiles.insert(key, Arc::new(profile)));
131            }
132        }
133        if reg.is_empty() {
134            return Err(ProfileError::Empty {
135                path: dir.to_path_buf(),
136            });
137        }
138        Ok(reg)
139    }
140
141    fn load_toml(&mut self, toml_str: &str) {
142        let profile = BrowserProfile::from_toml(toml_str)
143            .expect("built-in profile is statically valid (profile_validation)");
144        let key = (profile.meta.browser.clone(), profile.meta.version);
145        self.profiles.insert(key, Arc::new(profile));
146    }
147
148    pub fn get(&self, browser: &str, version: u32) -> Option<&BrowserProfile> {
149        self.profiles
150            .get(&(browser.to_string(), version))
151            .map(|profile| &**profile)
152    }
153
154    pub(crate) fn shared(&self, browser: Browser) -> Option<Arc<BrowserProfile>> {
155        let (name, version) = browser.profile_key();
156        self.profiles.get(&(name.to_string(), version)).cloned()
157    }
158
159    bench_pub! {
160        fn get_browser(&self, browser: Browser) -> Option<&BrowserProfile> {
161            let (name, version) = browser.profile_key();
162            self.get(name, version)
163        }
164    }
165
166    pub(crate) fn is_empty(&self) -> bool {
167        self.profiles.is_empty()
168    }
169}
170
171fn sorted(dir: &Path) -> Result<Vec<PathBuf>, ProfileError> {
172    let entries = read_dir(dir).map_err(|source| ProfileError::Io {
173        path: dir.to_path_buf(),
174        source,
175    })?;
176    let mut paths = Vec::new();
177    for entry in entries {
178        let entry = entry.map_err(|source| ProfileError::Io {
179            path: dir.to_path_buf(),
180            source,
181        })?;
182        paths.push(entry.path());
183    }
184    paths.sort();
185    Ok(paths)
186}
187
188impl Default for ProfileRegistry {
189    fn default() -> Self {
190        Self::builtin()
191    }
192}
193
194#[cfg(test)]
195mod tests;