added initial structure and dependencies

This commit is contained in:
2026-02-22 00:08:57 +01:00
parent 87db938735
commit 854215bc45
9 changed files with 242 additions and 1 deletions

View File

@@ -4,3 +4,11 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
dotenvy = "^0"
ron = "^0"
toml = "^1"
serde_json = "^1"
serde = { version = "^1", features = ["derive"] }
thiserror = "^2"
tracing-subscriber = { version = "^0", features = ["env-filter"] }

99
src/app/config.rs Normal file
View File

@@ -0,0 +1,99 @@
use std::{ env, path::PathBuf };
use serde::{ Serialize, Deserialize };
use crate::app::parser::FromConfig;
#[derive(Clone, Deserialize, Serialize, Debug)]
pub struct Config {
pub defaults: Defaults,
pub settings: Settings,
pub rules: Rules,
}
//
impl Config
{
pub fn load() -> Self { FromConfig::load() }
}
impl FromConfig for Config
{
fn load() -> Self
{
let root = env::current_exe()
.expect("Unable to locate executable.").parent()
.expect("Unable to locate root directory.").to_owned();
// Removed expectation for dockerized-environments.
dotenvy::dotenv()
.map(|_| ())
.or_else(|_| dotenvy::from_path(root.join(".env")))
.ok(); //.expect("Unable to load .env file.");
let assets_directory: PathBuf = env::var("RNV_ASSETS_DIRECTORY")
.ok()
.and_then(|v| v.parse().ok())
.map(|p: PathBuf| root.join(p))
.unwrap();
Self::deserialize_config(
assets_directory.join("config"),
&[
("ron", Self::from_ron),
("toml", Self::from_toml),
("json", Self::from_json),
],
).expect("Failed to deserialize config.")
}
}
//
#[derive(Clone, Deserialize, Serialize, Debug)]
pub struct Defaults {
pub songs_directory: String,
pub generic_directory: String,
pub log_directory: String,
pub temp_directory: String,
}
#[derive(Clone, Deserialize, Serialize, Debug)]
pub struct Settings {
pub debug: bool,
pub weight: f64,
pub volume: f64,
pub crossfade: f64,
pub weights: Weights,
}
#[derive(Clone, Deserialize, Serialize, Debug)]
pub struct Weights {
pub news: NewsWeight,
pub genre: RecoveringWeight,
pub composite: RecoveringWeight,
}
#[derive(Clone, Deserialize, Serialize, Debug)]
pub struct NewsWeight {
pub chance: f64,
}
#[derive(Clone, Deserialize, Serialize, Debug)]
pub struct RecoveringWeight {
pub floor: f64,
pub recover_after: f64,
}
#[derive(Clone, Deserialize, Serialize, Debug)]
pub struct Rules {
pub shuffle_mode: ShuffleMode,
}
#[derive(Clone, Deserialize, Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum ShuffleMode {
Weighted,
StrictRandom,
}

7
src/app/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
pub mod state;
pub mod models;
pub mod config;
mod parser;
pub use parser::FromConfig;

2
src/app/models.rs Normal file
View File

@@ -0,0 +1,2 @@
pub struct Scheduler {}

3
src/app/parser/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
mod parser;
pub use parser::FromConfig;

81
src/app/parser/parser.rs Normal file
View File

@@ -0,0 +1,81 @@
use std::path::Path;
use serde::Deserialize;
use thiserror::Error;
/// A trait for types that can be loaded from a configuration file.
///
/// 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,
/// and can be overridden if custom parsing behaviour is needed.
pub trait FromConfig
{
/// Loads and deserializes the config into `Self`.
///
/// Implementations should resolve the config file path and call
/// [`deserialize_config`](FromConfig::deserialize_config) with the desired formats.
fn load() -> Self;
/// Deserializes a config file into a rust struct.
///
/// Example:
/// ```
/// let cfg: Config = Config::deserialize_config(
/// "path/to/config",
/// &[
/// ("ron", Config::from_ron),
/// ("toml", Config::from_toml),
/// ("json", Config::from_json),
/// ],
/// )?;
/// ```
fn deserialize_config<T>(file: impl AsRef<Path>, formats: &[(&str, ParseFn<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)?;
return parse(&raw);
}
}
Err(ConfigError::NotFound { path: file.as_ref().to_string_lossy().into() })
}
fn from_ron<T: for<'de> Deserialize<'de>>(s: &str) -> Result<T, ConfigError> {
ron::from_str(s).map_err(ConfigError::Ron)
}
fn from_toml<T: for<'de> Deserialize<'de>>(s: &str) -> Result<T, ConfigError> {
toml::from_str(s).map_err(ConfigError::Toml)
}
fn from_json<T: for<'de> Deserialize<'de>>(s: &str) -> Result<T, ConfigError> {
serde_json::from_str(s).map_err(ConfigError::Json)
}
}
//
pub type ParseFn<T> = fn(&str) -> Result<T, ConfigError>;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("No config file found at '{path}.(ron|toml|json)'.")]
NotFound { path: String },
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Ron parse error: {0}")]
Ron(#[from] ron::error::SpannedError),
#[error("TOML parse error: {0}")]
Toml(#[from] toml::de::Error),
#[error("JSON parse error: {0}")]
Json(#[from] serde_json::Error),
}

29
src/app/state.rs Normal file
View File

@@ -0,0 +1,29 @@
use tracing_subscriber::{
EnvFilter,
fmt::{
SubscriberBuilder,
format::{ Format, DefaultFields }
}
};
use crate::app::{
models::*,
config::Config
};
pub struct AppState {
pub config: Config,
pub scheduler: Scheduler
}
impl AppState
{
pub fn new(config: Config, scheduler: Scheduler) -> Self
{
Self { config, scheduler }
}
pub fn get_tracer(filter: impl Into<EnvFilter>) -> SubscriberBuilder<DefaultFields, Format, EnvFilter> {
tracing_subscriber::fmt().with_env_filter(filter)
}
}

3
src/lib.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod app;
pub use app::{ config, state };

View File

@@ -1,4 +1,13 @@
use new_vegas_radio::app::{ config::Config, state::AppState, models::Scheduler };
fn main() { fn main()
{
let cfg = Config::load();
let _app = AppState::new(cfg.clone(), Scheduler {});
if cfg.settings.debug {
AppState::get_tracer("new_vegas_radio=debug").init();
}
println!("{:?}", cfg);
} }