conjure_runtime/
user_agent.rs

1// Copyright 2020 Palantir Technologies, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14use once_cell::sync::Lazy;
15use regex::Regex;
16use std::fmt;
17use witchcraft_log::warn;
18
19static VALID_NODE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-zA-Z0-9][a-zA-Z0-9.\-]*$").unwrap());
20static VALID_NAME: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-zA-Z][a-zA-Z0-9\-]*$").unwrap());
21static VALID_VERSION: Lazy<Regex> =
22    Lazy::new(|| Regex::new(r"^[0-9]+(\.[0-9]+)*(-rc[0-9]+)?(-[0-9]+-g[a-f0-9]+)?$").unwrap());
23
24const DEFAULT_VERSION: &str = "0.0.0";
25
26/// A representation of an HTTP `User-Agent` header value.
27#[derive(Debug, Clone, PartialEq, Eq, Hash)]
28pub struct UserAgent {
29    node_id: Option<String>,
30    primary: Agent,
31    informational: Vec<Agent>,
32}
33
34impl fmt::Display for UserAgent {
35    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
36        write!(fmt, "{}", self.primary)?;
37
38        if let Some(ref node_id) = self.node_id {
39            write!(fmt, " (nodeId:{})", node_id)?;
40        }
41
42        for agent in &self.informational {
43            write!(fmt, " {}", agent)?;
44        }
45
46        Ok(())
47    }
48}
49
50impl UserAgent {
51    /// Creates a new `UserAgent`.
52    pub fn new(primary: Agent) -> UserAgent {
53        UserAgent {
54            node_id: None,
55            primary,
56            informational: vec![],
57        }
58    }
59
60    /// Adds an additional informational agent to the `User-Agent`.
61    pub fn push_agent(&mut self, agent: Agent) {
62        self.informational.push(agent);
63    }
64
65    /// Sets the identifier of this node.
66    ///
67    /// For example, this could be the node's IP address.
68    pub fn set_node_id(&mut self, node_id: &str) {
69        assert!(
70            VALID_NODE.is_match(node_id),
71            "invalid user agent node ID `{}`",
72            node_id
73        );
74        self.node_id = Some(node_id.to_string());
75    }
76
77    /// Returns the identifier of this node, if provided.
78    pub fn node_id(&self) -> Option<&str> {
79        self.node_id.as_deref()
80    }
81
82    /// Returns the primary agent.
83    pub fn primary(&self) -> &Agent {
84        &self.primary
85    }
86
87    /// Returns additional informational agents.
88    pub fn informational(&self) -> &[Agent] {
89        &self.informational
90    }
91}
92
93/// A component of a [`UserAgent`].
94#[derive(Debug, Clone, PartialEq, Eq, Hash)]
95pub struct Agent {
96    name: String,
97    version: String,
98}
99
100impl fmt::Display for Agent {
101    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
102        write!(fmt, "{}/{}", self.name, self.version)
103    }
104}
105
106impl Agent {
107    /// Creates a new `Agent`.
108    pub fn new(name: &str, mut version: &str) -> Agent {
109        assert!(VALID_NAME.is_match(name), "invalid agent name `{}`", name);
110
111        if !VALID_VERSION.is_match(version) {
112            warn!(
113                "encountered invalid user agent version",
114                safe: {
115                    version: version,
116                }
117            );
118            version = DEFAULT_VERSION;
119        }
120
121        Agent {
122            name: name.to_string(),
123            version: version.to_string(),
124        }
125    }
126
127    /// Returns the agent's name.
128    pub fn name(&self) -> &str {
129        &self.name
130    }
131
132    /// Returns the agent's version.
133    pub fn version(&self) -> &str {
134        &self.version
135    }
136}
137
138#[cfg(test)]
139mod test {
140    use super::*;
141
142    #[test]
143    fn fmt() {
144        let mut agent = UserAgent::new(Agent::new("foobar", "1.2.3"));
145        agent.set_node_id("127.0.0.1");
146        agent.push_agent(Agent::new("fizzbuzz", "0.0.0-1-g12345"));
147        agent.push_agent(Agent::new("btob", "1.0.0-rc1"));
148        assert_eq!(
149            agent.to_string(),
150            "foobar/1.2.3 (nodeId:127.0.0.1) fizzbuzz/0.0.0-1-g12345 btob/1.0.0-rc1"
151        );
152    }
153
154    #[test]
155    fn version_fallback() {
156        let agent = Agent::new("foobar", "some-invalid-version");
157        assert_eq!(agent.version(), "0.0.0");
158    }
159}