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}