diff --git a/.github/workflows/ci-matrix.yml b/.github/workflows/ci-matrix.yml index a119db5..6db6788 100644 --- a/.github/workflows/ci-matrix.yml +++ b/.github/workflows/ci-matrix.yml @@ -82,7 +82,7 @@ jobs: strategy: matrix: rust: - - 1.60.0 # MSRV + - 1.63.0 # MSRV steps: - uses: actions/checkout@v2 diff --git a/Cargo.toml b/Cargo.toml index a67f0a8..4529b7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ rustversion = "1.0.7" serde_json = "1.0.68" serde_plain = "1.0.0" surf = { version = "2.3.1", optional = true } +uuid = { version = "1.11.0", features = ["v4"] } [dependencies.chrono] default-features = false @@ -91,6 +92,7 @@ futures = "0.3.17" maplit = "1.0.2" num_cpus = "1.13.0" simple_logger = "2.1.0" +regex = "1.9.6" [dev-dependencies.tokio] features = ["macros", "rt-multi-thread", "time"] diff --git a/src/api.rs b/src/api.rs index 8bc0ec1..66272c2 100644 --- a/src/api.rs +++ b/src/api.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use std::default::Default; +use crate::version::get_sdk_version; use chrono::Utc; use serde::{Deserialize, Serialize}; @@ -101,7 +102,7 @@ impl Default for Registration { Self { app_name: "".into(), instance_id: "".into(), - sdk_version: "unleash-client-rust-0.1.0".into(), + sdk_version: get_sdk_version().into(), strategies: vec![], started: Utc::now(), interval: 15 * 1000, diff --git a/src/http.rs b/src/http.rs index 5582c94..933a9fe 100644 --- a/src/http.rs +++ b/src/http.rs @@ -12,16 +12,27 @@ mod surf; pub struct HTTP { authorization_header: C::HeaderName, app_name_header: C::HeaderName, + unleash_app_name_header: C::HeaderName, + unleash_sdk_header: C::HeaderName, + unleash_connection_id_header: C::HeaderName, instance_id_header: C::HeaderName, app_name: String, + sdk_version: &'static str, instance_id: String, + // The connection_id represents a logical connection from the SDK to Unleash. + // It's assigned internally by the SDK and lives as long as the Unleash client instance. + // We can't reuse instance_id since some SDKs allow to override it while + // connection_id has to be uniquely defined by the SDK. + connection_id: String, authorization: Option, client: C, } +use crate::version::get_sdk_version; use serde::{de::DeserializeOwned, Serialize}; #[doc(inline)] pub use shim::HttpClient; +use uuid::Uuid; impl HTTP where @@ -36,10 +47,15 @@ where Ok(HTTP { client: C::default(), app_name, + sdk_version: get_sdk_version(), + connection_id: Uuid::new_v4().to_string(), instance_id, authorization, authorization_header: C::build_header("authorization")?, app_name_header: C::build_header("appname")?, + unleash_app_name_header: C::build_header("unleash-appname")?, + unleash_sdk_header: C::build_header("unleash-sdk")?, + unleash_connection_id_header: C::build_header("unleash-connection-id")?, instance_id_header: C::build_header("instance_id")?, }) } @@ -73,6 +89,17 @@ where fn attach_headers(&self, request: C::RequestBuilder) -> C::RequestBuilder { let request = C::header(request, &self.app_name_header, self.app_name.as_str()); + let request = C::header( + request, + &self.unleash_app_name_header, + self.app_name.as_str(), + ); + let request = C::header(request, &self.unleash_sdk_header, self.sdk_version); + let request = C::header( + request, + &self.unleash_connection_id_header, + self.connection_id.as_str(), + ); let request = C::header(request, &self.instance_id_header, self.instance_id.as_str()); if let Some(auth) = &self.authorization { C::header(request, &self.authorization_header.clone(), auth.as_str()) @@ -81,3 +108,95 @@ where } } } + +#[cfg(test)] +mod tests { + use super::*; + use async_trait::async_trait; + use regex::Regex; + + #[derive(Clone, Default)] + struct MockHttpClient { + headers: std::collections::HashMap, + } + + #[async_trait] + impl HttpClient for MockHttpClient { + type Error = std::io::Error; + type HeaderName = String; + type RequestBuilder = Self; + + fn build_header(name: &'static str) -> Result { + Ok(name.to_string()) + } + + fn header(mut builder: Self, key: &Self::HeaderName, value: &str) -> Self::RequestBuilder { + builder.headers.insert(key.clone(), value.to_string()); + builder + } + + fn get(&self, _uri: &str) -> Self::RequestBuilder { + self.clone() + } + + fn post(&self, _uri: &str) -> Self::RequestBuilder { + self.clone() + } + + async fn get_json( + _req: Self::RequestBuilder, + ) -> Result { + unimplemented!() + } + + async fn post_json( + _req: Self::RequestBuilder, + _content: &T, + ) -> Result { + unimplemented!() + } + } + + #[tokio::test] + async fn test_specific_headers() { + let http_client = HTTP::::new( + "my_app".to_string(), + "my_instance_id".to_string(), + Some("auth_token".to_string()), + ) + .unwrap(); + + let request_builder = http_client.client.post("http://example.com"); + let request_with_headers = http_client.attach_headers(request_builder); + + assert_eq!( + request_with_headers.headers.get("unleash-appname").unwrap(), + "my_app" + ); + assert_eq!( + request_with_headers.headers.get("instance_id").unwrap(), + "my_instance_id" + ); + assert_eq!( + request_with_headers.headers.get("authorization").unwrap(), + "auth_token" + ); + + let version_regex = Regex::new(r"^unleash-client-rust:\d+\.\d+\.\d+$").unwrap(); + let sdk_version = request_with_headers.headers.get("unleash-sdk").unwrap(); + assert!( + version_regex.is_match(sdk_version), + "Version output did not match expected format: {}", + sdk_version + ); + + let connection_id = request_with_headers + .headers + .get("unleash-connection-id") + .unwrap(); + assert!( + Uuid::parse_str(connection_id).is_ok(), + "Connection ID is not a valid UUID" + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 9ad807f..fd45a54 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -124,6 +124,7 @@ pub mod config; pub mod context; pub mod http; pub mod strategy; +pub mod version; // Exports for ergonomical use pub use crate::client::{Client, ClientBuilder}; diff --git a/src/version.rs b/src/version.rs new file mode 100644 index 0000000..a2b1ff3 --- /dev/null +++ b/src/version.rs @@ -0,0 +1,32 @@ +//! Managing SDK versions +//! +//! This module includes utilities to handle versioning aspects used internally +//! by the crate. +use std::env; + +/// Returns the version of the `unleash-client-rust` SDK compiled into the binary. +/// +/// The version number is included at compile time using the cargo package version +/// and is formatted as "unleash-client-rust:X.Y.Z", where X.Y.Z is the semantic +/// versioning format. This ensures a consistent versioning approach that aligns +/// with other Unleash SDKs. +pub(crate) fn get_sdk_version() -> &'static str { + concat!("unleash-client-rust:", env!("CARGO_PKG_VERSION")) +} + +#[cfg(test)] +mod tests { + use super::*; + use regex::Regex; + + #[test] + fn test_get_sdk_version_with_version_set() { + let version_output = get_sdk_version(); + let version_regex = Regex::new(r"^unleash-client-rust:\d+\.\d+\.\d+$").unwrap(); + assert!( + version_regex.is_match(version_output), + "Version output did not match expected format: {}", + version_output + ); + } +}