pub mod calendar;
pub mod chart;
pub mod company;
pub mod directory;
pub mod economics;
pub mod metadata;
pub mod quote;
pub mod screener;
pub mod search;
pub mod statements;
pub mod transcripts;
use std::{fmt, marker::PhantomData};
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,
}
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(),
}
}
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
}
}
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 => {
media_type.eq_ignore_ascii_case(APPLICATION_JSON)
|| media_type
.to_ascii_lowercase()
.strip_suffix("+json")
.is_some_and(|prefix| prefix.contains('/'))
}
Self::Binary(expected) => expected
.iter()
.any(|expected| media_type.eq_ignore_ascii_case(expected)),
}
}
}
#[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(&[u8], 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: &[u8],
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: &[u8], _metadata: ResponseMetadata<'_>) -> std::result::Result<R, ()>
where
R: DeserializeOwned,
{
serde_json::from_slice(body).map_err(|_| ())
}
#[derive(Clone, PartialEq, Eq)]
pub struct BinaryResponse {
bytes: Vec<u8>,
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
}
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: &[u8],
metadata: ResponseMetadata<'_>,
) -> std::result::Result<BinaryResponse, ()> {
Ok(BinaryResponse {
bytes: body.to_vec(),
content_type: metadata.content_type.into(),
content_disposition: metadata.content_disposition.map(Into::into),
})
}