Skip to content

Commit

Permalink
Reword app_name -> product_info, support multiple (similar to the Go …
Browse files Browse the repository at this point in the history
…client)
  • Loading branch information
slvrtrn committed Aug 22, 2024
1 parent 66b2995 commit eb14de1
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 21 deletions.
19 changes: 13 additions & 6 deletions src/headers.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::ProductInfo;
use hyper::header::USER_AGENT;
use hyper::http::request::Builder;
use std::collections::HashMap;
Expand All @@ -7,23 +8,29 @@ const PKG_VER: &str = env!("CARGO_PKG_VERSION", "unknown");
const RUST_VER: &str = env!("CARGO_PKG_RUST_VERSION", "unknown");
const OS: &str = std::env::consts::OS;

fn get_user_agent(app_name: Option<&str>) -> String {
fn get_user_agent(products_info: &Vec<ProductInfo>) -> String {
let default_agent = format!("clickhouse-rs/{PKG_VER} (lv:rust/{RUST_VER}, os:{OS})");
if let Some(app_name) = app_name {
format!("{app_name} {default_agent}")
} else {
if products_info.is_empty() {
default_agent
} else {
let products = products_info
.iter()
.rev()
.map(|product_info| product_info.to_string())
.collect::<Vec<String>>()
.join(" ");
format!("{products} {default_agent}")
}
}

pub(crate) fn with_request_headers(
mut builder: Builder,
headers: &HashMap<String, String>,
app_name: Option<&str>,
products_info: &Vec<ProductInfo>,
) -> Builder {
for (name, value) in headers {
builder = builder.header(name, value);
}
builder = builder.header(USER_AGENT.to_string(), get_user_agent(app_name));
builder = builder.header(USER_AGENT.to_string(), get_user_agent(products_info));
builder
}
2 changes: 1 addition & 1 deletion src/insert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ impl<T> Insert<T> {
drop(pairs);

let mut builder = Request::post(url.as_str());
builder = with_request_headers(builder, &client.headers, client.app_name.as_deref());
builder = with_request_headers(builder, &client.headers, &client.products_info);

if let Some(user) = &client.user {
builder = builder.header("X-ClickHouse-User", user);
Expand Down
61 changes: 52 additions & 9 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
extern crate static_assertions;

use std::{collections::HashMap, sync::Arc, time::Duration};

use std::fmt::Display;
pub use clickhouse_derive::Row;
#[cfg(feature = "tls")]
use hyper_tls::HttpsConnector;
Expand Down Expand Up @@ -60,7 +60,19 @@ pub struct Client {
compression: Compression,
options: HashMap<String, String>,
headers: HashMap<String, String>,
app_name: Option<String>,
products_info: Vec<ProductInfo>,
}

#[derive(Clone)]
pub struct ProductInfo {
pub name: String,
pub version: String,
}

impl Display for ProductInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", format!("{}/{}", self.name, self.version))
}
}

impl Default for Client {
Expand Down Expand Up @@ -106,7 +118,7 @@ impl Client {
compression: Compression::default(),
options: HashMap::new(),
headers: HashMap::new(),
app_name: None,
products_info: Vec::default(),
}
}

Expand Down Expand Up @@ -196,21 +208,52 @@ impl Client {
self
}

/// Specifies the name that will be included in the default User-Agent header.
/// Specifies the product name and version that will be included
/// in the default User-Agent header. Multiple products are supported.
/// This could be useful for the applications built on top of this client.
///
/// # Examples
///
/// Sample default User-Agent header:
///
/// ```plaintext
/// clickhouse-rs/0.12.2 (lv:rust/1.67.0, os:macos)
/// ```
///
/// Sample User-Agent with a single product information:
///
/// ```
/// # use clickhouse::Client;
/// let client = Client::default().with_product_info("MyDataSource", "v1.0.0");
/// ```
///
/// ```plaintext
/// MyDataSource/v1.0.0 clickhouse-rs/0.12.2 (lv:rust/1.67.0, os:macos)
/// ```
///
/// Sample User-Agent with multiple products information
/// (NB: the products are added in the reverse order of [`Client::with_product_info`] calls,
/// which could be useful to add higher abstraction layers first):
///
/// ```
/// # use clickhouse::Client;
/// let client = Client::default().with_app_name("MyApplication");
/// let client = Client::default()
/// .with_product_info("MyDataSource", "v1.0.0")
/// .with_product_info("MyApp", "0.0.1");
/// ```
/// # Sample User-Agent header
///
/// ```plaintext
/// MyApplication clickhouse-rs/0.1.0 (lv:rust/1.55.0, os:linux)
/// MyApp/0.0.1 MyDataSource/v1.0.0 clickhouse-rs/0.12.2 (lv:rust/1.67.0, os:macos)
/// ```
pub fn with_app_name(mut self, app_name: impl Into<String>) -> Self {
self.app_name = Some(app_name.into());
pub fn with_product_info(
mut self,
product_name: impl Into<String>,
product_version: impl Into<String>,
) -> Self {
self.products_info.push(ProductInfo{
name: product_name.into(),
version: product_version.into(),
});
self
}

Expand Down
2 changes: 1 addition & 1 deletion src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ impl Query {
builder = with_request_headers(
builder,
&self.client.headers,
self.client.app_name.as_deref(),
&self.client.products_info,
);

if content_length == 0 {
Expand Down
19 changes: 15 additions & 4 deletions tests/it/user_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,22 @@ async fn default_user_agent() {
}

#[tokio::test]
async fn user_agent_with_app_name() {
let table_name = "chrs_user_agent_with_app_name";
let client = prepare_database!().with_app_name("my-app");
async fn user_agent_with_single_product_info() {
let table_name = "chrs_user_agent_with_single_product_info";
let client = prepare_database!().with_product_info("my-app", "0.1.0");
let expected_user_agent =
format!("my-app clickhouse-rs/{PKG_VER} (lv:rust/{RUST_VER}, os:{OS})");
format!("my-app/0.1.0 clickhouse-rs/{PKG_VER} (lv:rust/{RUST_VER}, os:{OS})");
assert_queries_user_agents(&client, table_name, &expected_user_agent).await;
}

#[tokio::test]
async fn user_agent_with_multiple_product_info() {
let table_name = "chrs_user_agent_with_multiple_product_info";
let client = prepare_database!()
.with_product_info("my-datasource", "2.5.0")
.with_product_info("my-app", "0.1.0");
let expected_user_agent =
format!("my-app/0.1.0 my-datasource/2.5.0 clickhouse-rs/{PKG_VER} (lv:rust/{RUST_VER}, os:{OS})");
assert_queries_user_agents(&client, table_name, &expected_user_agent).await;
}

Expand Down

0 comments on commit eb14de1

Please sign in to comment.