Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .idea/lnx.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 12 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ members = [
"lnx-shell",
"lnx-doc",
"lnx-tantivy",
"lnx-config",
]

[workspace.dependencies]
Expand Down Expand Up @@ -58,4 +59,4 @@ sqlx = { version = "0.8.2", default-features = false, features = ["sqlite", "mac
opendal = { version = "0.50.2", default-features = false }
rend = { version = "0.5.2", features = ["bytemuck-1"] }
rkyv = { version = "0.8.8", features = ["little_endian", "pointer_width_32"] }
chrono = { version = "0.4.38" }
chrono = { version = "0.4.38" }
14 changes: 14 additions & 0 deletions lnx-config/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "lnx-config"
version = "0.1.0"
edition = "2024"
description = "lnx config manages the binary service's settings."

[dependencies]
ahash = { workspace = true }
parking_lot = { workspace = true }
thiserror = { workspace = true }

[features]
# Enables thread local state.
thread-local = []
80 changes: 80 additions & 0 deletions lnx-config/src/access.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use std::fmt::{Debug, Display, Formatter};
use std::ops::Deref;

#[macro_export]
/// Get a config parameter.
macro_rules! get {
($cfg:ident) => {
$crate::get_config::<$cfg>().unwrap_or_else(|| {
panic!("config {} has not be initialised yet", stringify!($cfg))
})
};
($cfg:ident.$attr:ident) => {
$crate::get_config::<$cfg>()
.map(|c| unsafe {
$crate::access::Accessed::__with_value_and_parent(c.clone(), &c.$attr)
})
.unwrap_or_else(|| {
panic!("config {} has not be initialised yet", stringify!($cfg))
})
};
}

/// A wrapper around a config value and an access value.
///
/// This is to allow it to hold onto the parent value which owns the data.
pub struct Accessed<C, T>
where
C: 'static,
T: 'static,
{
#[allow(unused)]
inner: C,
value: &'static T,
}

impl<C, T> Debug for Accessed<C, T>
where
T: Debug,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.value.fmt(f)
}
}

impl<C, T> Display for Accessed<C, T>
where
T: Display,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.value.fmt(f)
}
}

impl<C, T> PartialEq<T> for Accessed<C, T>
where
T: PartialEq<T>,
{
fn eq(&self, other: &T) -> bool {
self.value == other
}
}

impl<C, T> Accessed<C, T> {
#[doc(hidden)]
/// DO NOT USE!!
pub unsafe fn __with_value_and_parent(parent: C, value: &T) -> Accessed<C, T> {
Self {
inner: parent,
value: unsafe { std::mem::transmute::<&T, &'static T>(value) },
}
}
}

impl<C, T> Deref for Accessed<C, T> {
type Target = T;

fn deref(&self) -> &Self::Target {
self.value
}
}
87 changes: 87 additions & 0 deletions lnx-config/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use std::any::{Any, TypeId};
use std::sync::Arc;

#[doc(hidden)]
pub mod access;
mod state;

#[derive(Debug, thiserror::Error)]
#[error("provided config type is already initialised")]
/// The provided config is already initialised in the global state.
pub struct ConfigAlreadyExists;

#[derive(Debug, thiserror::Error)]
#[error("provided config type is not initialised")]
/// The provided config is not initialised in the global state.
pub struct ConfigNotInitialisedExists;

/// Initialise a config in the global or thread local state for dependants to pull.
///
/// Errors if the config already exists within the state.
pub fn init<T>(config: T) -> Result<(), ConfigAlreadyExists>
where
T: Any + Send + Sync + 'static,
{
let type_id = TypeId::of::<T>();
if state::exists(type_id) {
return Err(ConfigAlreadyExists);
}
state::set_auto(type_id, Arc::new(config));
Ok(())
}

#[cfg(feature = "thread-local")]
/// Initialise a config in the _global state only_ for dependants to pull.
///
/// Errors if the config already exists within the state.
pub fn init_global<T>(config: T) -> Result<(), ConfigAlreadyExists>
where
T: Any + Send + Sync + 'static,
{
let type_id = TypeId::of::<T>();
if state::exists(type_id) {
return Err(ConfigAlreadyExists);
}
state::set_global(type_id, Arc::new(config));
Ok(())
}

/// Update an existing config.
///
/// This will trigger any watching callbacks for the given config if they are using [WatchMode::Live].
pub fn update<T>(config: T) -> Result<(), ConfigNotInitialisedExists>
where
T: Any + Send + Sync + 'static,
{
let type_id = TypeId::of::<T>();
if !state::exists(type_id) {
return Err(ConfigNotInitialisedExists);
}
state::set_auto(type_id, Arc::new(config));
Ok(())
}

#[cfg(feature = "thread-local")]
/// Update an existing config in the thread-local state.
///
/// This will trigger any watching callbacks for the given config if they are using [WatchMode::Live].
pub fn update_global<T>(config: T) -> Result<(), ConfigNotInitialisedExists>
where
T: Any + Send + Sync + 'static,
{
let type_id = TypeId::of::<T>();
if !state::exists(type_id) {
return Err(ConfigNotInitialisedExists);
}
state::set_global(type_id, Arc::new(config));
Ok(())
}

#[doc(hidden)]
/// Get an existing config if it is initialised.
pub fn get_config<T>() -> Option<Arc<T>>
where
T: Any + Send + Sync + 'static,
{
state::get(TypeId::of::<T>()).and_then(|v| v.downcast().ok())
}
123 changes: 123 additions & 0 deletions lnx-config/src/state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
use std::any::{Any, TypeId};
use std::sync::Arc;

use parking_lot::RwLock;

type DynConfig = Arc<dyn Any + Send + Sync>;

struct StateEntry {
entry: DynConfig,
}

#[derive(Default)]
struct State(RwLock<ahash::HashMap<TypeId, StateEntry>>);

impl State {
fn exists(&self, type_id: TypeId) -> bool {
self.0.read().contains_key(&type_id)
}

fn set(&self, type_id: TypeId, cfg: DynConfig) {
self.0
.write()
.entry(type_id)
.and_modify(|e| {
e.entry = cfg.clone();
})
.or_insert_with(|| StateEntry { entry: cfg });
}

fn get(&self, type_id: TypeId) -> Option<DynConfig> {
self.0.read().get(&type_id).map(|e| e.entry.clone())
}
}

mod global_state {
use std::sync::OnceLock;

use super::*;

static GLOBAL_STATE: OnceLock<State> = OnceLock::new();

pub(super) fn set(type_id: TypeId, cfg: DynConfig) {
let state = GLOBAL_STATE.get_or_init(State::default);
state.set(type_id, cfg)
}

pub(super) fn exists(type_id: TypeId) -> bool {
let state = GLOBAL_STATE.get_or_init(State::default);
state.exists(type_id)
}

pub(super) fn get(type_id: TypeId) -> Option<DynConfig> {
let state = GLOBAL_STATE.get_or_init(State::default);
state.get(type_id)
}
}

#[cfg(feature = "thread-local")]
mod thread_local_state {
use std::sync::OnceLock;

use super::*;

thread_local! {
static GLOBAL_STATE: OnceLock<State> = const { OnceLock::new() };
}

pub(super) fn set(type_id: TypeId, cfg: DynConfig) {
GLOBAL_STATE.with(|s| {
let state = s.get_or_init(State::default);
state.set(type_id, cfg)
})
}

pub(super) fn exists(type_id: TypeId) -> bool {
GLOBAL_STATE.with(|s| {
let state = s.get_or_init(State::default);
state.exists(type_id)
})
}

pub(super) fn get(type_id: TypeId) -> Option<DynConfig> {
GLOBAL_STATE.with(|s| {
let state = s.get_or_init(State::default);
state.get(type_id)
})
}
}

pub(crate) fn set_auto(type_id: TypeId, cfg: DynConfig) {
#[cfg(feature = "thread-local")]
thread_local_state::set(type_id, cfg);
#[cfg(not(feature = "thread-local"))]
set_global(type_id, cfg);
}

pub(crate) fn set_global(type_id: TypeId, cfg: DynConfig) {
global_state::set(type_id, cfg)
}

pub(crate) fn exists(type_id: TypeId) -> bool {
#[cfg(feature = "thread-local")]
{
let exists_local = thread_local_state::exists(type_id);
let exists_global = global_state::exists(type_id);
exists_local || exists_global
}

#[cfg(not(feature = "thread-local"))]
{
global_state::exists(type_id)
}
}

pub(crate) fn get(type_id: TypeId) -> Option<DynConfig> {
#[cfg(feature = "thread-local")]
{
thread_local_state::get(type_id).or_else(|| global_state::get(type_id))
}

#[cfg(not(feature = "thread-local"))]
global_state::get(type_id)
}
14 changes: 14 additions & 0 deletions lnx-config/tests/config_access.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
struct ExampleConfig {
example: i32,
}

#[test]
fn test_config_access() {
let config = ExampleConfig { example: 123 };

lnx_config::init(config).unwrap();

let value = lnx_config::get!(ExampleConfig.example);
println!("Got {}", value);

Check warning on line 12 in lnx-config/tests/config_access.rs

View workflow job for this annotation

GitHub Actions / clippy

variables can be used directly in the `format!` string

warning: variables can be used directly in the `format!` string --> lnx-config/tests/config_access.rs:12:5 | 12 | println!("Got {}", value); | ^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args = note: `#[warn(clippy::uninlined_format_args)]` on by default help: change this to | 12 - println!("Got {}", value); 12 + println!("Got {value}"); |
assert_eq!(value, 123);
}
Loading