use std::fmt;
use std::sync::Arc;
use super::value::{Setting, SettingValue};
use crate::icons::{IconMode, PillarStyle};
type Valid = Arc<dyn Fn(&SettingValue) -> bool + Send + Sync>;
#[derive(Clone)]
enum Allowed {
Flag,
Text,
Choice(Vec<String>),
Check(Valid),
}
impl Allowed {
fn accepts(&self, value: &SettingValue) -> bool {
match (self, value) {
(Self::Flag, SettingValue::Bool(_)) | (Self::Text, SettingValue::Text(_)) => true,
(Self::Choice(choices), SettingValue::Text(text)) => choices.contains(text),
(Self::Check(valid), value) => valid(value),
_ => false,
}
}
fn describe(&self) -> String {
match self {
Self::Flag => "a boolean".to_owned(),
Self::Text => "a string".to_owned(),
Self::Choice(choices) => format!("one of {}", choices.join(", ")),
Self::Check(_) => "a value this application accepts".to_owned(),
}
}
}
impl PartialEq for Allowed {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Flag, Self::Flag) | (Self::Text, Self::Text) => true,
(Self::Choice(a), Self::Choice(b)) => a == b,
(Self::Check(a), Self::Check(b)) => Arc::ptr_eq(a, b),
_ => false,
}
}
}
#[derive(Clone, PartialEq)]
pub struct SettingKind(Allowed);
impl SettingKind {
#[must_use]
pub fn flag() -> Self {
Self(Allowed::Flag)
}
#[must_use]
pub fn text() -> Self {
Self(Allowed::Text)
}
#[must_use]
pub fn choice(choices: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self(Allowed::Choice(choices.into_iter().map(Into::into).collect()))
}
#[must_use]
pub fn check<T: Setting + 'static>(valid: impl Fn(&T) -> bool + Send + Sync + 'static) -> Self {
Self(Allowed::Check(Arc::new(move |value| T::from_setting(value).is_some_and(|value| valid(&value)))))
}
}
impl fmt::Debug for SettingKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0.describe())
}
}
#[derive(Clone, PartialEq)]
pub(crate) struct Rule {
key: String,
allowed: Allowed,
default: Option<SettingValue>,
}
impl Rule {
pub(crate) fn accepts(&self, value: &SettingValue) -> bool {
self.allowed.accepts(value)
}
pub(crate) fn describe(&self) -> String {
self.allowed.describe()
}
pub(crate) fn default_value(&self) -> Option<&SettingValue> {
self.default.as_ref()
}
}
#[derive(Clone, Default, PartialEq)]
pub struct Schema {
rules: Vec<Rule>,
open: Vec<String>,
}
impl Schema {
#[must_use]
pub fn builtin() -> Self {
use super::Settings;
Self::default()
.text(Settings::THEME, "monochrome")
.text(Settings::LANGUAGE, "en")
.choice(Settings::ICONS, IconMode::ALL.map(IconMode::name), IconMode::Auto.name())
.flag(Settings::REDUCED_MOTION, false)
.choice(Settings::PILLAR, PillarStyle::ALL.map(PillarStyle::name), PillarStyle::Thick.name())
.flag(Settings::SLIDE, true)
}
#[must_use]
pub fn flag(self, key: &str, default: bool) -> Self {
self.rule(key, Allowed::Flag, Some(SettingValue::Bool(default)))
}
#[must_use]
pub fn text(self, key: &str, default: impl Into<String>) -> Self {
self.rule(key, Allowed::Text, Some(SettingValue::Text(default.into())))
}
#[must_use]
pub fn choice(self, key: &str, choices: impl IntoIterator<Item = impl Into<String>>, default: &str) -> Self {
let SettingKind(allowed) = SettingKind::choice(choices);
self.rule(key, allowed, Some(SettingValue::Text(default.to_owned())))
}
#[must_use]
pub fn check<T: Setting + 'static>(
self,
key: &str,
default: T,
valid: impl Fn(&T) -> bool + Send + Sync + 'static,
) -> Self {
let SettingKind(allowed) = SettingKind::check(valid);
self.rule(key, allowed, Some(default.to_setting()))
}
#[must_use]
pub fn optional(self, key: &str, kind: SettingKind) -> Self {
let SettingKind(allowed) = kind;
self.rule(key, allowed, None)
}
#[must_use]
pub fn open(mut self, prefix: &str) -> Self {
let prefix = prefix.trim_end_matches('.');
if !prefix.is_empty() && !self.open.iter().any(|open| open == prefix) {
self.open.push(prefix.to_owned());
}
self
}
fn rule(mut self, key: &str, allowed: Allowed, default: Option<SettingValue>) -> Self {
self.rules.retain(|rule| rule.key != key);
self.rules.push(Rule { key: key.to_owned(), allowed, default });
self
}
pub(crate) fn get(&self, key: &str) -> Option<&Rule> {
self.rules.iter().find(|rule| rule.key == key)
}
pub(crate) fn is_open(&self, key: &str) -> bool {
self.open.iter().any(|prefix| key.strip_prefix(prefix.as_str()).is_some_and(|rest| rest.starts_with('.')))
}
}
impl fmt::Debug for Schema {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Schema")
.field("rules", &self.rules.iter().map(|rule| (&rule.key, rule.describe())).collect::<Vec<_>>())
.field("open", &self.open)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rules_accept_only_their_values() {
let schema = Schema::builtin().check("editor.tab-width", 4u16, |width| (1..=16).contains(width));
let icons = schema.get("icons").expect("built in");
assert!(icons.accepts(&SettingValue::Text("ascii".into())));
assert!(!icons.accepts(&SettingValue::Text("sparkly".into())));
assert!(!icons.accepts(&SettingValue::Bool(true)));
assert_eq!(icons.describe(), "one of auto, nerd, unicode, ascii");
let slide = schema.get("slide").expect("built in");
assert!(slide.accepts(&SettingValue::Bool(false)));
assert!(!slide.accepts(&SettingValue::Text("true".into())));
let width = schema.get("editor.tab-width").expect("declared");
assert!(width.accepts(&SettingValue::Integer(8)));
assert!(!width.accepts(&SettingValue::Integer(40)));
assert!(!width.accepts(&SettingValue::Text("8".into())));
assert_eq!(width.default_value(), Some(&SettingValue::Integer(4)));
assert!(schema.get("color").is_none());
}
#[test]
fn declaring_a_key_again_replaces_it() {
let schema = Schema::builtin().choice("language", ["en", "tr"], "tr");
assert_eq!(schema.clone().choice("language", ["en", "tr"], "tr"), schema, "one rule per key");
let language = schema.get("language").expect("declared");
assert!(!language.accepts(&SettingValue::Text("sjds".into())));
assert_eq!(language.default_value(), Some(&SettingValue::Text("tr".into())));
assert_eq!(Schema::builtin(), Schema::builtin());
assert_ne!(schema, Schema::builtin());
}
#[test]
fn optional_rules_have_no_default_and_open_prefixes_name_tables() {
let schema = Schema::default()
.optional("deploy.note", SettingKind::text())
.optional("deploy.retries", SettingKind::check(|retries: &u8| (1..=10).contains(retries)))
.open("plugins")
.open("plugins.")
.open("");
let note = schema.get("deploy.note").expect("declared");
assert_eq!(note.default_value(), None);
assert!(note.accepts(&SettingValue::Text("freeze".into())) && !note.accepts(&SettingValue::Integer(1)));
let retries = schema.get("deploy.retries").expect("declared");
assert!(retries.accepts(&SettingValue::Integer(3)) && !retries.accepts(&SettingValue::Integer(30)));
assert!(schema.is_open("plugins.git") && schema.is_open("plugins.git.sign") && schema.is_open("plugins."));
assert!(!schema.is_open("plugins") && !schema.is_open("plugins-extra.x") && !schema.is_open("deploy.note"));
assert_eq!(format!("{schema:?}"), format!("{:?}", schema.clone().open("plugins")), "each prefix once");
let declared = Schema::default().text("deploy.note", "").optional("deploy.note", SettingKind::text());
assert_eq!(declared.get("deploy.note").and_then(Rule::default_value), None, "declaring again replaces");
assert_eq!(format!("{:?}", SettingKind::choice(["a", "b"])), "one of a, b");
}
}