use std::fmt;
use crate::options::{CorrelationId, UserAgentSuffix, WorkloadId};
const MAX_USER_AGENT_LENGTH: usize = 255;
const AZSDK_USER_AGENT_PREFIX: &str = "azsdk-rust-";
const SDK_NAME: &str = "cosmos-driver";
const SDK_VERSION: &str = env!("CARGO_PKG_VERSION");
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UserAgent {
full_user_agent: String,
suffix: Option<String>,
}
impl Default for UserAgent {
fn default() -> Self {
Self::new(None::<&str>)
}
}
impl UserAgent {
fn base_user_agent() -> String {
let os_name = std::env::consts::OS;
let os_arch = std::env::consts::ARCH;
let rust_version = option_env!("RUSTC_VERSION").unwrap_or("unknown");
let mut value = String::with_capacity(
AZSDK_USER_AGENT_PREFIX.len()
+ SDK_NAME.len()
+ 1
+ SDK_VERSION.len()
+ 1
+ os_name.len()
+ 1
+ os_arch.len()
+ 7
+ rust_version.len(),
);
value.push_str(AZSDK_USER_AGENT_PREFIX);
value.push_str(SDK_NAME);
value.push('/');
value.push_str(SDK_VERSION);
value.push(' ');
value.push_str(os_name);
value.push('/');
value.push_str(os_arch);
value.push_str(" rustc/");
value.push_str(rust_version);
value
}
fn new(suffix: Option<impl Into<String>>) -> Self {
let base = strip_non_ascii(&Self::base_user_agent());
let normalized_suffix = suffix.map(Into::into).map(|s| strip_non_ascii(&s));
let max_suffix_len = MAX_USER_AGENT_LENGTH.saturating_sub(base.len() + 1);
let effective_suffix = normalized_suffix.and_then(|s| {
if s.is_empty() || max_suffix_len == 0 {
None
} else {
Some(s[..s.len().min(max_suffix_len)].to_string())
}
});
let mut full_user_agent = String::with_capacity(
base.len() + effective_suffix.as_ref().map_or(0, |s| 1 + s.len()),
);
full_user_agent.push_str(&base);
if let Some(s) = &effective_suffix {
full_user_agent.push(' ');
full_user_agent.push_str(s);
}
Self {
full_user_agent,
suffix: effective_suffix,
}
}
pub(crate) fn from_suffix(suffix: &UserAgentSuffix) -> Self {
Self::new(Some(suffix.as_str()))
}
pub(crate) fn from_workload_id(workload_id: WorkloadId) -> Self {
Self::new(Some(format!("w{}", workload_id.value())))
}
pub(crate) fn from_correlation_id(correlation_id: &CorrelationId) -> Self {
Self::new(Some(correlation_id.as_str()))
}
pub fn as_str(&self) -> &str {
&self.full_user_agent
}
pub fn suffix(&self) -> Option<&str> {
self.suffix.as_deref()
}
}
impl fmt::Display for UserAgent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.full_user_agent)
}
}
fn strip_non_ascii(input: &str) -> String {
input
.chars()
.map(|c| {
if c.is_ascii() && !c.is_ascii_control() {
c
} else {
'_'
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn user_agent_default_has_base_prefix() {
let ua = UserAgent::default();
assert!(ua.as_str().starts_with("azsdk-rust-cosmos-driver/"));
assert!(ua.suffix().is_none());
}
#[test]
fn user_agent_with_suffix() {
let ua = UserAgent::new(Some("my-app"));
assert!(ua.as_str().contains("my-app"));
assert_eq!(ua.suffix(), Some("my-app"));
}
#[test]
fn user_agent_from_user_agent_suffix() {
let suffix = UserAgentSuffix::new("myapp-westus2");
let ua = UserAgent::from_suffix(&suffix);
assert!(ua.as_str().contains("myapp-westus2"));
}
#[test]
fn user_agent_from_workload_id() {
let workload_id = WorkloadId::new(25);
let ua = UserAgent::from_workload_id(workload_id);
assert!(ua.as_str().contains("w25"));
}
#[test]
fn user_agent_from_correlation_id() {
let correlation_id = CorrelationId::new("aks-prod-eastus");
let ua = UserAgent::from_correlation_id(&correlation_id);
assert!(ua.as_str().contains("aks-prod-eastus"));
}
#[test]
fn user_agent_strips_non_ascii() {
let input = "test café";
let stripped = strip_non_ascii(input);
assert!(stripped.is_ascii());
}
}