Skip to main content

ccdata_api/
utils.rs

1use serde::de::DeserializeOwned;
2use reqwest::Response;
3use crate::error::Error;
4use crate::{CCUnit, CCAPIEndpoint};
5use crate::schemas::data_api::spot::CCSpotInstrumentStatus;
6use crate::schemas::data_api::news::{CCNewsLang, CCNewsSourceID, CCNewsSourceType, CCNewsStatus};
7
8
9#[cfg(feature = "debug")]
10/// Checks whether the `CCDATA_API_DEBUG` environment variable is enabled.
11/// Note: If `CCDATA_API_DEBUG` variable exists, the reponses will be printed to command line.
12fn debug() -> Result<bool, Error> {
13    dotenv::dotenv()?;
14    let debug: bool = serde_json::from_str(&std::env::var("CCDATA_API_DEBUG")?)?;
15    Ok(debug)
16}
17
18
19/// Market interface that ensures all market enums implement .to_string().
20pub trait Market {
21    /// Converts enum value to `String`.
22    fn to_string(&self) -> String;
23}
24
25
26/// Convert a vector of inputs into a string of inputs suitable for use in REST API requests.
27fn vec_to_str(vec: &Vec<String>) -> String {
28    let mut s: String = String::from("");
29    for v in vec {
30        s.push_str(&v);
31        s.push_str(&",");
32    }
33    s.pop();
34    s
35}
36
37
38/// Converts a vector of optional parameters into a string.
39fn optional_vec_arguments(o: Option<Vec<String>>, api_argument: String) -> String {
40    let mut s: String = String::from("");
41    match o {
42        Some(v) => {
43            s.push_str(&api_argument);
44            s.push_str(&vec_to_str(&v));
45        },
46        None => (),
47    }
48    s
49}
50
51
52/// All possible parameter types for the REST API request.
53pub enum Param<'a> {
54    // Instrument parameters
55    /// Asset symbol
56    Symbol { v: &'a str },
57    /// Instrument symbol
58    Instrument { v: &'a str },
59    /// List of instrument symbols
60    Instruments { v: &'a Vec<String> },
61    /// Chain asset symbol
62    ChainAsset { v: &'a str },
63    /// Asset symbol
64    Asset {v: &'a str},
65    /// Assets' symbols
66    Assets {v: Vec<String>},
67    // Overlapping parameters
68    /// Final timestamp up to which the data will be extracted
69    ToTs {v: Option<i64>},
70    /// Final timestamp up to which the data will be extracted
71    ToTimestamp { v: Option<i64> },
72    /// Maximum number of datapoints per API endpoint call
73    Limit { v: Option<usize> },
74    // Special parameters
75    /// Market name
76    Market {v: String},
77    /// Status of the instrument (e.g., `ACTIVE`, `EXPIRED`)
78    InstrumentStatus { v: CCSpotInstrumentStatus },
79    /// Block number on the blockchain
80    OCCoreBlockNumber { v: i64 },
81    /// Blockchain address
82    OCCoreAddress { v: &'a str },
83    /// Asset to quote data in
84    OCCoreQuoteAsset { v: &'a str },
85    /// Language of the news
86    NewsLanguage { v: CCNewsLang },
87    /// Source ID of the news stream
88    NewsSourceID { v: CCNewsSourceID },
89    /// List of news categories
90    NewsCategories { v: Option<Vec<String>> },
91    /// List of news categories to exclude
92    NewsExcludeCategories { v: Option<Vec<String>> },
93    /// Type of news stream
94    NewsSourceType { v: CCNewsSourceType },
95    /// Status of the news stream (e.g., `ACTIVE`, `INACTIVE`)
96    NewsStatus { v: CCNewsStatus },
97}
98
99impl<'a> Param<'a> {
100    fn add_param_to_url(&self, url: &mut String) -> () {
101        let url_param: String = match self {
102            // Instrument parameters
103            Self::Symbol { v } => format!("&fsym={}", v),
104            Self::Instrument { v } => format!("&instrument={}", v),
105            Self::Instruments { v } => format!("&instruments={}", vec_to_str(v.to_owned())),
106            Self::ChainAsset { v } => format!("&chain_asset={}", v),
107            Self::Asset { v } => format!("&asset={}", v),
108            Self::Assets { v } => format!("&assets={}", v.join(",")),
109            // Overlapping parameters
110            Self::ToTs { v } => match v {
111                Some(v_) => format!("&toTs={}", v_),
112                None => String::from(""),
113            },
114            Self::ToTimestamp { v } => match v {
115                Some(v_) => format!("&to_ts={}", v_),
116                None => String::from(""),
117            },
118            Self::Limit { v } => match v {
119                Some(v_) => format!("&limit={}", v_),
120                None => String::from("&limit=2000"),
121            },
122            // Special parameters
123            Self::Market { v } => format!("&market={}", v),
124            Self::InstrumentStatus { v } => format!("&instrument_status={}", v.to_string()),
125            Self::OCCoreBlockNumber { v } => format!("&block_number={}", v),
126            Self::OCCoreAddress { v } => format!("&address={}", v),
127            Self::OCCoreQuoteAsset { v } => format!("&quote_asset={}", v),
128            Self::NewsLanguage { v } => format!("&lang={}", v.to_string()),
129            Self::NewsSourceID { v } => format!("&source_ids={}", v.to_string()),
130            Self::NewsCategories { v } => optional_vec_arguments(v.to_owned(), String::from("&categories=")),
131            Self::NewsExcludeCategories { v } => optional_vec_arguments(v.to_owned(), String::from("&exclude_categories=")),
132            Self::NewsSourceType { v } => format!("&source_type={}", v.to_string()),
133            Self::NewsStatus { v } => format!("&status={}", v.to_string()),
134        };
135        url.push_str(&url_param);
136    }
137}
138
139
140/// Make a request to the provided URL, validate the status code of the response, and return deserialized data.
141async fn process_request<T: DeserializeOwned>(url: String) -> Result<T, Error> {
142    let response: Response = reqwest::get(url).await?;
143    let response_body: String = response.text().await?;
144    // Print response body to the command line
145    #[cfg(feature = "debug")]
146    if debug()? { println!("{:?}", response_body) }
147    // Replace empty object and empty list placeholders
148    let response_body: String = response_body.replace("{}", "null");
149    // let response_body: String = response_body.replace("[]", "null");
150    let data: T = serde_json::from_str(&response_body)?;
151    Ok(data)
152}
153
154
155/// Constructs a URL for API request, sends the request, and returns the deserialzied response.
156///
157/// # Input
158/// - `api_key`: CoinDesk API key
159/// - `endpoint`: Enum that represents the CoinDesk API endpoint for function to send the request to
160/// - `unit`: Unit for data to be binned by (e.g., `Day`, `Hour`)
161/// - `params`: List of parameters expected by the CoinDesk API endpoint
162/// - `additional_params`: Additional parameters to add to the request
163pub async fn call_api_endpoint<'a, R: DeserializeOwned>(
164    api_key: &str, endpoint: CCAPIEndpoint, unit: CCUnit, params: Vec<Param<'a>>, additional_params: Option<String>) -> Result<R, Error>
165{
166    // Set up a URL for the API endpoint
167    let mut url: String = endpoint.url(&unit);
168    // Add parameters to the URL
169    url.push_str(&format!("?api_key={}", api_key));
170    for param in params {
171        param.add_param_to_url(&mut url);
172    }
173    // Add additional parameters to the URL
174    match additional_params {
175        Some(v) => url.push_str(&v),
176        None => (),
177    }
178    // Process API response
179    process_request::<R>(url).await
180}