Changed config to a singleton pattern, updated state and config trait by adding resolver variant.

Updated README to reflect current structure approach.
This commit is contained in:
2026-02-22 15:17:35 +01:00
parent 854215bc45
commit 809b7c81a4
4 changed files with 102 additions and 14 deletions

View File

@@ -19,11 +19,14 @@ assets/
generic/ generic/
intro/ intro/
Intro_MrNewVegas_N.mp3 Intro_MrNewVegas_N.mp3
shared/
Story_MrNewVegas_N.mp3
news/ news/
story_N/ story_N/
story.toml (ignored, metadata) story.toml (optional, ignored)
Story_MrNewVegas_N.mp3 Story_MrNewVegas_N.mp3
Story_Guest_N.mp3 Story_Guest_N.mp3
Story_MrNewVegas_N.mp3 -> symlink shared/Story_MrNewVegas_N.mp3
.cache/ (automatically generated) .cache/ (automatically generated)
songs/ songs/
my_song/ my_song/
@@ -32,11 +35,16 @@ assets/
cache.json cache.json
generic/ generic/
intro/ intro/
Intro_MrNewVegas_N.pcm (f32 pcm) intro_N.pcm (f32 pcm)
Intro_MrNewVegas_N.cache.json intro_N.cache.json
shared/
story_N.pcm (f32 pcm)
story_N.cache.json
news/ news/
story_N/ story_N/
part_N.pcm (f32 pcm) part_N.pcm (f32 pcm)
part_N.pcm (f32 pcm)
part_N.pcm (f32 pcm) -> symlink shared/story_N.pcm
cache.json cache.json
``` ```

View File

@@ -1,4 +1,8 @@
use std::{ env, path::PathBuf }; use std::{
env,
sync::{ Arc, LazyLock },
path::{ Path, PathBuf, Component },
};
use serde::{ Serialize, Deserialize }; use serde::{ Serialize, Deserialize };
use crate::app::parser::FromConfig; use crate::app::parser::FromConfig;
@@ -12,9 +16,33 @@ pub struct Config {
// //
static CFG: LazyLock<Arc<Config>> = LazyLock::new(|| {
Arc::new(<Config as FromConfig>::load())
});
impl Config impl Config
{ {
pub fn load() -> Self { FromConfig::load() } /// Arc to a Singleton Instance of Config.
pub fn load() -> Arc<Self> { CFG.clone() }
fn resolve(&mut self, root: impl AsRef<Path>)
{
let d = &mut self.defaults;
let r = root.as_ref();
d.songs_directory = Self::normalize(r.join(&d.songs_directory));
d.generic_directory = Self::normalize(r.join(&d.generic_directory));
d.cache_directory = Self::normalize(r.join(&d.cache_directory));
d.log_directory = Self::normalize(r.join(&d.log_directory));
d.temp_directory = Self::normalize(r.join(&d.temp_directory));
}
fn normalize(path: impl AsRef<Path>) -> PathBuf
{
path.as_ref().components().fold(PathBuf::new(), |mut acc, c| {
match c { Component::ParentDir => { acc.pop(); acc }, _ => { acc.push(c); acc } }
})
}
} }
impl FromConfig for Config impl FromConfig for Config
@@ -37,13 +65,14 @@ impl FromConfig for Config
.map(|p: PathBuf| root.join(p)) .map(|p: PathBuf| root.join(p))
.unwrap(); .unwrap();
Self::deserialize_config( Self::deserialize_config_with_resolver(
assets_directory.join("config"), assets_directory.join("config"),
&[ &[
("ron", Self::from_ron), ("ron", Self::from_ron),
("toml", Self::from_toml), ("toml", Self::from_toml),
("json", Self::from_json), ("json", Self::from_json),
], ],
|cfg: &mut Self| cfg.resolve(&assets_directory)
).expect("Failed to deserialize config.") ).expect("Failed to deserialize config.")
} }
} }
@@ -52,11 +81,12 @@ impl FromConfig for Config
#[derive(Clone, Deserialize, Serialize, Debug)] #[derive(Clone, Deserialize, Serialize, Debug)]
pub struct Defaults { pub struct Defaults {
pub songs_directory: String, pub songs_directory: PathBuf,
pub generic_directory: String, pub generic_directory: PathBuf,
pub cache_directory: PathBuf,
pub log_directory: String, pub log_directory: PathBuf,
pub temp_directory: String, pub temp_directory: PathBuf,
} }
#[derive(Clone, Deserialize, Serialize, Debug)] #[derive(Clone, Deserialize, Serialize, Debug)]

View File

@@ -6,7 +6,8 @@ use thiserror::Error;
/// ///
/// Implementors must define [`load`](FromConfig::load), which is responsible for /// Implementors must define [`load`](FromConfig::load), which is responsible for
/// resolving the config path and calling [`deserialize_config`](FromConfig::deserialize_config). /// resolving the config path and calling [`deserialize_config`](FromConfig::deserialize_config).
/// All parsing methods have default implementations supporting RON, TOML, and JSON, /// All parsing methods have default implementations supporting [`RON`](FromConfig::from_ron),
/// [`TOML`](FromConfig::from_toml), and [`JSON`](FromConfig::from_json),
/// and can be overridden if custom parsing behaviour is needed. /// and can be overridden if custom parsing behaviour is needed.
pub trait FromConfig pub trait FromConfig
{ {
@@ -18,10 +19,14 @@ pub trait FromConfig
/// Deserializes a config file into a rust struct. /// Deserializes a config file into a rust struct.
/// ///
/// Returns [`ConfigError::NotFound`] if no file matching any of the provided extensions
/// exists, [`ConfigError::Io`] if the file cannot be read, or the appropriate parse
/// error variant if deserialisation fails.
///
/// Example: /// Example:
/// ``` /// ```
/// let cfg: Config = Config::deserialize_config( /// let cfg: Config = Config::deserialize_config(
/// "path/to/config", /// "path/to/config/file",
/// &[ /// &[
/// ("ron", Config::from_ron), /// ("ron", Config::from_ron),
/// ("toml", Config::from_toml), /// ("toml", Config::from_toml),
@@ -49,6 +54,50 @@ pub trait FromConfig
Err(ConfigError::NotFound { path: file.as_ref().to_string_lossy().into() }) Err(ConfigError::NotFound { path: file.as_ref().to_string_lossy().into() })
} }
/// Deserializes a config file into a Rust struct, then applies a post-processing
/// resolver before returning.
///
/// This is identical to [`deserialize_config`](FromConfig::deserialize_config), except
/// that after a successful parse, `resolver` is called with a mutable reference to the
/// deserialized value. Use it to fill in derived fields, apply default overrides, or
/// validate and normalise values that cannot be expressed in the raw config format.
///
/// Example:
/// ```
/// let cfg: Config = Config::deserialize_config(
/// "path/to/config/file",
/// &[
/// ("ron", Config::from_ron),
/// ("toml", Config::from_toml),
/// ("json", Config::from_json),
/// ],
/// |cfg: &mut Self| cfg.resolve(&assets_directory)
/// )?;
/// ```
fn deserialize_config_with_resolver<T>(file: impl AsRef<Path>, formats: &[(&str, ParseFn<T>)], resolver: impl Fn(&mut T)) -> Result<T, ConfigError>
where
T: for<'de> Deserialize<'de>,
{
let base = file.as_ref();
for (ext, parse) in formats
{
let path = base.with_extension(ext);
if path.exists()
{
let raw = std::fs::read_to_string(&path)
.map_err(ConfigError::Io)?;
let mut cfg = parse(&raw)?;
resolver(&mut cfg);
return Ok(cfg);
}
}
Err(ConfigError::NotFound { path: file.as_ref().to_string_lossy().into() })
}
fn from_ron<T: for<'de> Deserialize<'de>>(s: &str) -> Result<T, ConfigError> { fn from_ron<T: for<'de> Deserialize<'de>>(s: &str) -> Result<T, ConfigError> {
ron::from_str(s).map_err(ConfigError::Ron) ron::from_str(s).map_err(ConfigError::Ron)
} }

View File

@@ -1,3 +1,4 @@
use std::sync::Arc;
use tracing_subscriber::{ use tracing_subscriber::{
EnvFilter, EnvFilter,
fmt::{ fmt::{
@@ -12,13 +13,13 @@ use crate::app::{
}; };
pub struct AppState { pub struct AppState {
pub config: Config, pub config: Arc<Config>,
pub scheduler: Scheduler pub scheduler: Scheduler
} }
impl AppState impl AppState
{ {
pub fn new(config: Config, scheduler: Scheduler) -> Self pub fn new(config: Arc<Config>, scheduler: Scheduler) -> Self
{ {
Self { config, scheduler } Self { config, scheduler }
} }