1use std::{str::FromStr, sync::LazyLock};
2
3use regex::Regex;
4
5use crate::FWClientError;
6
7static API_KEY_RE: LazyLock<Regex> = LazyLock::new(|| {
8 Regex::new(
9 r"(?i)((?P<api_key_type>bearer|scitran-user) )?((?P<scheme>https?://)?(?P<host>[^:]+)(?P<port>:\d+)?:)?(?P<api_key>.+)"
10 ).expect("Failed to compile API key regex")
11});
12
13pub fn get_user_agent(name: Option<String>, version: Option<String>) -> String {
14 let version = version.unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string());
15 let name = name.unwrap_or_else(|| env!("CARGO_PKG_NAME").to_string());
16 let os = std::env::consts::OS;
17 let family = std::env::consts::FAMILY;
18 let arch = std::env::consts::ARCH;
19 let comments = [("family", family), ("arch", arch), ("os", os)]
20 .iter()
21 .map(|(k, v)| format!("{}:{}", k, v))
22 .collect::<Vec<_>>()
23 .join(";");
24 format!("{}/{} ({})", name, version, comments)
25}
26
27#[derive(Debug, Clone)]
34pub struct ApiKey {
35 pub api_key_type: Option<String>,
36 pub scheme: Option<String>,
37 pub host: String,
38 pub port: Option<String>,
39 pub api_key: String,
40}
41
42impl<'de> serde::Deserialize<'de> for ApiKey {
43 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
44 where
45 D: serde::Deserializer<'de>,
46 {
47 let api_key = String::deserialize(deserializer)?;
48 ApiKey::from_str(api_key.as_ref()).map_err(serde::de::Error::custom)
49 }
50}
51
52impl FromStr for ApiKey {
53 type Err = FWClientError;
54
55 fn from_str(api_key: &str) -> Result<Self, Self::Err> {
58 let caps = API_KEY_RE
59 .captures(api_key.as_ref())
60 .ok_or_else(|| FWClientError::InvalidApiKey(api_key.to_string()))?;
61 Ok(ApiKey {
62 api_key_type: caps.name("api_key_type").map(|m| m.as_str().to_string()),
63 scheme: caps.name("scheme").map(|m| m.as_str().to_string()),
64 host: caps
65 .name("host")
66 .map(|m| m.as_str())
67 .ok_or_else(|| FWClientError::MissingComponent("host".to_string()))?
68 .to_string(),
69 port: caps.name("port").map(|m| m.as_str().to_string()),
70 api_key: caps
71 .name("api_key")
72 .map(|m| m.as_str())
73 .ok_or_else(|| FWClientError::MissingComponent("api_key".to_string()))?
74 .to_string(),
75 })
76 }
77}
78
79impl std::fmt::Display for ApiKey {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 let mut api_key = String::new();
82 match &self.api_key_type {
83 Some(api_key_type) => api_key.push_str(format!("{} ", api_key_type).as_str()),
84 None => api_key.push_str("scitran-user "),
85 }
86 api_key.push_str(&self.host);
87 api_key.push_str(&format!(":{}", self.api_key));
88 write!(f, "{}", api_key)
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use rstest::rstest;
96
97 #[rstest]
98 #[case::with_api_key_type(
99 "scitran-user flywheel.io:my-api-key",
100 "scitran-user flywheel.io:my-api-key"
101 )]
102 #[case::with_bearer(
103 "bearer flywheel.io:my-bearer-token",
104 "bearer flywheel.io:my-bearer-token"
105 )]
106 #[case::with_scheme(
107 "https://api.flywheel.io:my-api-key",
108 "scitran-user api.flywheel.io:my-api-key"
109 )]
110 #[case::with_port(
111 "api.flywheel.io:8000:my-api-key",
112 "scitran-user api.flywheel.io:my-api-key"
113 )]
114 #[case::with_all_components(
115 "bearer https://api.flywheel.io:8000:my-api-key",
116 "bearer api.flywheel.io:my-api-key"
117 )]
118 #[case::simple_format("flywheel.io:my-api-key", "scitran-user flywheel.io:my-api-key")]
119 #[case::colon_in_key("fw_instance:443::", "scitran-user fw_instance::")]
121 #[case::port_is_key("fw_instance:443", "scitran-user fw_instance:443")]
122 fn test_parse_api_key_success(#[case] input_key: &str, #[case] expected_display: &str) {
123 let api_key = ApiKey::from_str(input_key).unwrap();
125
126 assert_eq!(api_key.to_string(), expected_display);
128 }
129
130 #[rstest]
131 #[case::without_host(":my-api-key", FWClientError::MissingComponent("host".to_string()))]
132 #[case::invalid_format("invalid-format", FWClientError::MissingComponent("host".to_string()))]
133 fn test_parse_api_key_failure(
134 #[case] api_key_str: &str,
135 #[case] expected_error: FWClientError,
136 ) {
137 let result = ApiKey::from_str(api_key_str);
138
139 assert!(result.is_err());
140 match (result.unwrap_err(), expected_error) {
141 (
142 FWClientError::MissingComponent(component),
143 FWClientError::MissingComponent(expected),
144 ) => {
145 assert_eq!(component, expected);
146 }
147 (FWClientError::InvalidApiKey(key), FWClientError::InvalidApiKey(expected)) => {
148 assert_eq!(key, expected);
149 }
150 (err, expected) => panic!("Expected {:?}, got {:?}", expected, err),
151 }
152 }
153
154 #[test]
155 fn test_user_agent() {
156 let user_agent = get_user_agent(None, None);
157 assert!(user_agent.contains("fw-client"));
158 assert!(user_agent.contains("os:"));
159 assert!(user_agent.contains("arch:"));
160 assert!(user_agent.contains("family:"));
161 println!("User agent: {}", user_agent);
162 }
163}