dwara_core/extensions/config_source.rs
1//! Configuration source extension point.
2//!
3//! # Contract: [`ConfigSource`]
4//!
5//! **Purpose:** produce the current [`Gateway`] configuration on demand.
6//!
7//! **Semantics:** `load` is a full read of the current configuration
8//! generation. It may be called on startup, on demand, and (once DW-006
9//! lands) whenever the source signals a change. Implementations should be
10//! cheap to call repeatedly but need not cache. `load` is not expected to
11//! run on the request hot path.
12//!
13//! **Watch contract (documented, NOT implemented here — DW-006):** an
14//! OSS file-watch source will notify the runtime on file change; the trait
15//! deliberately keeps `load` pull-only so watch mechanics (tokio watch
16//! channel, notify crate) can be layered by the consumer without a
17//! signature change. Future revisions may add an optional `subscribe`
18//! method with a default no-op implementation, which is backward compatible.
19//!
20//! **Failure model:** unreadable source maps to [`ExtensionsError::Io`];
21//! malformed configuration maps to [`ExtensionsError::Invalid`] (wrapping
22//! the path-precise [`crate::config::ConfigError`] message). No retries.
23//!
24//! **Editions:** OSS ships [`FileConfigSource`] (single YAML file).
25//! Additional remote/control-plane sources may be provided separately in
26//! future editions.
27
28use async_trait::async_trait;
29
30use crate::config::includes;
31use crate::config::{parse_gateway, Gateway};
32
33use super::ExtensionsError;
34
35/// Swappable configuration origin.
36#[async_trait]
37pub trait ConfigSource: Send + Sync {
38 /// Load the current configuration generation.
39 async fn load(&self) -> Result<Gateway, ExtensionsError>;
40}
41
42/// Single-file YAML configuration source (OSS).
43#[derive(Debug, Clone)]
44pub struct FileConfigSource {
45 path: std::path::PathBuf,
46}
47
48impl FileConfigSource {
49 /// Source reading configuration from `path` on each [`ConfigSource::load`].
50 pub fn new(path: impl Into<std::path::PathBuf>) -> Self {
51 Self { path: path.into() }
52 }
53}
54
55#[async_trait]
56impl ConfigSource for FileConfigSource {
57 async fn load(&self) -> Result<Gateway, ExtensionsError> {
58 // The io site keeps the file path (context the blanket
59 // From<std::io::Error> cannot provide); the config site uses the
60 // From<ConfigError> conversion (#128) — ConfigError's Display is
61 // already path-precise.
62 let text = std::fs::read_to_string(&self.path)
63 .map_err(|e| ExtensionsError::Io(format!("{}: {e}", self.path.display())))?;
64 // CFG-01 (#180): resolve `includes:` directives and apply the
65 // selected profile (from DWARA_PROFILE) before parsing. The
66 // base_dir is the config file's parent (relative include/profile
67 // paths resolve against it).
68 let base_dir = self
69 .path
70 .parent()
71 .unwrap_or(std::path::Path::new("."))
72 .to_path_buf();
73 let text =
74 includes::preprocess(&text, &base_dir, None).map_err(ExtensionsError::Invalid)?;
75 parse_gateway(&text).map_err(ExtensionsError::from)
76 }
77}