use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
use reqwest::StatusCode;
use crate::error::{FinanceError, Result};
#[cfg(any(feature = "binance", feature = "kraken"))]
pub(crate) mod coins;
#[cfg(any(feature = "binance", feature = "kraken"))]
pub(crate) mod crypto_chart;
pub(crate) mod date_range;
#[cfg(any(
feature = "alphavantage",
feature = "bls",
feature = "fmp",
feature = "fred",
feature = "polygon"
))]
pub(crate) mod keyed;
#[cfg(any(feature = "bls", feature = "fiscaldata"))]
pub(crate) mod numbers;
#[cfg(any(feature = "bls", feature = "worldbank"))]
pub(crate) mod periods;
#[cfg(any(feature = "alphavantage", feature = "fmp"))]
pub(crate) mod percent;
const PATH_SEGMENT_ENCODE_SET: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'"')
.add(b'#')
.add(b'<')
.add(b'>')
.add(b'?')
.add(b'`')
.add(b'{')
.add(b'}')
.add(b'/')
.add(b'%')
.add(b':')
.add(b';')
.add(b'=')
.add(b'@')
.add(b'[')
.add(b'\\')
.add(b']')
.add(b'^')
.add(b'|')
.add(b'!')
.add(b'$')
.add(b'&')
.add(b'\'')
.add(b'(')
.add(b')')
.add(b'*')
.add(b'+')
.add(b',');
#[allow(dead_code)] pub(crate) fn encode_path_segment(segment: &str) -> String {
utf8_percent_encode(segment, PATH_SEGMENT_ENCODE_SET).to_string()
}
#[allow(dead_code)] pub(crate) fn user_agent() -> String {
format!(
"finance-query/{} (https://github.com/Verdenroz/finance-query)",
env!("CARGO_PKG_VERSION")
)
}
#[allow(dead_code)] pub(crate) fn keyless_http_client(timeout: std::time::Duration) -> Result<reqwest::Client> {
Ok(reqwest::Client::builder()
.timeout(timeout)
.user_agent(user_agent())
.build()?)
}
#[allow(dead_code)] pub(crate) fn status_error(api: &'static str, status: StatusCode) -> FinanceError {
match status {
StatusCode::TOO_MANY_REQUESTS => FinanceError::RateLimited { retry_after: None },
s => FinanceError::ExternalApiError {
api: api.to_string(),
status: s.as_u16(),
},
}
}
#[allow(dead_code)] pub(crate) fn check_status(api: &'static str, status: StatusCode) -> Result<()> {
if status.is_success() {
return Ok(());
}
Err(status_error(api, status))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plain_symbol_unchanged() {
assert_eq!(encode_path_segment("AAPL"), "AAPL");
}
#[test]
fn dot_separated_ticker_unchanged() {
assert_eq!(encode_path_segment("BRK.B"), "BRK.B");
}
#[test]
fn question_mark_is_encoded() {
assert_eq!(encode_path_segment("FOO?bar"), "FOO%3Fbar");
}
#[test]
fn hash_is_encoded() {
assert_eq!(encode_path_segment("FOO#bar"), "FOO%23bar");
}
#[test]
fn slash_is_encoded() {
assert_eq!(encode_path_segment("a/b"), "a%2Fb");
}
#[test]
fn space_is_encoded() {
assert_eq!(encode_path_segment("a b"), "a%20b");
}
#[test]
fn dot_dot_is_preserved_literally() {
assert_eq!(encode_path_segment(".."), "..");
}
#[test]
fn rate_limit_status_maps_to_rate_limited() {
assert!(matches!(
status_error("Test", StatusCode::TOO_MANY_REQUESTS),
FinanceError::RateLimited { retry_after: None }
));
}
#[test]
fn other_failures_carry_the_api_name_and_status() {
match status_error("Test", StatusCode::BAD_GATEWAY) {
FinanceError::ExternalApiError { api, status } => {
assert_eq!(api, "Test");
assert_eq!(status, 502);
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn success_statuses_pass_check() {
assert!(check_status("Test", StatusCode::OK).is_ok());
assert!(check_status("Test", StatusCode::NO_CONTENT).is_ok());
assert!(check_status("Test", StatusCode::NOT_FOUND).is_err());
}
#[test]
fn dot_dot_slash_is_encoded() {
assert_eq!(encode_path_segment("../foo"), "..%2Ffoo");
}
}