crates/fig_settings/src/lib.rs (315 lines of code) (raw):
pub mod error;
pub mod history;
pub mod keybindings;
pub mod keys;
pub mod settings;
pub mod sqlite;
pub mod state;
use std::fs::{
self,
File,
};
use std::io::{
Read,
Seek,
SeekFrom,
Write,
};
use std::path::PathBuf;
pub use error::{
Error,
Result,
};
use fd_lock::RwLock as FileRwLock;
use fig_util::directories;
use parking_lot::{
MappedRwLockReadGuard,
MappedRwLockWriteGuard,
RwLock,
RwLockReadGuard,
RwLockWriteGuard,
};
use serde_json::Value;
pub use settings::{
Settings,
SettingsProvider,
};
pub use state::{
State,
StateProvider,
};
use thiserror::Error;
use tracing::error;
pub type Map = serde_json::Map<String, Value>;
static SETTINGS_FILE_LOCK: RwLock<()> = RwLock::new(());
static SETTINGS_DATA: RwLock<Option<Map>> = RwLock::new(None);
#[derive(Debug, Clone)]
pub enum Backend {
Global,
Memory(Map),
}
pub enum ReadGuard<'a, T> {
Global(RwLockReadGuard<'a, Option<T>>),
Memory(&'a T),
}
impl<'a, T> ReadGuard<'a, T> {
pub fn map<U, F: FnOnce(&T) -> &U>(self, f: F) -> MappedReadGuard<'a, U> {
match self {
ReadGuard::Global(guard) => {
MappedReadGuard::Global(RwLockReadGuard::<'a, Option<T>>::map(guard, |data: &Option<T>| {
f(data.as_ref().expect("global backend is not used"))
}))
},
ReadGuard::Memory(data) => MappedReadGuard::Memory(f(data)),
}
}
pub fn try_map<U, F: FnOnce(&T) -> Option<&U>>(self, f: F) -> Option<MappedReadGuard<'a, U>> {
match self {
ReadGuard::Global(guard) => RwLockReadGuard::<'a, Option<T>>::try_map(guard, |data: &Option<T>| {
f(data.as_ref().expect("global backend is not used"))
})
.ok()
.map(MappedReadGuard::Global),
ReadGuard::Memory(data) => f(data).map(MappedReadGuard::Memory),
}
}
}
impl<T> std::ops::Deref for ReadGuard<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
match self {
ReadGuard::Global(guard) => guard.as_ref().expect("global backend is not used"),
ReadGuard::Memory(data) => data,
}
}
}
pub enum MappedReadGuard<'a, T> {
Global(MappedRwLockReadGuard<'a, T>),
Memory(&'a T),
}
impl<T> std::ops::Deref for MappedReadGuard<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
match self {
MappedReadGuard::Global(guard) => guard,
MappedReadGuard::Memory(data) => data,
}
}
}
pub enum WriteGuard<'a, T> {
Global(RwLockWriteGuard<'a, Option<T>>),
Memory(&'a mut T),
}
impl<'a, T> WriteGuard<'a, T> {
pub fn map<U, F: FnOnce(&mut T) -> &mut U>(self, f: F) -> MappedWriteGuard<'a, U> {
match self {
WriteGuard::Global(guard) => {
MappedWriteGuard::Global(RwLockWriteGuard::<'a, Option<T>>::map(guard, |data: &mut Option<T>| {
f(data.as_mut().expect("global backend is not used"))
}))
},
WriteGuard::Memory(data) => MappedWriteGuard::Memory(f(data)),
}
}
pub fn try_map<U, F: FnOnce(&mut T) -> Option<&mut U>>(self, f: F) -> Option<MappedWriteGuard<'a, U>> {
match self {
WriteGuard::Global(guard) => RwLockWriteGuard::<'a, Option<T>>::try_map(guard, |data: &mut Option<T>| {
f(data.as_mut().expect("global backend is not used"))
})
.ok()
.map(MappedWriteGuard::Global),
WriteGuard::Memory(data) => f(data).map(MappedWriteGuard::Memory),
}
}
}
impl<T> std::ops::Deref for WriteGuard<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
match self {
WriteGuard::Global(guard) => guard.as_ref().expect("global backend is not used"),
WriteGuard::Memory(data) => data,
}
}
}
impl<T> std::ops::DerefMut for WriteGuard<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
WriteGuard::Global(guard) => guard.as_mut().expect("global backend is not used"),
WriteGuard::Memory(data) => data,
}
}
}
pub enum MappedWriteGuard<'a, T> {
Global(MappedRwLockWriteGuard<'a, T>),
Memory(&'a mut T),
}
impl<T> std::ops::Deref for MappedWriteGuard<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
match self {
MappedWriteGuard::Global(guard) => guard,
MappedWriteGuard::Memory(data) => data,
}
}
}
impl<T> std::ops::DerefMut for MappedWriteGuard<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
MappedWriteGuard::Global(guard) => guard,
MappedWriteGuard::Memory(data) => data,
}
}
}
pub trait JsonStore: Sized {
/// Path to the file
fn path() -> Result<PathBuf>;
/// In mem lock on the file
fn file_lock() -> &'static RwLock<()>;
/// [RwLock] on the data, [None] if not using the global backend
fn data_lock() -> &'static RwLock<Option<Map>>;
fn new_from_backend(backend: Backend) -> Self;
fn map(&self) -> ReadGuard<'_, Map>;
fn map_mut(&mut self) -> WriteGuard<'_, Map>;
fn load() -> Result<Self> {
let is_global = Self::data_lock().read().as_ref().is_some();
if is_global {
Ok(Self::new_from_backend(Backend::Global))
} else {
Ok(Self::new_from_backend(Backend::Memory(Self::load_from_file()?)))
}
}
fn load_from_file() -> Result<Map> {
let path = Self::path()?;
// If the folder doesn't exist, create it.
if let Some(parent) = path.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
let json: Map = {
let _lock_guard = Self::file_lock().write();
// If the file doesn't exist, create it.
if !path.exists() {
let mut file = FileRwLock::new(File::create(path)?);
file.write()?.write_all(b"{}")?;
serde_json::Map::new()
} else {
let mut file = FileRwLock::new(File::open(&path)?);
let mut read = file.write()?;
serde_json::from_reader(&mut *read)?
}
};
Ok(json)
}
/// Loads data from file into global backend
fn load_into_global() -> Result<()> {
match Self::load_from_file() {
Ok(json) => {
*Self::data_lock().write() = Some(json);
Ok(())
},
Err(err) => {
*Self::data_lock().write() = Some(Map::new());
let file_content: Result<String> = (|| {
let _lock_guard = Self::file_lock().write();
let mut file = FileRwLock::new(File::open(Self::path()?)?);
let mut read = file.write()?;
let mut content = String::new();
#[allow(clippy::verbose_file_reads)]
read.read_to_string(&mut content)?;
Ok(content)
})();
error!(%err, ?file_content, "Failed to load json file into global backend");
// Write default data to file
let json = Self::new_from_backend(Backend::Memory(Map::new()));
if let Err(err) = json.save_to_file() {
error!(%err, "Failed to write default data to file");
}
Err(err)
},
}
}
fn save_to_file(&self) -> Result<()> {
let path = Self::path()?;
// If the folder doesn't exist, create it.
if let Some(parent) = path.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
let _lock_guard = Self::file_lock().write();
let mut file_opts = File::options();
file_opts.create(true).write(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
file_opts.mode(0o600);
}
let mut file = FileRwLock::new(file_opts.open(&path)?);
let mut lock = file.write()?;
if let Err(_err) = serde_json::to_writer_pretty(&mut *lock, &*self.map()) {
// Write {} to the file if the serialization failed
lock.seek(SeekFrom::Start(0))?;
lock.set_len(0)?;
lock.write_all(b"{}")?;
};
lock.flush()?;
Ok(())
}
fn set(&mut self, key: impl Into<String>, value: impl Into<serde_json::Value>) {
self.map_mut().insert(key.into(), value.into());
}
fn get(&self, key: impl AsRef<str>) -> Option<MappedReadGuard<'_, Value>> {
self.map().try_map(|data| data.get(key.as_ref()))
}
fn remove(&mut self, key: impl AsRef<str>) -> Option<Value> {
self.map_mut().remove(key.as_ref())
}
fn get_mut(&mut self, key: impl Into<String>) -> Option<MappedWriteGuard<'_, Value>> {
self.map_mut().try_map(|data| data.get_mut(&key.into()))
}
fn get_bool(&self, key: impl AsRef<str>) -> Option<bool> {
self.get(key).and_then(|value| value.as_bool())
}
fn get_bool_or(&self, key: impl AsRef<str>, default: bool) -> bool {
self.get_bool(key).unwrap_or(default)
}
fn get_string(&self, key: impl AsRef<str>) -> Option<String> {
self.get(key).and_then(|value| value.as_str().map(|s| s.into()))
}
fn get_string_or(&self, key: impl AsRef<str>, default: String) -> String {
self.get_string(key).unwrap_or(default)
}
fn get_int(&self, key: impl AsRef<str>) -> Option<i64> {
self.get(key).and_then(|value| value.as_i64())
}
fn get_int_or(&self, key: impl AsRef<str>, default: i64) -> i64 {
self.get_int(key).unwrap_or(default)
}
}
pub struct OldSettings {
pub(crate) inner: Backend,
}
impl JsonStore for OldSettings {
fn path() -> Result<PathBuf> {
Ok(directories::settings_path()?)
}
fn file_lock() -> &'static RwLock<()> {
&SETTINGS_FILE_LOCK
}
fn data_lock() -> &'static RwLock<Option<Map>> {
&SETTINGS_DATA
}
fn new_from_backend(backend: Backend) -> Self {
match backend {
Backend::Global => Self { inner: Backend::Global },
Backend::Memory(map) => Self {
inner: Backend::Memory(map),
},
}
}
fn map(&self) -> ReadGuard<'_, Map> {
match &self.inner {
Backend::Global => ReadGuard::Global(Self::data_lock().read()),
Backend::Memory(map) => ReadGuard::Memory(map),
}
}
fn map_mut(&mut self) -> WriteGuard<'_, Map> {
match &mut self.inner {
Backend::Global => WriteGuard::Global(Self::data_lock().write()),
Backend::Memory(map) => WriteGuard::Memory(map),
}
}
}
// #[cfg(test)]
// mod test {
// use std::path::Path;
// use super::*;
// fn test_store_type(path: &Path, store: JsonType) {
// let mut local_json = LocalJson::load_file(store).unwrap();
// assert_eq!(fs::read_to_string(path).unwrap(), "");
// assert_eq!(local_json.inner, serde_json::Map::new());
// local_json.save().unwrap();
// assert_eq!(fs::read_to_string(path).unwrap(), "{}");
// local_json.set("a", 123);
// local_json.set("b", "hello");
// local_json.set("c", false);
// local_json.save().unwrap();
// assert_eq!(
// fs::read_to_string(path).unwrap(),
// "{\n \"a\": 123,\n \"b\": \"hello\",\n \"c\": false\n}"
// );
// local_json.remove("a").unwrap();
// local_json.save().unwrap();
// assert_eq!(
// fs::read_to_string(path).unwrap(),
// "{\n \"b\": \"hello\",\n \"c\": false\n}"
// );
// assert_eq!(local_json.get("b").unwrap(), "hello");
// fs::write(path, "invalid json").unwrap();
// assert!(matches!(
// LocalJson::load_file(store).unwrap_err(),
// Error::SettingsNotObject
// ));
// }
// #[fig_test::test]
// fn test_settings_raw() {
// let path = tempfile::tempdir().unwrap().into_path().join("local.json");
// std::env::set_var("FIG_DIRECTORIES_SETTINGS_PATH", &path);
// test_store_type(&path, JsonType::Settings);
// }
// #[fig_test::test]
// fn test_state_raw() {
// let path = tempfile::tempdir().unwrap().into_path().join("local.json");
// std::env::set_var("FIG_DIRECTORIES_STATE_PATH", &path);
// test_store_type(&path, JsonType::State);
// }
// }