Skip to content

Commit

Permalink
ADD: Implement Debug and Clone for client types
Browse files Browse the repository at this point in the history
  • Loading branch information
threecgreen committed Mar 1, 2024
1 parent aeebd48 commit 2cac66f
Show file tree
Hide file tree
Showing 8 changed files with 80 additions and 26 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
- Document cancellation safety of `LiveClient` methods (credit: @yongqli)
- Document `live::Subscription::start` is based on `ts_event`
- Allow constructing a `DateRange` and `DateTimeRange` with an `end` based on a `time::Duration`
- Implemented `Debug` for `LiveClient`, `LiveClientBuilder`, `HistoricalClient`,
`HistoricalClientBuilder`, `BatchClient`, `MetadataClient`, `SymbologyClient`, and
`TimeseriesClient`
- Derived `Clone` for `LiveClientBuilder` and `HistoricalClientBuilder`
- Added `ApiKey` type for safely deriving `Debug` for types containing an API key

#### Breaking changes
- Changed default `upgrade_policy` in `LiveBuilder` and `GetRangeParams` to `Upgrade` so
Expand Down
1 change: 1 addition & 0 deletions src/historical/batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use crate::{historical::check_http_error, Error, Symbols};
use super::{handle_response, DateTimeRange};

/// A client for the batch group of Historical API endpoints.
#[derive(Debug)]
pub struct BatchClient<'a> {
pub(crate) inner: &'a mut super::Client,
}
Expand Down
25 changes: 14 additions & 11 deletions src/historical/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use log::warn;
use reqwest::{header::ACCEPT, IntoUrl, RequestBuilder, Url};
use serde::Deserialize;

use crate::{error::ApiError, Error};
use crate::{error::ApiError, ApiKey, Error};

use super::{
batch::BatchClient, metadata::MetadataClient, symbology::SymbologyClient,
Expand All @@ -20,8 +20,9 @@ use super::{
/// - [`timeseries()`](Self::timeseries)
/// - [`symbology()`](Self::symbology)
/// - [`batch()`](Self::batch)
#[derive(Debug, Clone)]
pub struct Client {
key: String,
key: ApiKey,
base_url: Url,
gateway: HistoricalGateway,
client: reqwest::Client,
Expand Down Expand Up @@ -78,7 +79,7 @@ impl Client {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(ACCEPT, "application/json".parse().unwrap());
Ok(Self {
key,
key: ApiKey(key),
base_url,
gateway,
client: reqwest::ClientBuilder::new()
Expand All @@ -90,7 +91,7 @@ impl Client {

/// Returns the API key used by the instance of the client.
pub fn key(&self) -> &str {
&self.key
&self.key.0
}

/// Returns the configured Historical gateway.
Expand Down Expand Up @@ -130,7 +131,7 @@ impl Client {
.join(path)
.map_err(|e| Error::Internal(format!("created invalid URL: {e:?}")))?,
)
.basic_auth(&self.key, Option::<&str>::None))
.basic_auth(self.key(), Option::<&str>::None))
}

pub(crate) fn post(&mut self, slug: &str) -> crate::Result<RequestBuilder> {
Expand All @@ -146,7 +147,7 @@ impl Client {
.join(&format!("v{API_VERSION}/{slug}"))
.map_err(|e| Error::Internal(format!("created invalid URL: {e:?}")))?,
)
.basic_auth(&self.key, Option::<&str>::None))
.basic_auth(self.key(), Option::<&str>::None))
}
}

Expand Down Expand Up @@ -204,10 +205,12 @@ fn check_warnings(response: &reqwest::Response) {
}

#[doc(hidden)]
#[derive(Debug, Copy, Clone)]
pub struct Unset;

/// A type-safe builder for the [`HistoricalClient`](Client). It will not allow you to
/// call [`Self::build()`] before setting the required `key` field.
#[derive(Clone)]
pub struct ClientBuilder<AK> {
key: AK,
base_url: Option<Url>,
Expand Down Expand Up @@ -249,7 +252,7 @@ impl ClientBuilder<Unset> {
///
/// # Errors
/// This function returns an error when the API key is invalid.
pub fn key(self, key: impl ToString) -> crate::Result<ClientBuilder<String>> {
pub fn key(self, key: impl ToString) -> crate::Result<ClientBuilder<ApiKey>> {
Ok(ClientBuilder {
key: crate::validate_key(key.to_string())?,
base_url: self.base_url,
Expand All @@ -263,22 +266,22 @@ impl ClientBuilder<Unset> {
/// # Errors
/// This function returns an error when the environment variable is not set or the
/// API key is invalid.
pub fn key_from_env(self) -> crate::Result<ClientBuilder<String>> {
pub fn key_from_env(self) -> crate::Result<ClientBuilder<ApiKey>> {
let key = crate::key_from_env()?;
self.key(key)
}
}

impl ClientBuilder<String> {
impl ClientBuilder<ApiKey> {
/// Initializes the client.
///
/// # Errors
/// This function returns an error when it fails to build the HTTP client.
pub fn build(self) -> crate::Result<Client> {
if let Some(url) = self.base_url {
Client::with_url(url, self.key, self.gateway)
Client::with_url(url, self.key.0, self.gateway)
} else {
Client::new(self.key, self.gateway)
Client::new(self.key.0, self.gateway)
}
}
}
1 change: 1 addition & 0 deletions src/historical/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::Symbols;
use super::{handle_response, AddToQuery, DateRange, DateTimeRange};

/// A client for the metadata group of Historical API endpoints.
#[derive(Debug)]
pub struct MetadataClient<'a> {
pub(crate) inner: &'a mut super::Client,
}
Expand Down
1 change: 1 addition & 0 deletions src/historical/symbology.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::Symbols;
use super::{handle_response, DateRange};

/// A client for the symbology group of Historical API endpoints.
#[derive(Debug)]
pub struct SymbologyClient<'a> {
pub(crate) inner: &'a mut super::Client,
}
Expand Down
1 change: 1 addition & 0 deletions src/historical/timeseries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use super::{check_http_error, DateTimeRange};
pub use dbn::decode::AsyncDbnDecoder;

/// A client for the timeseries group of Historical API endpoints.
#[derive(Debug)]
pub struct TimeseriesClient<'a> {
pub(crate) inner: &'a mut super::Client,
}
Expand Down
38 changes: 34 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub use live::Client as LiveClient;
// Re-export to keep versions synchronized
pub use dbn;

use std::fmt::{Display, Write};
use std::fmt::{self, Display, Write};

use log::error;
#[cfg(feature = "historical")]
Expand Down Expand Up @@ -160,7 +160,7 @@ impl From<Vec<&str>> for Symbols {
}
}

pub(crate) fn validate_key(key: String) -> crate::Result<String> {
pub(crate) fn validate_key(key: String) -> crate::Result<ApiKey> {
if key == "$YOUR_API_KEY" {
Err(Error::bad_arg(
"key",
Expand All @@ -181,7 +181,7 @@ pub(crate) fn validate_key(key: String) -> crate::Result<String> {
"expected to be composed of only ASCII characters",
))
} else {
Ok(key)
Ok(ApiKey(key))
}
}

Expand Down Expand Up @@ -224,6 +224,23 @@ impl<'de> Deserialize<'de> for Symbols {
}
}

/// A struct for holding an API key that implements Debug, but will only print the last
/// five characters of the key.
#[derive(Clone)]
pub struct ApiKey(String);

pub(crate) const BUCKET_ID_LENGTH: usize = 5;

impl fmt::Debug for ApiKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"\"…{}\"",
&self.0[self.0.len().saturating_sub(BUCKET_ID_LENGTH)..]
)
}
}

#[cfg(test)]
const TEST_DATA_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data");
#[cfg(test)]
Expand All @@ -235,7 +252,7 @@ pub(crate) fn body_contains(
key: impl Display,
val: impl Display,
) -> wiremock::matchers::BodyContainsMatcher {
wiremock::matchers::body_string_contains(&format!("{key}={val}"))
wiremock::matchers::body_string_contains(format!("{key}={val}"))
}

#[cfg(test)]
Expand All @@ -256,4 +273,17 @@ mod tests {
assert_eq!(symbol_res[3], Symbols::Symbols(vec!["TSLA".to_owned()]));
assert_eq!(symbol_res[4], Symbols::Ids(vec![1001]));
}

#[test]
fn test_key_debug_truncates() {
assert_eq!(
format!("{:?}", ApiKey("abcdefghijklmnopqrstuvwxyz".to_owned())),
"\"…vwxyz\""
);
}

#[test]
fn test_key_debug_doesnt_underflow() {
assert_eq!(format!("{:?}", ApiKey("test".to_owned())), "\"…test\"");
}
}
34 changes: 23 additions & 11 deletions src/live.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! The Live client and related API types. Used for both real-time data and intraday historical.
use std::collections::HashMap;
use std::{collections::HashMap, fmt};

use dbn::{
compat::SymbolMappingMsgV1,
Expand All @@ -17,14 +17,14 @@ use tokio::{
};
use typed_builder::TypedBuilder;

use crate::{validate_key, Error, Symbols, API_KEY_LENGTH};
use crate::{validate_key, ApiKey, Error, Symbols, API_KEY_LENGTH, BUCKET_ID_LENGTH};

/// The Live client. Used for subscribing to real-time and intraday historical market data.
///
/// Use [`LiveClient::builder()`](Client::builder) to get a type-safe builder for
/// initializing the required parameters for the client.
pub struct Client {
key: String,
key: ApiKey,
dataset: String,
send_ts_out: bool,
upgrade_policy: VersionUpgradePolicy,
Expand All @@ -33,8 +33,6 @@ pub struct Client {
session_id: String,
}

const BUCKET_ID_LENGTH: usize = 5;

impl Client {
/// Returns a type-safe builder for setting the required parameters
/// for initializing a [`LiveClient`](Client).
Expand Down Expand Up @@ -83,7 +81,7 @@ impl Client {

// Authenticate CRAM
let session_id =
Self::cram_challenge(&mut reader, &mut writer, &key, &dataset, send_ts_out).await?;
Self::cram_challenge(&mut reader, &mut writer, &key.0, &dataset, send_ts_out).await?;

Ok(Self {
key,
Expand All @@ -101,7 +99,7 @@ impl Client {

/// Returns the API key used by the instance of the client.
pub fn key(&self) -> &str {
&self.key
&self.key.0
}

/// Returns the dataset the client is configured for.
Expand Down Expand Up @@ -327,12 +325,14 @@ pub struct Subscription {
}

#[doc(hidden)]
#[derive(Debug, Copy, Clone)]
pub struct Unset;

/// A type-safe builder for the [`LiveClient`](Client). It will not allow you to call
/// [`Self::build()`] before setting the required fields:
/// - `key`
/// - `dataset`
#[derive(Debug, Clone)]
pub struct ClientBuilder<AK, D> {
key: AK,
dataset: D,
Expand Down Expand Up @@ -379,7 +379,7 @@ impl<D> ClientBuilder<Unset, D> {
///
/// # Errors
/// This function returns an error when the API key is invalid.
pub fn key(self, key: impl ToString) -> crate::Result<ClientBuilder<String, D>> {
pub fn key(self, key: impl ToString) -> crate::Result<ClientBuilder<ApiKey, D>> {
Ok(ClientBuilder {
key: crate::validate_key(key.to_string())?,
dataset: self.dataset,
Expand All @@ -394,7 +394,7 @@ impl<D> ClientBuilder<Unset, D> {
/// # Errors
/// This function returns an error when the environment variable is not set or the
/// API key is invalid.
pub fn key_from_env(self) -> crate::Result<ClientBuilder<String, D>> {
pub fn key_from_env(self) -> crate::Result<ClientBuilder<ApiKey, D>> {
let key = crate::key_from_env()?;
self.key(key)
}
Expand All @@ -412,15 +412,15 @@ impl<AK> ClientBuilder<AK, Unset> {
}
}

impl ClientBuilder<String, String> {
impl ClientBuilder<ApiKey, String> {
/// Initializes the client and attempts to connect to the gateway.
///
/// # Errors
/// This function returns an error when its unable
/// to connect and authenticate with the Live gateway.
pub async fn build(self) -> crate::Result<Client> {
Client::connect(
self.key,
self.key.0,
self.dataset,
self.send_ts_out,
self.upgrade_policy,
Expand Down Expand Up @@ -501,6 +501,18 @@ impl std::ops::Index<u32> for SymbolMap {
}
}

impl fmt::Debug for Client {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LiveClient")
.field("key", &self.key)
.field("dataset", &self.dataset)
.field("send_ts_out", &self.send_ts_out)
.field("upgrade_policy", &self.upgrade_policy)
.field("session_id", &self.session_id)
.finish_non_exhaustive()
}
}

#[cfg(test)]
#[allow(deprecated)]
mod tests {
Expand Down

0 comments on commit 2cac66f

Please sign in to comment.