Skip to main content

voxora_config/
lib.rs

1#![forbid(unsafe_code)]
2#![warn(missing_docs)]
3
4//! Single source of truth for every voxora runtime setting.
5//!
6//! Replaces the ad-hoc cascade currently duplicated across
7//! `voxora-cli/src/args.rs::resolve_hf_cache_dir` and
8//! `voxora-hf/src/cache.rs::default_cache_root`.
9//!
10//! Settings come from three layers — built-in defaults, an optional
11//! TOML file, and the environment — and every getter resolves them in
12//! the same order, first non-empty wins:
13//!
14//!   1. An explicit value on the config itself: either set by the
15//!      caller ([`CacheConfig::root`], [`HfConfig::token`], …) or
16//!      loaded from a TOML file via [`VoxoraConfig::from_file`].
17//!   2. The matching `VOXORA_*` environment variable, listed in
18//!      [`mod@env`] (plus the `HF_TOKEN` / `HUGGING_FACE_HUB_TOKEN`
19//!      aliases for the token).
20//!   3. The built-in default (`VoxoraConfig::default`).
21//!
22//! ASR-specific: this crate only models configuration concerns that
23//! exist in the voxora speech-recognition stack (cache dir, HF token,
24//! HF base URL, default revision). It does not model LLM / vision /
25//! multimodal configuration.
26//!
27//! # Example
28//!
29//! ```no_run
30//! use voxora_config::VoxoraConfig;
31//!
32//! # fn run() -> Result<(), voxora_config::ConfigError> {
33//! let cfg = VoxoraConfig::from_file(std::path::Path::new("voxora.toml"))?;
34//! println!("cache root: {}", cfg.cache_root().display());
35//! println!("hub: {}", cfg.hf_base_url());
36//! # Ok(()) }
37//! ```
38
39pub mod env;
40pub mod file;
41
42mod cache;
43mod error;
44mod hf;
45
46pub use cache::CacheConfig;
47pub use error::ConfigError;
48pub use hf::HfConfig;
49
50use serde::{Deserialize, Serialize};
51
52/// Top-level voxora configuration.
53#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
54#[serde(default, deny_unknown_fields)]
55#[non_exhaustive]
56pub struct VoxoraConfig {
57    /// Cache directory configuration.
58    pub cache: CacheConfig,
59    /// Hugging Face source configuration.
60    pub hf: HfConfig,
61}
62
63impl VoxoraConfig {
64    /// Build a [`VoxoraConfig`] from its two sections. Provided because
65    /// the type is `#[non_exhaustive]` and cannot be built with a
66    /// struct expression from outside this crate.
67    pub fn new(cache: CacheConfig, hf: HfConfig) -> Self {
68        Self { cache, hf }
69    }
70
71    /// Resolve the cache directory honouring the cascade.
72    pub fn cache_root(&self) -> std::path::PathBuf {
73        self.cache.resolve()
74    }
75
76    /// Resolve the HF bearer token honouring the cascade.
77    pub fn hf_token(&self) -> Option<String> {
78        self.hf.token()
79    }
80
81    /// Resolve the HF base URL honouring the cascade.
82    pub fn hf_base_url(&self) -> String {
83        self.hf.base_url()
84    }
85
86    /// Resolve the default HF revision honouring the cascade.
87    pub fn hf_default_revision(&self) -> String {
88        self.hf.default_revision()
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn defaults_roundtrip_via_toml() {
98        let cfg = VoxoraConfig::default();
99        let text = toml::to_string(&cfg).expect("toml::to_string");
100        let parsed = VoxoraConfig::from_str(&text, std::path::Path::new("inline")).expect("parse");
101        assert_eq!(cfg, parsed);
102    }
103
104    #[test]
105    fn unknown_field_is_rejected() {
106        let bad = r#"
107            cache.root = "/tmp/cache"
108            hf.token = "abc"
109            bogus_field = "should-error"
110        "#;
111        let err = VoxoraConfig::from_str(bad, std::path::Path::new("inline"))
112            .expect_err("deny_unknown_fields");
113        assert!(matches!(err, ConfigError::FileParse { .. }));
114    }
115
116    #[test]
117    fn cache_root_matches_inner_resolve() {
118        let cfg = VoxoraConfig::default();
119        assert_eq!(cfg.cache_root(), cfg.cache.resolve());
120    }
121
122    #[test]
123    fn hf_token_propagates_from_inner() {
124        let cfg = VoxoraConfig {
125            hf: HfConfig {
126                token: Some("xyz".into()),
127                ..HfConfig::default()
128            },
129            ..VoxoraConfig::default()
130        };
131        assert_eq!(cfg.hf_token().as_deref(), Some("xyz"));
132    }
133
134    #[test]
135    fn new_matches_struct_expression() {
136        let cache = CacheConfig::new(Some(std::path::PathBuf::from("/tmp/c")));
137        let hf = HfConfig::new(Some("t".into()), None, None);
138        assert_eq!(
139            VoxoraConfig::new(cache.clone(), hf.clone()),
140            VoxoraConfig { cache, hf }
141        );
142    }
143}