use std::{collections::HashMap, fmt, path::PathBuf};
use reovim_arch::sync::RwLock;
#[derive(Debug, Clone, PartialEq)]
pub enum ConfigValue {
Bool(bool),
Integer(i64),
String(String),
Array(Vec<Self>),
Table(HashMap<String, Self>),
}
impl ConfigValue {
#[must_use]
pub const fn type_name(&self) -> &'static str {
match self {
Self::Bool(_) => "bool",
Self::Integer(_) => "integer",
Self::String(_) => "string",
Self::Array(_) => "array",
Self::Table(_) => "table",
}
}
#[must_use]
pub const fn as_bool(&self) -> Option<bool> {
match self {
Self::Bool(b) => Some(*b),
_ => None,
}
}
#[must_use]
pub const fn as_int(&self) -> Option<i64> {
match self {
Self::Integer(i) => Some(*i),
_ => None,
}
}
#[must_use]
pub fn as_str(&self) -> Option<&str> {
match self {
Self::String(s) => Some(s),
_ => None,
}
}
#[must_use]
pub fn as_array(&self) -> Option<&[Self]> {
match self {
Self::Array(a) => Some(a),
_ => None,
}
}
#[must_use]
pub const fn as_table(&self) -> Option<&HashMap<String, Self>> {
match self {
Self::Table(t) => Some(t),
_ => None,
}
}
#[must_use]
pub const fn as_table_mut(&mut self) -> Option<&mut HashMap<String, Self>> {
match self {
Self::Table(t) => Some(t),
_ => None,
}
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl fmt::Display for ConfigValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Bool(b) => write!(f, "{b}"),
Self::Integer(i) => write!(f, "{i}"),
Self::String(s) => write!(f, "{s}"),
Self::Array(arr) => write!(f, "[{} items]", arr.len()),
Self::Table(t) => write!(f, "{{{} entries}}", t.len()),
}
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl From<bool> for ConfigValue {
fn from(b: bool) -> Self {
Self::Bool(b)
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl From<i64> for ConfigValue {
fn from(i: i64) -> Self {
Self::Integer(i)
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl From<i32> for ConfigValue {
fn from(i: i32) -> Self {
Self::Integer(i64::from(i))
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl From<String> for ConfigValue {
fn from(s: String) -> Self {
Self::String(s)
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl From<&str> for ConfigValue {
fn from(s: &str) -> Self {
Self::String(s.to_string())
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl<T: Into<Self>> From<Vec<T>> for ConfigValue {
fn from(v: Vec<T>) -> Self {
Self::Array(v.into_iter().map(Into::into).collect())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConfigError {
NotFound(String),
TypeMismatch {
key: String,
expected: &'static str,
got: &'static str,
},
PathError(String),
Io(String),
Parse(String),
Serialize(String),
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotFound(key) => write!(f, "config key not found: {key}"),
Self::TypeMismatch { key, expected, got } => {
write!(f, "type mismatch for '{key}': expected {expected}, got {got}")
}
Self::PathError(msg) => write!(f, "path error: {msg}"),
Self::Io(msg) => write!(f, "IO error: {msg}"),
Self::Parse(msg) => write!(f, "parse error: {msg}"),
Self::Serialize(msg) => write!(f, "serialize error: {msg}"),
}
}
}
impl std::error::Error for ConfigError {}
#[derive(Debug, Default)]
pub struct Config {
data: RwLock<HashMap<String, ConfigValue>>,
path: RwLock<Option<PathBuf>>,
}
impl Config {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_path(path: PathBuf) -> Self {
Self {
data: RwLock::new(HashMap::new()),
path: RwLock::new(Some(path)),
}
}
#[must_use]
pub fn path(&self) -> Option<PathBuf> {
self.path.read().clone()
}
pub fn set_path(&self, path: PathBuf) {
*self.path.write() = Some(path);
}
#[must_use]
pub fn get(&self, key: &str) -> Option<ConfigValue> {
let data = self.data.read();
data.get(key).cloned()
}
#[must_use]
pub fn get_bool(&self, key: &str) -> Option<bool> {
self.get(key).and_then(|v| v.as_bool())
}
#[must_use]
pub fn get_int(&self, key: &str) -> Option<i64> {
self.get(key).and_then(|v| v.as_int())
}
#[must_use]
pub fn get_str(&self, key: &str) -> Option<String> {
self.get(key).and_then(|v| v.as_str().map(String::from))
}
#[must_use]
pub fn get_or(&self, key: &str, default: ConfigValue) -> ConfigValue {
self.get(key).unwrap_or(default)
}
#[must_use]
pub fn get_bool_or(&self, key: &str, default: bool) -> bool {
self.get_bool(key).unwrap_or(default)
}
#[must_use]
pub fn get_int_or(&self, key: &str, default: i64) -> i64 {
self.get_int(key).unwrap_or(default)
}
#[must_use]
pub fn get_str_or(&self, key: &str, default: &str) -> String {
self.get_str(key).unwrap_or_else(|| default.to_string())
}
pub fn set(&self, key: &str, value: ConfigValue) {
let mut data = self.data.write();
data.insert(key.to_string(), value);
}
pub fn set_bool(&self, key: &str, value: bool) {
self.set(key, ConfigValue::Bool(value));
}
pub fn set_int(&self, key: &str, value: i64) {
self.set(key, ConfigValue::Integer(value));
}
pub fn set_str(&self, key: &str, value: impl Into<String>) {
self.set(key, ConfigValue::String(value.into()));
}
pub fn remove(&self, key: &str) -> Option<ConfigValue> {
let mut data = self.data.write();
data.remove(key)
}
#[must_use]
pub fn contains(&self, key: &str) -> bool {
let data = self.data.read();
data.contains_key(key)
}
#[must_use]
pub fn keys(&self) -> Vec<String> {
let data = self.data.read();
data.keys().cloned().collect()
}
#[must_use]
pub fn keys_with_prefix(&self, prefix: &str) -> Vec<String> {
let data = self.data.read();
data.keys()
.filter(|k| k.starts_with(prefix))
.cloned()
.collect()
}
pub fn clear(&self) {
let mut data = self.data.write();
data.clear();
}
#[must_use]
pub fn len(&self) -> usize {
let data = self.data.read();
data.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
let data = self.data.read();
data.is_empty()
}
pub fn merge(&self, other: &Self) {
let other_data = other.data.read();
let mut self_data = self.data.write();
for (key, value) in other_data.iter() {
self_data.insert(key.clone(), value.clone());
}
}
#[must_use]
pub fn to_map(&self) -> HashMap<String, ConfigValue> {
let data = self.data.read();
data.clone()
}
pub fn from_map(&self, map: HashMap<String, ConfigValue>) {
let mut data = self.data.write();
*data = map;
}
}
pub struct ConfigPaths;
impl ConfigPaths {
pub fn config_dir() -> Result<PathBuf, ConfigError> {
Self::resolve_dir(
std::env::var("REOVIM_CONFIG_DIR").ok(),
reovim_arch::dirs::config_dir(),
"config",
)
}
pub fn data_dir() -> Result<PathBuf, ConfigError> {
Self::resolve_dir(
std::env::var("REOVIM_DATA_DIR").ok(),
reovim_arch::dirs::data_local_dir(),
"data",
)
}
pub fn cache_dir() -> Result<PathBuf, ConfigError> {
Self::resolve_dir(
std::env::var("REOVIM_CACHE_DIR").ok(),
reovim_arch::dirs::cache_dir(),
"cache",
)
}
fn resolve_dir(
env_override: Option<String>,
platform_default: Option<PathBuf>,
kind: &str,
) -> Result<PathBuf, ConfigError> {
if let Some(dir) = env_override {
return Ok(PathBuf::from(dir));
}
platform_default
.map(|p| p.join("reovim"))
.ok_or_else(|| ConfigError::PathError(format!("cannot determine {kind} directory")))
}
pub fn config_file() -> Result<PathBuf, ConfigError> {
Self::config_dir().map(|p| p.join("config.toml"))
}
pub fn profiles_dir() -> Result<PathBuf, ConfigError> {
Self::config_dir().map(|p| p.join("profiles"))
}
}
#[cfg(test)]
#[path = "config_tests.rs"]
mod tests;