Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -82,15 +87,13 @@ fn default_forwarded_headers() -> Vec<String> {

/// Upstream gRPC service configuration.
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct UpstreamConfig {
/// gRPC upstream address (e.g., "http://localhost:4180").
pub default: String,
}

/// Descriptor loading source.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum DescriptorSource {
/// Pre-compiled descriptor file.
File { file: PathBuf },
Expand Down Expand Up @@ -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")]
Expand All @@ -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")]
Expand Down Expand Up @@ -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<Self> {
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<Self> {
Ok(serde_yaml::from_str(yaml)?)
}

/// Parse rate string like "20/min" → requests per window.
Expand Down
64 changes: 64 additions & 0 deletions tests/embedded.rs
Original file line number Diff line number Diff line change
@@ -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()],
Comment thread
polaz marked this conversation as resolved.
};
// 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");
}