conjure_runtime/
user_agent.rs1use conjure_object::log_safety::AssertLogSafe;
15use once_cell::sync::Lazy;
16use regex::Regex;
17use std::fmt;
18use witchcraft_log::warn;
19
20static VALID_NODE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-zA-Z0-9][a-zA-Z0-9.\-]*$").unwrap());
21static VALID_NAME: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-zA-Z][a-zA-Z0-9\-]*$").unwrap());
22static VALID_VERSION: Lazy<Regex> =
23 Lazy::new(|| Regex::new(r"^[0-9]+(\.[0-9]+)*(-rc[0-9]+)?(-[0-9]+-g[a-f0-9]+)?$").unwrap());
24
25const DEFAULT_VERSION: &str = "0.0.0";
26
27#[derive(Debug, Clone, PartialEq, Eq, Hash)]
29pub struct UserAgent {
30 node_id: Option<String>,
31 primary: Agent,
32 informational: Vec<Agent>,
33}
34
35impl fmt::Display for UserAgent {
36 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
37 write!(fmt, "{}", self.primary)?;
38
39 if let Some(ref node_id) = self.node_id {
40 write!(fmt, " (nodeId:{node_id})")?;
41 }
42
43 for agent in &self.informational {
44 write!(fmt, " {agent}")?;
45 }
46
47 Ok(())
48 }
49}
50
51impl UserAgent {
52 pub fn new(primary: Agent) -> UserAgent {
54 UserAgent {
55 node_id: None,
56 primary,
57 informational: vec![],
58 }
59 }
60
61 pub fn push_agent(&mut self, agent: Agent) {
63 self.informational.push(agent);
64 }
65
66 pub fn set_node_id(&mut self, node_id: &str) {
70 assert!(
71 VALID_NODE.is_match(node_id),
72 "invalid user agent node ID `{}`",
73 node_id
74 );
75 self.node_id = Some(node_id.to_string());
76 }
77
78 pub fn node_id(&self) -> Option<&str> {
80 self.node_id.as_deref()
81 }
82
83 pub fn primary(&self) -> &Agent {
85 &self.primary
86 }
87
88 pub fn informational(&self) -> &[Agent] {
90 &self.informational
91 }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Hash)]
96pub struct Agent {
97 name: String,
98 version: String,
99}
100
101impl fmt::Display for Agent {
102 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
103 write!(fmt, "{}/{}", self.name, self.version)
104 }
105}
106
107impl Agent {
108 pub fn new(name: &str, mut version: &str) -> Agent {
110 assert!(VALID_NAME.is_match(name), "invalid agent name `{}`", name);
111
112 if !VALID_VERSION.is_match(version) {
113 warn!(
114 "encountered invalid user agent version",
115 safe: {
116 version: AssertLogSafe(version),
117 }
118 );
119 version = DEFAULT_VERSION;
120 }
121
122 Agent {
123 name: name.to_string(),
124 version: version.to_string(),
125 }
126 }
127
128 pub fn name(&self) -> &str {
130 &self.name
131 }
132
133 pub fn version(&self) -> &str {
135 &self.version
136 }
137}
138
139#[cfg(test)]
140mod test {
141 use super::*;
142
143 #[test]
144 fn fmt() {
145 let mut agent = UserAgent::new(Agent::new("foobar", "1.2.3"));
146 agent.set_node_id("127.0.0.1");
147 agent.push_agent(Agent::new("fizzbuzz", "0.0.0-1-g12345"));
148 agent.push_agent(Agent::new("btob", "1.0.0-rc1"));
149 assert_eq!(
150 agent.to_string(),
151 "foobar/1.2.3 (nodeId:127.0.0.1) fizzbuzz/0.0.0-1-g12345 btob/1.0.0-rc1"
152 );
153 }
154
155 #[test]
156 fn version_fallback() {
157 let agent = Agent::new("foobar", "some-invalid-version");
158 assert_eq!(agent.version(), "0.0.0");
159 }
160}