diff --git a/src/config.rs b/src/config.rs index c60ab81..9f31004 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,8 +7,13 @@ use serde::Deserialize; use std::path::PathBuf; /// Top-level proxy configuration (loaded from YAML). +/// +/// This and the wiring structs below (`UpstreamConfig`, `ListenConfig`, +/// `ServiceConfig`, `DescriptorSource`) are intentionally NOT +/// `#[non_exhaustive]`: embedding consumers build them programmatically with +/// runtime values. The leaf auth/shield/oidc config structs are +/// `#[non_exhaustive]` instead, since those are deserialized, not hand-built. #[derive(Debug, Clone, Deserialize)] -#[non_exhaustive] pub struct ProxyConfig { /// Upstream gRPC service(s). pub upstream: UpstreamConfig, @@ -82,7 +87,6 @@ fn default_forwarded_headers() -> Vec { /// Upstream gRPC service configuration. #[derive(Debug, Clone, Deserialize)] -#[non_exhaustive] pub struct UpstreamConfig { /// gRPC upstream address (e.g., "http://localhost:4180"). pub default: String, @@ -90,7 +94,6 @@ pub struct UpstreamConfig { /// Descriptor loading source. #[derive(Debug, Clone)] -#[non_exhaustive] pub enum DescriptorSource { /// Pre-compiled descriptor file. File { file: PathBuf }, @@ -131,7 +134,6 @@ where /// Listen address configuration. #[derive(Debug, Clone, Deserialize)] -#[non_exhaustive] pub struct ListenConfig { /// HTTP listen address (default: "0.0.0.0:8080"). #[serde(default = "default_http_listen")] @@ -152,7 +154,6 @@ impl Default for ListenConfig { /// Service identity. #[derive(Debug, Clone, Deserialize)] -#[non_exhaustive] pub struct ServiceConfig { /// Service name (appears in /health response and metrics namespace). #[serde(default = "default_service_name")] @@ -500,9 +501,15 @@ pub struct MetricsClassConfig { impl ProxyConfig { /// Load configuration from a YAML file. pub fn from_file(path: &std::path::Path) -> anyhow::Result { - let content = std::fs::read_to_string(path)?; - let config: Self = serde_yaml::from_str(&content)?; - Ok(config) + Self::from_yaml_str(&std::fs::read_to_string(path)?) + } + + /// Parse configuration from a YAML string. + /// + /// Useful for embedding the proxy: load a baked-in config (e.g. via + /// `include_str!`) without touching the filesystem. + pub fn from_yaml_str(yaml: &str) -> anyhow::Result { + Ok(serde_yaml::from_str(yaml)?) } /// Parse rate string like "20/min" → requests per window. diff --git a/tests/embedded.rs b/tests/embedded.rs new file mode 100644 index 0000000..0f88510 --- /dev/null +++ b/tests/embedded.rs @@ -0,0 +1,64 @@ +//! Guards the embedded-construction contract. +//! +//! Downstream products embed the proxy by building a [`ProxyConfig`] +//! programmatically (runtime upstream port, listen address, baked-in +//! descriptors) rather than loading a YAML file. This test lives in a separate +//! crate, so it sees the config types exactly as an external consumer does: if +//! any wiring struct gains `#[non_exhaustive]`, this stops compiling and the +//! embedded API regression is caught here. + +use structured_proxy::config::{ + DescriptorSource, ListenConfig, ProxyConfig, ServiceConfig, UpstreamConfig, +}; +use structured_proxy::ProxyServer; + +#[test] +fn embedded_config_is_constructible() { + static DESCRIPTOR_BYTES: &[u8] = &[]; + let config = ProxyConfig { + upstream: UpstreamConfig { + default: "http://127.0.0.1:50051".into(), + }, + descriptors: vec![DescriptorSource::Embedded { + bytes: DESCRIPTOR_BYTES, + }], + listen: ListenConfig { + http: "0.0.0.0:8080".into(), + }, + service: ServiceConfig { + name: "embedded-test".into(), + }, + aliases: vec![], + openapi: None, + auth: None, + shield: None, + oidc_discovery: None, + maintenance: Default::default(), + cors: Default::default(), + logging: Default::default(), + metrics_classes: vec![], + // Arbitrary: this test only exercises that the config is constructible, + // not header forwarding. NOTE for real embeddings: a programmatic literal + // bypasses the serde defaults, so set every header you need here (or load + // the config via from_file / from_yaml_str, where the default list applies). + forwarded_headers: vec!["authorization".into()], + }; + // The server accepts a programmatically-built config (the embedded path). + let _server = ProxyServer::from_config(config); +} + +#[test] +fn from_yaml_str_loads_config() { + let config = ProxyConfig::from_yaml_str( + r#" +upstream: + default: "http://127.0.0.1:50051" +descriptors: + - file: "/x.bin" +service: + name: "yaml-test" +"#, + ) + .unwrap(); + assert_eq!(config.service.name, "yaml-test"); +}