pub mod analyst;
pub mod asset_chart;
pub mod bulk;
pub mod calendar;
pub mod chart;
pub mod commitment_of_traders;
pub mod commodities;
pub mod company;
pub mod congressional;
pub mod crypto;
pub mod dcf;
pub mod directory;
pub mod economics;
pub mod esg;
pub mod forex;
pub mod fundraising;
pub mod funds;
pub mod indexes;
pub mod insider_trading;
pub mod institutional_ownership;
pub mod market;
pub mod market_hours;
pub mod metadata;
pub mod news;
pub mod quote;
pub mod screener;
pub mod search;
pub mod sec_filings;
pub mod statements;
pub mod technical_indicators;
pub mod tipranks;
pub mod transcripts;
use std::{fmt, marker::PhantomData};
use bytes::Bytes;
use serde::de::DeserializeOwned;
use crate::transport::HttpMethod;
use metadata::EndpointMetadata;
pub const APPLICATION_JSON: &str = "application/json";
pub struct EndpointSpec<Q, R> {
method: HttpMethod,
id: &'static str,
relative_path: &'static str,
query: Q,
response: ResponseContract<R>,
metadata: EndpointMetadata,
max_response_body_bytes: Option<usize>,
}
impl<Q, R> EndpointSpec<Q, R> {
pub const fn with_response(
method: HttpMethod,
id: &'static str,
relative_path: &'static str,
query: Q,
response: ResponseContract<R>,
) -> Self {
Self {
method,
id,
relative_path,
query,
response,
metadata: EndpointMetadata::new(),
max_response_body_bytes: None,
}
}
pub const fn id(&self) -> &'static str {
self.id
}
pub const fn relative_path(&self) -> &'static str {
self.relative_path
}
pub const fn method(&self) -> HttpMethod {
self.method
}
pub const fn query(&self) -> &Q {
&self.query
}
pub const fn response(&self) -> &ResponseContract<R> {
&self.response
}
pub const fn metadata(&self) -> EndpointMetadata {
self.metadata
}
pub const fn with_metadata(mut self, metadata: EndpointMetadata) -> Self {
self.metadata = metadata;
self
}
pub const fn with_max_response_body_bytes(mut self, max_bytes: usize) -> Self {
self.max_response_body_bytes = Some(max_bytes);
self
}
pub const fn max_response_body_bytes(&self) -> Option<usize> {
self.max_response_body_bytes
}
}
impl<Q, R> EndpointSpec<Q, R>
where
R: DeserializeOwned,
{
pub const fn get(id: &'static str, relative_path: &'static str, query: Q) -> Self {
Self::with_response(
HttpMethod::Get,
id,
relative_path,
query,
ResponseContract::json(),
)
}
pub const fn new(
method: HttpMethod,
id: &'static str,
relative_path: &'static str,
query: Q,
) -> Self {
Self::with_response(method, id, relative_path, query, ResponseContract::json())
}
}
impl<Q> EndpointSpec<Q, BinaryBody> {
pub const fn get_binary(
id: &'static str,
relative_path: &'static str,
query: Q,
expected_content_types: &'static [&'static str],
) -> Self {
Self::with_response(
HttpMethod::Get,
id,
relative_path,
query,
ResponseContract::binary(expected_content_types),
)
}
}
impl<Q, R> fmt::Debug for EndpointSpec<Q, R> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("EndpointSpec")
.field("method", &self.method)
.field("id", &self.id)
.field("relative_path", &self.relative_path)
.field("query", &"[REDACTED QUERY]")
.field("response", &self.response)
.field("metadata", &self.metadata)
.finish()
}
}
pub struct QueryEncoder<'a> {
visitor: &'a mut dyn FnMut(&str, &str),
}
impl<'a> QueryEncoder<'a> {
pub(crate) fn new(visitor: &'a mut dyn FnMut(&str, &str)) -> Self {
Self { visitor }
}
pub fn required(&mut self, name: &'static str, value: impl fmt::Display) {
let value = value.to_string();
(self.visitor)(name, &value);
}
pub fn optional<T>(&mut self, name: &'static str, value: Option<T>)
where
T: fmt::Display,
{
if let Some(value) = value {
self.required(name, value);
}
}
}
pub trait QueryParameters {
fn encode(&self, encoder: &mut QueryEncoder<'_>);
}
impl QueryParameters for () {
fn encode(&self, _encoder: &mut QueryEncoder<'_>) {}
}
impl QueryParameters for Vec<(String, String)> {
fn encode(&self, encoder: &mut QueryEncoder<'_>) {
for (name, value) in self {
let value = value.as_str();
(encoder.visitor)(name, value);
}
}
}
impl<const N: usize> QueryParameters for [(&str, &str); N] {
fn encode(&self, encoder: &mut QueryEncoder<'_>) {
for (name, value) in self {
(encoder.visitor)(name, value);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ExpectedContentType {
Json,
Binary(&'static [&'static str]),
}
impl ExpectedContentType {
pub(crate) fn matches(self, content_type: &str) -> bool {
let media_type = content_type
.split_once(';')
.map_or(content_type, |(media_type, _)| media_type)
.trim();
match self {
Self::Json => valid_json_media_type(media_type),
Self::Binary(expected) => expected
.iter()
.any(|expected| media_type.eq_ignore_ascii_case(expected)),
}
}
}
fn valid_json_media_type(media_type: &str) -> bool {
let Some((type_name, subtype)) = media_type.split_once('/') else {
return false;
};
if !valid_media_type_token(type_name)
|| !valid_media_type_token(subtype)
|| subtype.contains('/')
{
return false;
}
(type_name.eq_ignore_ascii_case("application") && subtype.eq_ignore_ascii_case("json"))
|| subtype.rsplit_once('+').is_some_and(|(name, suffix)| {
!name.is_empty() && !name.contains('+') && suffix.eq_ignore_ascii_case("json")
})
}
fn valid_media_type_token(value: &str) -> bool {
!value.is_empty()
&& value.bytes().all(|byte| {
byte.is_ascii_alphanumeric()
|| matches!(
byte,
b'!' | b'#'
| b'$'
| b'%'
| b'&'
| b'\''
| b'*'
| b'+'
| b'-'
| b'.'
| b'^'
| b'_'
| b'`'
| b'|'
| b'~'
)
})
}
#[derive(Clone, Copy)]
pub(crate) struct ResponseMetadata<'a> {
content_type: &'a str,
content_disposition: Option<&'a str>,
}
impl<'a> ResponseMetadata<'a> {
pub(crate) const fn new(content_type: &'a str, content_disposition: Option<&'a str>) -> Self {
Self {
content_type,
content_disposition,
}
}
}
type Decoder<R> = for<'a> fn(Bytes, ResponseMetadata<'a>) -> std::result::Result<R, ()>;
pub struct ResponseContract<R> {
expected_content_type: ExpectedContentType,
decoder: Decoder<R>,
response: PhantomData<fn() -> R>,
}
impl<R> ResponseContract<R> {
pub const fn expected_content_type(&self) -> ExpectedContentType {
self.expected_content_type
}
pub(crate) fn decode(
&self,
body: Bytes,
metadata: ResponseMetadata<'_>,
) -> std::result::Result<R, ()> {
(self.decoder)(body, metadata)
}
}
impl<R> ResponseContract<R>
where
R: DeserializeOwned,
{
pub const fn json() -> Self {
Self {
expected_content_type: ExpectedContentType::Json,
decoder: decode_json::<R>,
response: PhantomData,
}
}
}
impl ResponseContract<BinaryBody> {
pub const fn binary(expected_content_types: &'static [&'static str]) -> Self {
Self {
expected_content_type: ExpectedContentType::Binary(expected_content_types),
decoder: decode_binary,
response: PhantomData,
}
}
}
impl<R> fmt::Debug for ResponseContract<R> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ResponseContract")
.field("expected_content_type", &self.expected_content_type)
.field("response_type", &std::any::type_name::<R>())
.finish()
}
}
fn decode_json<R>(body: Bytes, _metadata: ResponseMetadata<'_>) -> std::result::Result<R, ()>
where
R: DeserializeOwned,
{
serde_json::from_slice(&body).map_err(|_| ())
}
#[derive(Clone, PartialEq, Eq)]
pub struct BinaryResponse {
bytes: Bytes,
content_type: Box<str>,
content_disposition: Option<Box<str>>,
}
impl BinaryResponse {
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
pub fn into_bytes(self) -> Vec<u8> {
self.bytes.to_vec()
}
pub fn into_buffer(self) -> Bytes {
self.bytes
}
pub fn content_type(&self) -> &str {
&self.content_type
}
pub fn content_disposition(&self) -> Option<&str> {
self.content_disposition.as_deref()
}
}
impl fmt::Debug for BinaryResponse {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let media_type = self
.content_type
.split_once(';')
.map_or(self.content_type.as_ref(), |(media_type, _)| media_type)
.trim();
formatter
.debug_struct("BinaryResponse")
.field("body_bytes", &self.bytes.len())
.field("media_type", &media_type)
.field(
"has_content_disposition",
&self.content_disposition.is_some(),
)
.finish()
}
}
pub type BinaryBody = BinaryResponse;
fn decode_binary(
body: Bytes,
metadata: ResponseMetadata<'_>,
) -> std::result::Result<BinaryResponse, ()> {
Ok(BinaryResponse {
bytes: body,
content_type: metadata.content_type.into(),
content_disposition: metadata.content_disposition.map(Into::into),
})
}