use crate::{config::Cluster, errors::ProxyError};
const MAX_IDENTIFIER_LEN: usize = 64;
pub(super) fn validate_application_metadata(cluster: &Cluster) -> Result<(), ProxyError> {
if let Some(protocol) = cluster.http.application_protocol.as_deref() {
validate_identifier(protocol, "application_protocol", &cluster.name)?;
}
if let Some(provider) = cluster.http.application_provider.as_deref() {
validate_identifier(provider, "application_provider", &cluster.name)?;
}
Ok(())
}
fn validate_identifier(value: &str, field: &str, cluster_name: &str) -> Result<(), ProxyError> {
if value.is_empty() {
return Err(ProxyError::Config(format!(
"cluster '{cluster_name}': {field} must not be empty"
)));
}
if value.len() > MAX_IDENTIFIER_LEN {
return Err(ProxyError::Config(format!(
"cluster '{cluster_name}': {field} {value:?} exceeds {MAX_IDENTIFIER_LEN} bytes"
)));
}
let byte_allowed = |b: u8| b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'.' | b'_' | b'-');
if !value.bytes().all(byte_allowed) {
return Err(ProxyError::Config(format!(
"cluster '{cluster_name}': {field} {value:?} must use only lowercase ASCII \
letters, digits, '.', '_', or '-'"
)));
}
let alnum_boundary = |b: Option<&u8>| b.is_some_and(|&b| b.is_ascii_lowercase() || b.is_ascii_digit());
if !alnum_boundary(value.as_bytes().first()) || !alnum_boundary(value.as_bytes().last()) {
return Err(ProxyError::Config(format!(
"cluster '{cluster_name}': {field} {value:?} must start and end with a letter or digit"
)));
}
Ok(())
}
#[cfg(test)]
#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
reason = "tests use unwrap/expect/panic for brevity"
)]
mod tests {
use std::sync::Arc;
use super::*;
#[test]
fn accept_canonical_identifiers() {
ok("openai_chat_completions");
ok("openai_responses");
ok("openai");
ok("vllm");
ok("gpt-4.1");
ok("a");
ok("1");
}
#[test]
fn accept_at_max_length() {
let value = "a".repeat(MAX_IDENTIFIER_LEN);
assert_eq!(value.len(), 64, "test value should exercise the boundary");
ok(&value);
}
#[test]
fn both_fields_absent_is_ok() {
let cluster = Cluster::with_defaults("test", vec!["10.0.0.1:80".into()]);
validate_application_metadata(&cluster).expect("absent metadata should be accepted");
}
#[test]
fn reject_empty() {
assert!(err("").contains("must not be empty"), "got: {}", err(""));
}
#[test]
fn reject_over_max_length() {
let value = "a".repeat(MAX_IDENTIFIER_LEN + 1);
assert!(err(&value).contains("exceeds 64 bytes"), "got: {}", err(&value));
}
#[test]
fn reject_disallowed_characters() {
assert!(err("OpenAI").contains("lowercase ASCII"), "uppercase");
assert!(err("open ai").contains("lowercase ASCII"), "space");
assert!(err("openai!").contains("lowercase ASCII"), "punctuation");
assert!(err("openai/v1").contains("lowercase ASCII"), "slash");
}
#[test]
fn reject_non_ascii_identifiers() {
assert!(
err("café").contains("lowercase ASCII"),
"byte-wise check must reject accented latin as non-lowercase-ASCII"
);
assert!(
err("模型").contains("lowercase ASCII"),
"byte-wise check must reject multi-byte CJK as non-lowercase-ASCII"
);
assert!(
err("openai🚀").contains("lowercase ASCII"),
"byte-wise check must reject an emoji as non-lowercase-ASCII"
);
}
#[test]
fn reject_non_alphanumeric_boundaries() {
assert!(err("_openai").contains("must start and end"), "leading underscore");
assert!(err("openai_").contains("must start and end"), "trailing underscore");
assert!(err(".openai").contains("must start and end"), "leading dot");
assert!(err("openai-").contains("must start and end"), "trailing dash");
assert!(err("-").contains("must start and end"), "single symbol");
}
#[test]
fn error_names_the_offending_field() {
let protocol_err = validate_application_metadata(&with_protocol("Bad"))
.unwrap_err()
.to_string();
assert!(protocol_err.contains("application_protocol"), "got: {protocol_err}");
let provider_err = validate_application_metadata(&with_provider("Bad"))
.unwrap_err()
.to_string();
assert!(provider_err.contains("application_provider"), "got: {provider_err}");
}
#[test]
fn error_names_the_cluster() {
let err = validate_application_metadata(&with_protocol("Bad"))
.unwrap_err()
.to_string();
assert!(err.contains("test"), "error should name the cluster: {err}");
}
fn with_protocol(value: &str) -> Cluster {
let mut cluster = Cluster::with_defaults("test", vec!["10.0.0.1:80".into()]);
cluster.http.application_protocol = Some(Arc::from(value));
cluster
}
fn with_provider(value: &str) -> Cluster {
let mut cluster = Cluster::with_defaults("test", vec!["10.0.0.1:80".into()]);
cluster.http.application_provider = Some(Arc::from(value));
cluster
}
fn ok(value: &str) {
validate_application_metadata(&with_protocol(value))
.unwrap_or_else(|e| panic!("expected Ok for protocol {value:?}, got: {e}"));
validate_application_metadata(&with_provider(value))
.unwrap_or_else(|e| panic!("expected Ok for provider {value:?}, got: {e}"));
}
fn err(value: &str) -> String {
validate_application_metadata(&with_protocol(value))
.unwrap_err()
.to_string()
}
}