1use std::fmt;
5use std::sync::Arc;
6
7use super::value::{Setting, SettingValue};
8use crate::icons::{IconMode, PillarStyle};
9
10type Valid = Arc<dyn Fn(&SettingValue) -> bool + Send + Sync>;
12
13#[derive(Clone)]
15enum Allowed {
16 Flag,
18 Text,
20 Choice(Vec<String>),
22 Check(Valid),
24}
25
26impl Allowed {
27 fn accepts(&self, value: &SettingValue) -> bool {
28 match (self, value) {
29 (Self::Flag, SettingValue::Bool(_)) | (Self::Text, SettingValue::Text(_)) => true,
30 (Self::Choice(choices), SettingValue::Text(text)) => choices.contains(text),
31 (Self::Check(valid), value) => valid(value),
32 _ => false,
33 }
34 }
35
36 fn describe(&self) -> String {
38 match self {
39 Self::Flag => "a boolean".to_owned(),
40 Self::Text => "a string".to_owned(),
41 Self::Choice(choices) => format!("one of {}", choices.join(", ")),
42 Self::Check(_) => "a value this application accepts".to_owned(),
43 }
44 }
45}
46
47impl PartialEq for Allowed {
48 fn eq(&self, other: &Self) -> bool {
49 match (self, other) {
50 (Self::Flag, Self::Flag) | (Self::Text, Self::Text) => true,
51 (Self::Choice(a), Self::Choice(b)) => a == b,
52 (Self::Check(a), Self::Check(b)) => Arc::ptr_eq(a, b),
54 _ => false,
55 }
56 }
57}
58
59#[derive(Clone, PartialEq)]
74pub struct SettingKind(Allowed);
75
76impl SettingKind {
77 #[must_use]
79 pub fn flag() -> Self {
80 Self(Allowed::Flag)
81 }
82
83 #[must_use]
85 pub fn text() -> Self {
86 Self(Allowed::Text)
87 }
88
89 #[must_use]
91 pub fn choice(choices: impl IntoIterator<Item = impl Into<String>>) -> Self {
92 Self(Allowed::Choice(choices.into_iter().map(Into::into).collect()))
93 }
94
95 #[must_use]
97 pub fn check<T: Setting + 'static>(valid: impl Fn(&T) -> bool + Send + Sync + 'static) -> Self {
98 Self(Allowed::Check(Arc::new(move |value| T::from_setting(value).is_some_and(|value| valid(&value)))))
99 }
100}
101
102impl fmt::Debug for SettingKind {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 f.write_str(&self.0.describe())
105 }
106}
107
108#[derive(Clone, PartialEq)]
110pub(crate) struct Rule {
111 key: String,
112 allowed: Allowed,
113 default: Option<SettingValue>,
115}
116
117impl Rule {
118 pub(crate) fn accepts(&self, value: &SettingValue) -> bool {
120 self.allowed.accepts(value)
121 }
122
123 pub(crate) fn describe(&self) -> String {
125 self.allowed.describe()
126 }
127
128 pub(crate) fn default_value(&self) -> Option<&SettingValue> {
130 self.default.as_ref()
131 }
132}
133
134#[derive(Clone, Default, PartialEq)]
165pub struct Schema {
166 rules: Vec<Rule>,
167 open: Vec<String>,
169}
170
171impl Schema {
172 #[must_use]
177 pub fn builtin() -> Self {
178 use super::Settings;
179 Self::default()
180 .text(Settings::THEME, "monochrome")
181 .text(Settings::LANGUAGE, "en")
182 .choice(Settings::ICONS, IconMode::ALL.map(IconMode::name), IconMode::Auto.name())
183 .flag(Settings::REDUCED_MOTION, false)
184 .choice(Settings::PILLAR, PillarStyle::ALL.map(PillarStyle::name), PillarStyle::Thick.name())
185 .flag(Settings::SLIDE, true)
186 }
187
188 #[must_use]
190 pub fn flag(self, key: &str, default: bool) -> Self {
191 self.rule(key, Allowed::Flag, Some(SettingValue::Bool(default)))
192 }
193
194 #[must_use]
196 pub fn text(self, key: &str, default: impl Into<String>) -> Self {
197 self.rule(key, Allowed::Text, Some(SettingValue::Text(default.into())))
198 }
199
200 #[must_use]
202 pub fn choice(self, key: &str, choices: impl IntoIterator<Item = impl Into<String>>, default: &str) -> Self {
203 let SettingKind(allowed) = SettingKind::choice(choices);
204 self.rule(key, allowed, Some(SettingValue::Text(default.to_owned())))
205 }
206
207 #[must_use]
210 pub fn check<T: Setting + 'static>(
211 self,
212 key: &str,
213 default: T,
214 valid: impl Fn(&T) -> bool + Send + Sync + 'static,
215 ) -> Self {
216 let SettingKind(allowed) = SettingKind::check(valid);
217 self.rule(key, allowed, Some(default.to_setting()))
218 }
219
220 #[must_use]
234 pub fn optional(self, key: &str, kind: SettingKind) -> Self {
235 let SettingKind(allowed) = kind;
236 self.rule(key, allowed, None)
237 }
238
239 #[must_use]
253 pub fn open(mut self, prefix: &str) -> Self {
254 let prefix = prefix.trim_end_matches('.');
255 if !prefix.is_empty() && !self.open.iter().any(|open| open == prefix) {
256 self.open.push(prefix.to_owned());
257 }
258 self
259 }
260
261 fn rule(mut self, key: &str, allowed: Allowed, default: Option<SettingValue>) -> Self {
262 self.rules.retain(|rule| rule.key != key);
263 self.rules.push(Rule { key: key.to_owned(), allowed, default });
264 self
265 }
266
267 pub(crate) fn get(&self, key: &str) -> Option<&Rule> {
269 self.rules.iter().find(|rule| rule.key == key)
270 }
271
272 pub(crate) fn is_open(&self, key: &str) -> bool {
274 self.open.iter().any(|prefix| key.strip_prefix(prefix.as_str()).is_some_and(|rest| rest.starts_with('.')))
275 }
276}
277
278impl fmt::Debug for Schema {
279 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280 f.debug_struct("Schema")
281 .field("rules", &self.rules.iter().map(|rule| (&rule.key, rule.describe())).collect::<Vec<_>>())
282 .field("open", &self.open)
283 .finish()
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn rules_accept_only_their_values() {
293 let schema = Schema::builtin().check("editor.tab-width", 4u16, |width| (1..=16).contains(width));
294 let icons = schema.get("icons").expect("built in");
295 assert!(icons.accepts(&SettingValue::Text("ascii".into())));
296 assert!(!icons.accepts(&SettingValue::Text("sparkly".into())));
297 assert!(!icons.accepts(&SettingValue::Bool(true)));
298 assert_eq!(icons.describe(), "one of auto, nerd, unicode, ascii");
299 let slide = schema.get("slide").expect("built in");
300 assert!(slide.accepts(&SettingValue::Bool(false)));
301 assert!(!slide.accepts(&SettingValue::Text("true".into())));
302 let width = schema.get("editor.tab-width").expect("declared");
303 assert!(width.accepts(&SettingValue::Integer(8)));
304 assert!(!width.accepts(&SettingValue::Integer(40)));
305 assert!(!width.accepts(&SettingValue::Text("8".into())));
306 assert_eq!(width.default_value(), Some(&SettingValue::Integer(4)));
307 assert!(schema.get("color").is_none());
308 }
309
310 #[test]
311 fn declaring_a_key_again_replaces_it() {
312 let schema = Schema::builtin().choice("language", ["en", "tr"], "tr");
313 assert_eq!(schema.clone().choice("language", ["en", "tr"], "tr"), schema, "one rule per key");
314 let language = schema.get("language").expect("declared");
315 assert!(!language.accepts(&SettingValue::Text("sjds".into())));
316 assert_eq!(language.default_value(), Some(&SettingValue::Text("tr".into())));
317 assert_eq!(Schema::builtin(), Schema::builtin());
318 assert_ne!(schema, Schema::builtin());
319 }
320
321 #[test]
322 fn optional_rules_have_no_default_and_open_prefixes_name_tables() {
323 let schema = Schema::default()
324 .optional("deploy.note", SettingKind::text())
325 .optional("deploy.retries", SettingKind::check(|retries: &u8| (1..=10).contains(retries)))
326 .open("plugins")
327 .open("plugins.")
328 .open("");
329 let note = schema.get("deploy.note").expect("declared");
330 assert_eq!(note.default_value(), None);
331 assert!(note.accepts(&SettingValue::Text("freeze".into())) && !note.accepts(&SettingValue::Integer(1)));
332 let retries = schema.get("deploy.retries").expect("declared");
333 assert!(retries.accepts(&SettingValue::Integer(3)) && !retries.accepts(&SettingValue::Integer(30)));
334 assert!(schema.is_open("plugins.git") && schema.is_open("plugins.git.sign") && schema.is_open("plugins."));
335 assert!(!schema.is_open("plugins") && !schema.is_open("plugins-extra.x") && !schema.is_open("deploy.note"));
336 assert_eq!(format!("{schema:?}"), format!("{:?}", schema.clone().open("plugins")), "each prefix once");
337 let declared = Schema::default().text("deploy.note", "").optional("deploy.note", SettingKind::text());
338 assert_eq!(declared.get("deploy.note").and_then(Rule::default_value), None, "declaring again replaces");
339 assert_eq!(format!("{:?}", SettingKind::choice(["a", "b"])), "one of a, b");
340 }
341}