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}