diff --git a/README.md b/README.md index e52e667..596c8b3 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,14 @@ assets/ generic/ intro/ Intro_MrNewVegas_N.mp3 + shared/ + Story_MrNewVegas_N.mp3 news/ story_N/ - story.toml (ignored, metadata) + story.toml (optional, ignored) Story_MrNewVegas_N.mp3 Story_Guest_N.mp3 + Story_MrNewVegas_N.mp3 -> symlink shared/Story_MrNewVegas_N.mp3 .cache/ (automatically generated) songs/ my_song/ @@ -32,11 +35,16 @@ assets/ cache.json generic/ intro/ - Intro_MrNewVegas_N.pcm (f32 pcm) - Intro_MrNewVegas_N.cache.json + intro_N.pcm (f32 pcm) + intro_N.cache.json + shared/ + story_N.pcm (f32 pcm) + story_N.cache.json news/ story_N/ part_N.pcm (f32 pcm) + part_N.pcm (f32 pcm) + part_N.pcm (f32 pcm) -> symlink shared/story_N.pcm cache.json ``` diff --git a/src/app/config.rs b/src/app/config.rs index 22e160e..3be7b76 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -1,4 +1,8 @@ -use std::{ env, path::PathBuf }; +use std::{ + env, + sync::{ Arc, LazyLock }, + path::{ Path, PathBuf, Component }, +}; use serde::{ Serialize, Deserialize }; use crate::app::parser::FromConfig; @@ -12,9 +16,33 @@ pub struct Config { // +static CFG: LazyLock> = LazyLock::new(|| { + Arc::new(::load()) +}); + impl Config { - pub fn load() -> Self { FromConfig::load() } + /// Arc to a Singleton Instance of Config. + pub fn load() -> Arc { CFG.clone() } + + fn resolve(&mut self, root: impl AsRef) + { + 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) -> 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 @@ -37,13 +65,14 @@ impl FromConfig for Config .map(|p: PathBuf| root.join(p)) .unwrap(); - Self::deserialize_config( + Self::deserialize_config_with_resolver( assets_directory.join("config"), &[ ("ron", Self::from_ron), ("toml", Self::from_toml), ("json", Self::from_json), ], + |cfg: &mut Self| cfg.resolve(&assets_directory) ).expect("Failed to deserialize config.") } } @@ -52,11 +81,12 @@ impl FromConfig for Config #[derive(Clone, Deserialize, Serialize, Debug)] pub struct Defaults { - pub songs_directory: String, - pub generic_directory: String, + pub songs_directory: PathBuf, + pub generic_directory: PathBuf, + pub cache_directory: PathBuf, - pub log_directory: String, - pub temp_directory: String, + pub log_directory: PathBuf, + pub temp_directory: PathBuf, } #[derive(Clone, Deserialize, Serialize, Debug)] diff --git a/src/app/parser/parser.rs b/src/app/parser/parser.rs index c85fdb7..3d91fe0 100644 --- a/src/app/parser/parser.rs +++ b/src/app/parser/parser.rs @@ -6,7 +6,8 @@ use thiserror::Error; /// /// Implementors must define [`load`](FromConfig::load), which is responsible for /// 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. pub trait FromConfig { @@ -18,10 +19,14 @@ pub trait FromConfig /// 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: /// ``` /// let cfg: Config = Config::deserialize_config( - /// "path/to/config", + /// "path/to/config/file", /// &[ /// ("ron", Config::from_ron), /// ("toml", Config::from_toml), @@ -49,6 +54,50 @@ pub trait FromConfig 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(file: impl AsRef, formats: &[(&str, ParseFn)], resolver: impl Fn(&mut T)) -> Result + 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 Deserialize<'de>>(s: &str) -> Result { ron::from_str(s).map_err(ConfigError::Ron) } diff --git a/src/app/state.rs b/src/app/state.rs index 7f56683..d942e1d 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -1,3 +1,4 @@ +use std::sync::Arc; use tracing_subscriber::{ EnvFilter, fmt::{ @@ -12,13 +13,13 @@ use crate::app::{ }; pub struct AppState { - pub config: Config, + pub config: Arc, pub scheduler: Scheduler } impl AppState { - pub fn new(config: Config, scheduler: Scheduler) -> Self + pub fn new(config: Arc, scheduler: Scheduler) -> Self { Self { config, scheduler } }