nym_http_api_client/
user_agent.rs

1// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{fmt, str::FromStr};
5
6use http::HeaderValue;
7use nym_bin_common::build_information::{BinaryBuildInformation, BinaryBuildInformationOwned};
8use serde::{Deserialize, Serialize};
9
10/// Characteristic elements sent to the API providing basic context information of the requesting client.
11#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
12pub struct UserAgent {
13    /// The internal crate / application / subsystem making use of API client
14    pub application: String,
15    /// version of the calling crate / application / subsystem
16    pub version: String,
17    /// client platform
18    pub platform: String,
19    /// source commit version for the calling crate / subsystem
20    pub git_commit: String,
21}
22
23/// Create `UserAgent` based on the caller's crate information
24// we can't use normal function as then `application` and `version` would correspond
25// of that of `nym-http-api-client` lib
26#[macro_export]
27macro_rules! generate_user_agent {
28    () => {
29        $crate::UserAgent::from($crate::bin_info!())
30    };
31}
32
33#[derive(Clone, Debug, thiserror::Error)]
34#[error("invalid user agent string: {0}")]
35pub struct UserAgentError(String);
36
37impl FromStr for UserAgent {
38    type Err = UserAgentError;
39
40    fn from_str(s: &str) -> Result<Self, Self::Err> {
41        let parts: Vec<&str> = s.split('/').collect();
42        if parts.len() != 4 {
43            return Err(UserAgentError(s.to_string()));
44        }
45
46        Ok(UserAgent {
47            application: parts[0].to_string(),
48            version: parts[1].to_string(),
49            platform: parts[2].to_string(),
50            git_commit: parts[3].to_string(),
51        })
52    }
53}
54
55impl TryFrom<&str> for UserAgent {
56    type Error = UserAgentError;
57
58    fn try_from(s: &str) -> Result<Self, Self::Error> {
59        UserAgent::from_str(s)
60    }
61}
62
63impl fmt::Display for UserAgent {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        let abbreviated_commit = self.git_commit.chars().take(7).collect::<String>();
66        write!(
67            f,
68            "{}/{}/{}/{}",
69            self.application, self.version, self.platform, abbreviated_commit
70        )
71    }
72}
73
74impl TryFrom<UserAgent> for HeaderValue {
75    type Error = http::header::InvalidHeaderValue;
76
77    fn try_from(user_agent: UserAgent) -> Result<Self, Self::Error> {
78        HeaderValue::from_str(&user_agent.to_string())
79    }
80}
81
82impl From<BinaryBuildInformation> for UserAgent {
83    fn from(build_info: BinaryBuildInformation) -> Self {
84        UserAgent {
85            application: build_info.binary_name.to_string(),
86            version: build_info.build_version.to_string(),
87            platform: build_info.cargo_triple.to_string(),
88            git_commit: build_info.commit_sha.to_string(),
89        }
90    }
91}
92
93impl From<BinaryBuildInformationOwned> for UserAgent {
94    fn from(build_info: BinaryBuildInformationOwned) -> Self {
95        UserAgent {
96            application: build_info.binary_name,
97            version: build_info.build_version,
98            platform: build_info.cargo_triple,
99            git_commit: build_info.commit_sha,
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn parsing_valid_user_agent() {
110        let user_agent = "nym-mixnode/0.11.0/x86_64-unknown-linux-gnu/abcdefg";
111        let parsed = UserAgent::from_str(user_agent).unwrap();
112        assert_eq!(
113            parsed,
114            UserAgent {
115                application: "nym-mixnode".to_string(),
116                version: "0.11.0".to_string(),
117                platform: "x86_64-unknown-linux-gnu".to_string(),
118                git_commit: "abcdefg".to_string()
119            }
120        );
121    }
122
123    #[test]
124    fn parsing_invalid_user_agent() {
125        let user_agent = "nym-mixnode/0.11.0/x86_64-unknown-linux-gnu";
126        assert!(UserAgent::from_str(user_agent).is_err());
127    }
128
129    #[test]
130    fn converting_user_agent_to_string() {
131        let user_agent = UserAgent {
132            application: "nym-mixnode".to_string(),
133            version: "0.11.0".to_string(),
134            platform: "x86_64-unknown-linux-gnu".to_string(),
135            git_commit: "abcdefg".to_string(),
136        };
137
138        assert_eq!(
139            user_agent.to_string(),
140            "nym-mixnode/0.11.0/x86_64-unknown-linux-gnu/abcdefg"
141        );
142    }
143
144    #[test]
145    fn converting_user_agent_to_display() {
146        let user_agent = UserAgent {
147            application: "nym-mixnode".to_string(),
148            version: "0.11.0".to_string(),
149            platform: "x86_64-unknown-linux-gnu".to_string(),
150            git_commit: "abcdefg".to_string(),
151        };
152
153        assert_eq!(
154            format!("{user_agent}"),
155            "nym-mixnode/0.11.0/x86_64-unknown-linux-gnu/abcdefg"
156        );
157    }
158
159    #[test]
160    fn converting_user_agent_to_header_value_fails() {
161        let user_agent = UserAgent {
162            application: "nym-mixnode".to_string(),
163            version: "0.11.0".to_string(),
164            platform: "x86_64-unknown-linux-gnu".to_string(),
165            git_commit: "abcdefg".to_string(),
166        };
167
168        let header_value: Result<HeaderValue, _> = user_agent.clone().try_into();
169        assert!(header_value.is_ok());
170    }
171
172    #[test]
173    fn converting_user_agent_to_header_value_has_same_string_representation() {
174        let user_agent = UserAgent {
175            application: "nym-mixnode".to_string(),
176            version: "0.11.0".to_string(),
177            platform: "x86_64-unknown-linux-gnu".to_string(),
178            git_commit: "abcdefg".to_string(),
179        };
180
181        let header_value: HeaderValue = user_agent.clone().try_into().unwrap();
182        assert_eq!(header_value.to_str().unwrap(), user_agent.to_string());
183    }
184}