leyline/profile/
registry.rs1use 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;