1#![doc = include_str!("../README.md")]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4#![deny(missing_docs)]
5#![deny(rustdoc::broken_intra_doc_links)]
6#![deny(clippy::missing_errors_doc)]
7
8#[cfg(feature = "historical")]
9mod deserialize;
10pub mod error;
11#[cfg(feature = "historical")]
12pub mod historical;
13#[cfg(feature = "live")]
14pub mod live;
15#[cfg(feature = "historical")]
16pub mod reference;
17
18pub use error::{Error, Result};
19#[cfg(feature = "historical")]
20#[doc(inline)]
21pub use historical::Client as HistoricalClient;
22#[cfg(feature = "live")]
23#[doc(inline)]
24pub use live::Client as LiveClient;
25#[cfg(feature = "historical")]
26#[doc(inline)]
27pub use reference::Client as ReferenceClient;
28pub use dbn;
30
31use std::{
32 fmt::{self, Display, Write},
33 sync::LazyLock,
34};
35
36#[cfg(feature = "historical")]
37use serde::{Deserialize, Deserializer};
38use tracing::error;
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum Symbols {
43 All,
45 Ids(Vec<u32>),
47 Symbols(Vec<String>),
49}
50
51const ALL_SYMBOLS: &str = "ALL_SYMBOLS";
52const API_KEY_LENGTH: usize = 32;
53
54impl Symbols {
55 pub fn to_api_string(&self) -> String {
57 match self {
58 Symbols::All => ALL_SYMBOLS.to_owned(),
59 Symbols::Ids(ids) => ids.iter().fold(String::new(), |mut acc, s| {
60 if acc.is_empty() {
61 s.to_string()
62 } else {
63 write!(acc, ",{s}").unwrap();
64 acc
65 }
66 }),
67 Symbols::Symbols(symbols) => symbols.join(","),
68 }
69 }
70
71 #[cfg(feature = "live")]
72 pub fn to_chunked_api_string(&self) -> Vec<String> {
75 const CHUNK_SIZE: usize = 500;
76 match self {
77 Symbols::All => vec![ALL_SYMBOLS.to_owned()],
78 Symbols::Ids(ids) => ids
79 .chunks(CHUNK_SIZE)
80 .map(|chunk| {
81 chunk.iter().fold(String::new(), |mut acc, s| {
82 if acc.is_empty() {
83 s.to_string()
84 } else {
85 write!(acc, ",{s}").unwrap();
86 acc
87 }
88 })
89 })
90 .collect(),
91 Symbols::Symbols(symbols) => symbols
92 .chunks(CHUNK_SIZE)
93 .map(|chunk| chunk.join(","))
94 .collect(),
95 }
96 }
97}
98
99impl Display for Symbols {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 match self {
102 Symbols::All => f.write_str(ALL_SYMBOLS),
103 Symbols::Ids(ids) => {
104 for (i, id) in ids.iter().enumerate() {
105 if i == 0 {
106 write!(f, "{id}")?;
107 } else {
108 write!(f, ", {id}")?;
109 }
110 }
111 Ok(())
112 }
113 Symbols::Symbols(symbols) => {
114 for (i, sym) in symbols.iter().enumerate() {
115 if i == 0 {
116 write!(f, "{sym}")?;
117 } else {
118 write!(f, ", {sym}")?;
119 }
120 }
121 Ok(())
122 }
123 }
124 }
125}
126
127impl From<&str> for Symbols {
128 fn from(value: &str) -> Self {
129 Symbols::Symbols(vec![value.to_owned()])
130 }
131}
132
133impl From<u32> for Symbols {
134 fn from(value: u32) -> Self {
135 Symbols::Ids(vec![value])
136 }
137}
138
139impl From<Vec<u32>> for Symbols {
140 fn from(value: Vec<u32>) -> Self {
141 Symbols::Ids(value)
142 }
143}
144
145impl From<String> for Symbols {
146 fn from(value: String) -> Self {
147 Symbols::Symbols(vec![value])
148 }
149}
150
151impl From<Vec<String>> for Symbols {
152 fn from(value: Vec<String>) -> Self {
153 Symbols::Symbols(value)
154 }
155}
156
157impl<const N: usize> From<[&str; N]> for Symbols {
158 fn from(value: [&str; N]) -> Self {
159 Symbols::Symbols(value.iter().map(ToString::to_string).collect())
160 }
161}
162
163impl From<&[&str]> for Symbols {
164 fn from(value: &[&str]) -> Self {
165 Symbols::Symbols(value.iter().map(ToString::to_string).collect())
166 }
167}
168
169impl From<Vec<&str>> for Symbols {
170 fn from(value: Vec<&str>) -> Self {
171 Symbols::Symbols(value.into_iter().map(ToOwned::to_owned).collect())
172 }
173}
174
175pub(crate) fn key_from_env() -> crate::Result<String> {
176 std::env::var("DATABENTO_API_KEY").map_err(|e| {
177 Error::bad_arg(
178 "key",
179 match e {
180 std::env::VarError::NotPresent => "tried to read API key from environment variable DATABENTO_API_KEY but it is not set",
181 std::env::VarError::NotUnicode(_) => {
182 "environment variable DATABENTO_API_KEY contains invalid unicode"
183 }
184 },
185 )
186 })
187}
188
189#[cfg(feature = "historical")]
190impl<'de> Deserialize<'de> for Symbols {
191 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
192 where
193 D: Deserializer<'de>,
194 {
195 #[derive(Deserialize)]
196 #[serde(untagged)]
197 enum Helper {
198 Id(u32),
199 Ids(Vec<u32>),
200 Symbol(String),
201 Symbols(Vec<String>),
202 }
203 let ir = Helper::deserialize(deserializer)?;
204 Ok(match ir {
205 Helper::Id(id) => Symbols::Ids(vec![id]),
206 Helper::Ids(ids) => Symbols::Ids(ids),
207 Helper::Symbol(symbol) if symbol == ALL_SYMBOLS => Symbols::All,
208 Helper::Symbol(symbol) => Symbols::Symbols(vec![symbol]),
209 Helper::Symbols(symbols) => Symbols::Symbols(symbols),
210 })
211 }
212}
213
214#[derive(Clone)]
217pub struct ApiKey(String);
218
219pub(crate) const BUCKET_ID_LENGTH: usize = 5;
220
221impl fmt::Debug for ApiKey {
222 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223 write!(
224 f,
225 "\"…{}\"",
226 &self.0[self.0.len().saturating_sub(BUCKET_ID_LENGTH)..]
227 )
228 }
229}
230
231impl ApiKey {
232 pub fn new(key: String) -> crate::Result<ApiKey> {
237 if key == "$YOUR_API_KEY" {
238 Err(Error::bad_arg(
239 "key",
240 "got placeholder API key '$YOUR_API_KEY'. Please pass a real API key",
241 ))
242 } else if key.len() != API_KEY_LENGTH {
243 Err(Error::bad_arg(
244 "key",
245 format!(
246 "expected to be 32-characters long, got {} characters",
247 key.len()
248 ),
249 ))
250 } else if !key.is_ascii() {
251 error!("API key '{key}' contains non-ASCII characters");
252 Err(Error::bad_arg(
253 "key",
254 "expected to be composed of only ASCII characters",
255 ))
256 } else {
257 Ok(ApiKey(key))
258 }
259 }
260
261 #[cfg(feature = "live")]
263 pub fn bucket_id(&self) -> &str {
264 &self.0[API_KEY_LENGTH - BUCKET_ID_LENGTH..]
266 }
267
268 pub fn as_str(&self) -> &str {
270 self.0.as_str()
271 }
272}
273
274#[cfg(test)]
275const TEST_DATA_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data");
276#[cfg(test)]
277pub(crate) fn zst_test_data_path(schema: dbn::enums::Schema) -> String {
278 format!("{TEST_DATA_PATH}/test_data.{}.dbn.zst", schema.as_str())
279}
280#[cfg(test)]
281pub(crate) fn body_contains(
282 key: impl Display,
283 val: impl Display,
284) -> wiremock::matchers::BodyContainsMatcher {
285 wiremock::matchers::body_string_contains(format!("{key}={val}"))
286}
287
288static USER_AGENT: LazyLock<String> = LazyLock::new(|| {
289 format!(
290 "Databento/{} Rust {}-{}",
291 env!("CARGO_PKG_VERSION"),
292 std::env::consts::OS,
293 std::env::consts::ARCH,
294 )
295});
296
297pub trait DateTimeLike {
299 fn to_date_time(self) -> time::OffsetDateTime;
301}
302
303impl DateTimeLike for time::OffsetDateTime {
304 fn to_date_time(self) -> time::OffsetDateTime {
305 self
306 }
307}
308
309impl DateTimeLike for time::Date {
310 fn to_date_time(self) -> time::OffsetDateTime {
311 self.with_time(time::Time::MIDNIGHT).assume_utc()
312 }
313}
314
315#[cfg(feature = "chrono")]
316impl<Tz: chrono::TimeZone> DateTimeLike for chrono::DateTime<Tz> {
317 fn to_date_time(self) -> time::OffsetDateTime {
318 time::OffsetDateTime::from_unix_timestamp_nanos(
322 self.to_utc().timestamp_nanos_opt().unwrap() as i128,
323 )
324 .unwrap()
325 }
326}
327
328#[cfg(feature = "chrono")]
329impl DateTimeLike for chrono::NaiveDate {
330 fn to_date_time(self) -> time::OffsetDateTime {
331 use chrono::Datelike;
332 time::Date::from_calendar_date(
334 self.year(),
335 time::Month::try_from(self.month() as u8).unwrap(),
336 self.day() as u8,
337 )
338 .unwrap()
339 .with_time(time::Time::MIDNIGHT)
340 .assume_utc()
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 #[test]
349 fn test_deserialize_symbols() {
350 const JSON: &str = r#"["ALL_SYMBOLS", [1, 2, 3], ["ESZ3", "CLZ3"], "TSLA", 1001]"#;
351 let symbol_res: Vec<Symbols> = serde_json::from_str(JSON).unwrap();
352 assert_eq!(symbol_res.len(), 5);
353 assert_eq!(symbol_res[0], Symbols::All);
354 assert_eq!(symbol_res[1], Symbols::Ids(vec![1, 2, 3]));
355 assert_eq!(
356 symbol_res[2],
357 Symbols::Symbols(vec!["ESZ3".to_owned(), "CLZ3".to_owned()])
358 );
359 assert_eq!(symbol_res[3], Symbols::Symbols(vec!["TSLA".to_owned()]));
360 assert_eq!(symbol_res[4], Symbols::Ids(vec![1001]));
361 }
362
363 #[test]
364 fn test_key_debug_truncates() {
365 assert_eq!(
366 format!("{:?}", ApiKey("abcdefghijklmnopqrstuvwxyz".to_owned())),
367 "\"…vwxyz\""
368 );
369 }
370
371 #[test]
372 fn test_key_debug_doesnt_underflow() {
373 assert_eq!(format!("{:?}", ApiKey("test".to_owned())), "\"…test\"");
374 }
375}