use once_cell::sync::Lazy;
use regex::Regex;
use std::fmt;
use witchcraft_log::warn;
static VALID_NODE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-zA-Z0-9][a-zA-Z0-9.\-]*$").unwrap());
static VALID_NAME: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-zA-Z][a-zA-Z0-9\-]*$").unwrap());
static VALID_VERSION: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^[0-9]+(\.[0-9]+)*(-rc[0-9]+)?(-[0-9]+-g[a-f0-9]+)?$").unwrap());
const DEFAULT_VERSION: &str = "0.0.0";
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UserAgent {
node_id: Option<String>,
primary: Agent,
informational: Vec<Agent>,
}
impl fmt::Display for UserAgent {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(fmt, "{}", self.primary)?;
if let Some(ref node_id) = self.node_id {
write!(fmt, " (nodeId:{})", node_id)?;
}
for agent in &self.informational {
write!(fmt, " {}", agent)?;
}
Ok(())
}
}
impl UserAgent {
pub fn new(primary: Agent) -> UserAgent {
UserAgent {
node_id: None,
primary,
informational: vec![],
}
}
pub fn push_agent(&mut self, agent: Agent) {
self.informational.push(agent);
}
pub fn set_node_id(&mut self, node_id: &str) {
assert!(
VALID_NODE.is_match(node_id),
"invalid user agent node ID `{}`",
node_id
);
self.node_id = Some(node_id.to_string());
}
pub fn node_id(&self) -> Option<&str> {
self.node_id.as_deref()
}
pub fn primary(&self) -> &Agent {
&self.primary
}
pub fn informational(&self) -> &[Agent] {
&self.informational
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Agent {
name: String,
version: String,
}
impl fmt::Display for Agent {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(fmt, "{}/{}", self.name, self.version)
}
}
impl Agent {
pub fn new(name: &str, mut version: &str) -> Agent {
assert!(VALID_NAME.is_match(name), "invalid agent name `{}`", name);
if !VALID_VERSION.is_match(version) {
warn!(
"encountered invalid user agent version",
safe: {
version: version,
}
);
version = DEFAULT_VERSION;
}
Agent {
name: name.to_string(),
version: version.to_string(),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn version(&self) -> &str {
&self.version
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn fmt() {
let mut agent = UserAgent::new(Agent::new("foobar", "1.2.3"));
agent.set_node_id("127.0.0.1");
agent.push_agent(Agent::new("fizzbuzz", "0.0.0-1-g12345"));
agent.push_agent(Agent::new("btob", "1.0.0-rc1"));
assert_eq!(
agent.to_string(),
"foobar/1.2.3 (nodeId:127.0.0.1) fizzbuzz/0.0.0-1-g12345 btob/1.0.0-rc1"
);
}
#[test]
fn version_fallback() {
let agent = Agent::new("foobar", "some-invalid-version");
assert_eq!(agent.version(), "0.0.0");
}
}