Skip to content

Commit bfc846d

Browse files
authored
feat: client identification headers (#90)
1 parent 103d104 commit bfc846d

File tree

6 files changed

+157
-2
lines changed

6 files changed

+157
-2
lines changed

.github/workflows/ci-matrix.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ jobs:
8282
strategy:
8383
matrix:
8484
rust:
85-
- 1.60.0 # MSRV
85+
- 1.63.0 # MSRV
8686

8787
steps:
8888
- uses: actions/checkout@v2

Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ rustversion = "1.0.7"
6262
serde_json = "1.0.68"
6363
serde_plain = "1.0.0"
6464
surf = { version = "2.3.1", optional = true }
65+
uuid = { version = "1.11.0", features = ["v4"] }
6566

6667
[dependencies.chrono]
6768
default-features = false
@@ -91,6 +92,7 @@ futures = "0.3.17"
9192
maplit = "1.0.2"
9293
num_cpus = "1.13.0"
9394
simple_logger = "2.1.0"
95+
regex = "1.9.6"
9496

9597
[dev-dependencies.tokio]
9698
features = ["macros", "rt-multi-thread", "time"]

src/api.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use std::collections::HashMap;
44
use std::default::Default;
55

6+
use crate::version::get_sdk_version;
67
use chrono::Utc;
78
use serde::{Deserialize, Serialize};
89

@@ -101,7 +102,7 @@ impl Default for Registration {
101102
Self {
102103
app_name: "".into(),
103104
instance_id: "".into(),
104-
sdk_version: "unleash-client-rust-0.1.0".into(),
105+
sdk_version: get_sdk_version().into(),
105106
strategies: vec![],
106107
started: Utc::now(),
107108
interval: 15 * 1000,

src/http.rs

+119
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,27 @@ mod surf;
1212
pub struct HTTP<C: HttpClient> {
1313
authorization_header: C::HeaderName,
1414
app_name_header: C::HeaderName,
15+
unleash_app_name_header: C::HeaderName,
16+
unleash_sdk_header: C::HeaderName,
17+
unleash_connection_id_header: C::HeaderName,
1518
instance_id_header: C::HeaderName,
1619
app_name: String,
20+
sdk_version: &'static str,
1721
instance_id: String,
22+
// The connection_id represents a logical connection from the SDK to Unleash.
23+
// It's assigned internally by the SDK and lives as long as the Unleash client instance.
24+
// We can't reuse instance_id since some SDKs allow to override it while
25+
// connection_id has to be uniquely defined by the SDK.
26+
connection_id: String,
1827
authorization: Option<String>,
1928
client: C,
2029
}
2130

31+
use crate::version::get_sdk_version;
2232
use serde::{de::DeserializeOwned, Serialize};
2333
#[doc(inline)]
2434
pub use shim::HttpClient;
35+
use uuid::Uuid;
2536

2637
impl<C> HTTP<C>
2738
where
@@ -36,10 +47,15 @@ where
3647
Ok(HTTP {
3748
client: C::default(),
3849
app_name,
50+
sdk_version: get_sdk_version(),
51+
connection_id: Uuid::new_v4().to_string(),
3952
instance_id,
4053
authorization,
4154
authorization_header: C::build_header("authorization")?,
4255
app_name_header: C::build_header("appname")?,
56+
unleash_app_name_header: C::build_header("unleash-appname")?,
57+
unleash_sdk_header: C::build_header("unleash-sdk")?,
58+
unleash_connection_id_header: C::build_header("unleash-connection-id")?,
4359
instance_id_header: C::build_header("instance_id")?,
4460
})
4561
}
@@ -73,6 +89,17 @@ where
7389

7490
fn attach_headers(&self, request: C::RequestBuilder) -> C::RequestBuilder {
7591
let request = C::header(request, &self.app_name_header, self.app_name.as_str());
92+
let request = C::header(
93+
request,
94+
&self.unleash_app_name_header,
95+
self.app_name.as_str(),
96+
);
97+
let request = C::header(request, &self.unleash_sdk_header, self.sdk_version);
98+
let request = C::header(
99+
request,
100+
&self.unleash_connection_id_header,
101+
self.connection_id.as_str(),
102+
);
76103
let request = C::header(request, &self.instance_id_header, self.instance_id.as_str());
77104
if let Some(auth) = &self.authorization {
78105
C::header(request, &self.authorization_header.clone(), auth.as_str())
@@ -81,3 +108,95 @@ where
81108
}
82109
}
83110
}
111+
112+
#[cfg(test)]
113+
mod tests {
114+
use super::*;
115+
use async_trait::async_trait;
116+
use regex::Regex;
117+
118+
#[derive(Clone, Default)]
119+
struct MockHttpClient {
120+
headers: std::collections::HashMap<String, String>,
121+
}
122+
123+
#[async_trait]
124+
impl HttpClient for MockHttpClient {
125+
type Error = std::io::Error;
126+
type HeaderName = String;
127+
type RequestBuilder = Self;
128+
129+
fn build_header(name: &'static str) -> Result<Self::HeaderName, Self::Error> {
130+
Ok(name.to_string())
131+
}
132+
133+
fn header(mut builder: Self, key: &Self::HeaderName, value: &str) -> Self::RequestBuilder {
134+
builder.headers.insert(key.clone(), value.to_string());
135+
builder
136+
}
137+
138+
fn get(&self, _uri: &str) -> Self::RequestBuilder {
139+
self.clone()
140+
}
141+
142+
fn post(&self, _uri: &str) -> Self::RequestBuilder {
143+
self.clone()
144+
}
145+
146+
async fn get_json<T: DeserializeOwned>(
147+
_req: Self::RequestBuilder,
148+
) -> Result<T, Self::Error> {
149+
unimplemented!()
150+
}
151+
152+
async fn post_json<T: Serialize + Sync>(
153+
_req: Self::RequestBuilder,
154+
_content: &T,
155+
) -> Result<bool, Self::Error> {
156+
unimplemented!()
157+
}
158+
}
159+
160+
#[tokio::test]
161+
async fn test_specific_headers() {
162+
let http_client = HTTP::<MockHttpClient>::new(
163+
"my_app".to_string(),
164+
"my_instance_id".to_string(),
165+
Some("auth_token".to_string()),
166+
)
167+
.unwrap();
168+
169+
let request_builder = http_client.client.post("http://example.com");
170+
let request_with_headers = http_client.attach_headers(request_builder);
171+
172+
assert_eq!(
173+
request_with_headers.headers.get("unleash-appname").unwrap(),
174+
"my_app"
175+
);
176+
assert_eq!(
177+
request_with_headers.headers.get("instance_id").unwrap(),
178+
"my_instance_id"
179+
);
180+
assert_eq!(
181+
request_with_headers.headers.get("authorization").unwrap(),
182+
"auth_token"
183+
);
184+
185+
let version_regex = Regex::new(r"^unleash-client-rust:\d+\.\d+\.\d+$").unwrap();
186+
let sdk_version = request_with_headers.headers.get("unleash-sdk").unwrap();
187+
assert!(
188+
version_regex.is_match(sdk_version),
189+
"Version output did not match expected format: {}",
190+
sdk_version
191+
);
192+
193+
let connection_id = request_with_headers
194+
.headers
195+
.get("unleash-connection-id")
196+
.unwrap();
197+
assert!(
198+
Uuid::parse_str(connection_id).is_ok(),
199+
"Connection ID is not a valid UUID"
200+
);
201+
}
202+
}

src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ pub mod config;
124124
pub mod context;
125125
pub mod http;
126126
pub mod strategy;
127+
pub mod version;
127128

128129
// Exports for ergonomical use
129130
pub use crate::client::{Client, ClientBuilder};

src/version.rs

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//! Managing SDK versions
2+
//!
3+
//! This module includes utilities to handle versioning aspects used internally
4+
//! by the crate.
5+
use std::env;
6+
7+
/// Returns the version of the `unleash-client-rust` SDK compiled into the binary.
8+
///
9+
/// The version number is included at compile time using the cargo package version
10+
/// and is formatted as "unleash-client-rust:X.Y.Z", where X.Y.Z is the semantic
11+
/// versioning format. This ensures a consistent versioning approach that aligns
12+
/// with other Unleash SDKs.
13+
pub(crate) fn get_sdk_version() -> &'static str {
14+
concat!("unleash-client-rust:", env!("CARGO_PKG_VERSION"))
15+
}
16+
17+
#[cfg(test)]
18+
mod tests {
19+
use super::*;
20+
use regex::Regex;
21+
22+
#[test]
23+
fn test_get_sdk_version_with_version_set() {
24+
let version_output = get_sdk_version();
25+
let version_regex = Regex::new(r"^unleash-client-rust:\d+\.\d+\.\d+$").unwrap();
26+
assert!(
27+
version_regex.is_match(version_output),
28+
"Version output did not match expected format: {}",
29+
version_output
30+
);
31+
}
32+
}

0 commit comments

Comments
 (0)