1mod merge;
5
6use std::num::NonZeroU64;
7
8#[cfg(not(feature = "python"))]
11use dbn_macros::MockPyo3;
12
13use merge::MetadataMerger;
14#[cfg(feature = "serde")]
15use serde::Deserialize;
16
17use crate::{
18 compat::version_symbol_cstr_len, record::as_u8_slice, PitSymbolMap, SType, Schema, TsSymbolMap,
19 VersionUpgradePolicy,
20};
21
22#[derive(Debug, Clone, PartialEq, Eq)]
25#[cfg_attr(
26 feature = "python",
27 pyo3::pyclass(eq, from_py_object, module = "databento_dbn")
28)]
29#[cfg_attr(not(feature = "python"), derive(MockPyo3))] pub struct Metadata {
31 #[pyo3(get)]
34 pub version: u8,
35 #[pyo3(get)]
37 pub dataset: String,
38 #[pyo3(get)]
41 pub schema: Option<Schema>,
42 #[pyo3(get)]
45 pub start: u64,
46 #[pyo3(get)]
49 pub end: Option<NonZeroU64>,
50 #[pyo3(get)]
52 pub limit: Option<NonZeroU64>,
53 #[pyo3(get)]
56 pub stype_in: Option<SType>,
57 #[pyo3(get)]
59 pub stype_out: SType,
60 #[pyo3(get)]
63 pub ts_out: bool,
64 #[pyo3(get)]
67 pub symbol_cstr_len: usize,
68 #[pyo3(get)]
70 pub symbols: Vec<String>,
71 #[pyo3(get)]
73 pub partial: Vec<String>,
74 #[pyo3(get)]
76 pub not_found: Vec<String>,
77 pub mappings: Vec<SymbolMapping>,
79}
80
81impl Metadata {
82 pub fn builder() -> MetadataBuilder<Unset, Unset, Unset, Unset, Unset> {
86 MetadataBuilder::default()
87 }
88
89 pub fn start(&self) -> time::OffsetDateTime {
91 time::OffsetDateTime::from_unix_timestamp_nanos(self.start as i128).unwrap()
93 }
94
95 pub fn end(&self) -> Option<time::OffsetDateTime> {
98 self.end
99 .map(|end| time::OffsetDateTime::from_unix_timestamp_nanos(end.get() as i128).unwrap())
100 }
101
102 pub fn symbol_map_for_date(&self, date: time::Date) -> crate::Result<PitSymbolMap> {
114 PitSymbolMap::from_metadata(self, date)
115 }
116
117 pub fn symbol_map(&self) -> crate::Result<TsSymbolMap> {
126 TsSymbolMap::from_metadata(self)
127 }
128
129 pub fn upgrade(&mut self, upgrade_policy: VersionUpgradePolicy) {
131 if self.version < 2 {
132 match upgrade_policy {
133 VersionUpgradePolicy::AsIs => {
134 self.symbol_cstr_len = crate::v1::SYMBOL_CSTR_LEN;
135 }
136 VersionUpgradePolicy::UpgradeToV2 => {
137 self.version = 2;
138 self.symbol_cstr_len = crate::v2::SYMBOL_CSTR_LEN;
139 }
140 VersionUpgradePolicy::UpgradeToV3 => {
141 self.version = 3;
142 self.symbol_cstr_len = crate::v3::SYMBOL_CSTR_LEN;
143 }
144 }
145 } else if self.version == 2 && upgrade_policy == VersionUpgradePolicy::UpgradeToV3 {
146 self.version = 3;
147 }
148 }
149
150 pub fn merge(self, other: impl IntoIterator<Item = Metadata>) -> crate::Result<Self> {
169 let mut merger = MetadataMerger::new(self);
170 for metadata in other {
171 merger.merge(metadata)?;
172 }
173 Ok(merger.finalize())
174 }
175
176 pub fn is_inverse(&self) -> crate::Result<bool> {
183 match (self.stype_in, self.stype_out) {
184 (_, SType::InstrumentId) => Ok(false),
185 (Some(SType::InstrumentId), _) => Ok(true),
186 _ => {
187 Err(crate::Error::BadArgument {
188 param_name: "self".to_owned(),
189 desc: "Can only create symbol maps from metadata where either stype_out or stype_in is instrument ID".to_owned(),
190 })
191 }
192 }
193 }
194}
195
196#[derive(Debug)]
208pub struct MetadataBuilder<D, Sch, Start, StIn, StOut> {
209 version: u8,
210 dataset: D,
211 schema: Sch,
212 start: Start,
213 end: Option<NonZeroU64>,
214 limit: Option<NonZeroU64>,
215 stype_in: StIn,
216 stype_out: StOut,
217 ts_out: bool,
218 symbols: Vec<String>,
219 partial: Vec<String>,
220 not_found: Vec<String>,
221 mappings: Vec<SymbolMapping>,
222}
223
224pub struct Unset {}
226
227impl MetadataBuilder<Unset, Unset, Unset, Unset, Unset> {
228 pub fn new() -> Self {
230 Self::default()
231 }
232}
233
234impl AsRef<[u8]> for Metadata {
235 fn as_ref(&self) -> &[u8] {
236 unsafe { as_u8_slice(self) }
237 }
238}
239
240impl<D, Sch, Start, StIn, StOut> MetadataBuilder<D, Sch, Start, StIn, StOut> {
241 pub fn version(mut self, version: u8) -> Self {
243 self.version = version;
244 self
245 }
246
247 pub fn dataset(
249 self,
250 dataset: impl ToString,
251 ) -> MetadataBuilder<String, Sch, Start, StIn, StOut> {
252 MetadataBuilder {
253 version: self.version,
254 dataset: dataset.to_string(),
255 schema: self.schema,
256 start: self.start,
257 end: self.end,
258 limit: self.limit,
259 stype_in: self.stype_in,
260 stype_out: self.stype_out,
261 ts_out: self.ts_out,
262 symbols: self.symbols,
263 partial: self.partial,
264 not_found: self.not_found,
265 mappings: self.mappings,
266 }
267 }
268
269 pub fn schema(
271 self,
272 schema: Option<Schema>,
273 ) -> MetadataBuilder<D, Option<Schema>, Start, StIn, StOut> {
274 MetadataBuilder {
275 version: self.version,
276 dataset: self.dataset,
277 schema,
278 start: self.start,
279 end: self.end,
280 limit: self.limit,
281 stype_in: self.stype_in,
282 stype_out: self.stype_out,
283 ts_out: self.ts_out,
284 symbols: self.symbols,
285 partial: self.partial,
286 not_found: self.not_found,
287 mappings: self.mappings,
288 }
289 }
290
291 pub fn start(self, start: u64) -> MetadataBuilder<D, Sch, u64, StIn, StOut> {
293 MetadataBuilder {
294 version: self.version,
295 dataset: self.dataset,
296 schema: self.schema,
297 start,
298 end: self.end,
299 limit: self.limit,
300 stype_in: self.stype_in,
301 stype_out: self.stype_out,
302 symbols: self.symbols,
303 ts_out: self.ts_out,
304 partial: self.partial,
305 not_found: self.not_found,
306 mappings: self.mappings,
307 }
308 }
309
310 pub fn end(mut self, end: Option<NonZeroU64>) -> Self {
312 self.end = end;
313 self
314 }
315
316 pub fn limit(mut self, limit: Option<NonZeroU64>) -> Self {
318 self.limit = limit;
319 self
320 }
321
322 pub fn stype_in(
324 self,
325 stype_in: Option<SType>,
326 ) -> MetadataBuilder<D, Sch, Start, Option<SType>, StOut> {
327 MetadataBuilder {
328 version: self.version,
329 dataset: self.dataset,
330 schema: self.schema,
331 start: self.start,
332 end: self.end,
333 limit: self.limit,
334 stype_in,
335 stype_out: self.stype_out,
336 ts_out: self.ts_out,
337 symbols: self.symbols,
338 partial: self.partial,
339 not_found: self.not_found,
340 mappings: self.mappings,
341 }
342 }
343
344 pub fn stype_out(self, stype_out: SType) -> MetadataBuilder<D, Sch, Start, StIn, SType> {
346 MetadataBuilder {
347 version: self.version,
348 dataset: self.dataset,
349 schema: self.schema,
350 start: self.start,
351 end: self.end,
352 limit: self.limit,
353 stype_in: self.stype_in,
354 stype_out,
355 ts_out: self.ts_out,
356 symbols: self.symbols,
357 partial: self.partial,
358 not_found: self.not_found,
359 mappings: self.mappings,
360 }
361 }
362
363 pub fn ts_out(mut self, ts_out: bool) -> Self {
365 self.ts_out = ts_out;
366 self
367 }
368
369 pub fn symbols(mut self, symbols: Vec<String>) -> Self {
371 self.symbols = symbols;
372 self
373 }
374
375 pub fn partial(mut self, partial: Vec<String>) -> Self {
377 self.partial = partial;
378 self
379 }
380
381 pub fn not_found(mut self, not_found: Vec<String>) -> Self {
383 self.not_found = not_found;
384 self
385 }
386
387 pub fn mappings(mut self, mappings: Vec<SymbolMapping>) -> Self {
389 self.mappings = mappings;
390 self
391 }
392}
393
394impl MetadataBuilder<String, Option<Schema>, u64, Option<SType>, SType> {
395 pub fn build(self) -> Metadata {
398 Metadata {
399 version: self.version,
400 dataset: self.dataset,
401 schema: self.schema,
402 start: self.start,
403 end: self.end,
404 limit: self.limit,
405 stype_in: self.stype_in,
406 stype_out: self.stype_out,
407 ts_out: self.ts_out,
408 symbols: self.symbols,
409 partial: self.partial,
410 not_found: self.not_found,
411 mappings: self.mappings,
412 symbol_cstr_len: version_symbol_cstr_len(self.version),
413 }
414 }
415}
416
417impl Default for MetadataBuilder<Unset, Unset, Unset, Unset, Unset> {
418 fn default() -> Self {
419 Self {
420 version: crate::DBN_VERSION,
421 dataset: Unset {},
422 schema: Unset {},
423 start: Unset {},
424 end: None,
425 limit: None,
426 stype_in: Unset {},
427 stype_out: Unset {},
428 ts_out: false,
429 symbols: vec![],
430 partial: vec![],
431 not_found: vec![],
432 mappings: vec![],
433 }
434 }
435}
436
437#[derive(Debug, Clone, PartialEq, Eq)]
439#[cfg_attr(feature = "serde", derive(Deserialize))]
440#[cfg_attr(feature = "python", derive(pyo3::FromPyObject))]
441pub struct SymbolMapping {
442 pub raw_symbol: String,
444 pub intervals: Vec<MappingInterval>,
446}
447
448#[derive(Debug, Clone, PartialEq, Eq)]
450#[cfg_attr(feature = "serde", derive(Deserialize))]
451pub struct MappingInterval {
452 #[cfg_attr(
454 feature = "serde",
455 serde(rename = "d0", deserialize_with = "deserialize_date")
456 )]
457 pub start_date: time::Date,
458 #[cfg_attr(
460 feature = "serde",
461 serde(rename = "d1", deserialize_with = "deserialize_date")
462 )]
463 pub end_date: time::Date,
464 #[cfg_attr(feature = "serde", serde(rename = "s"))]
466 pub symbol: String,
467}
468
469pub const DATE_FORMAT: &[time::format_description::BorrowedFormatItem<'static>] =
471 time::macros::format_description!("[year]-[month]-[day]");
472
473#[cfg(feature = "serde")]
474fn deserialize_date<'de, D: serde::Deserializer<'de>>(
475 deserializer: D,
476) -> Result<time::Date, D::Error> {
477 let date_str = String::deserialize(deserializer)?;
478 time::Date::parse(&date_str, DATE_FORMAT).map_err(serde::de::Error::custom)
479}
480
481#[cfg(test)]
482mod tests {
483 use rstest::*;
484
485 use crate::Dataset;
486
487 use super::*;
488
489 #[rstest]
490 #[case(VersionUpgradePolicy::AsIs, 1)]
491 #[case(VersionUpgradePolicy::UpgradeToV2, 2)]
492 #[case(VersionUpgradePolicy::UpgradeToV3, 3)]
493 fn test_upgrade_metadata(
494 #[case] upgrade_policy: VersionUpgradePolicy,
495 #[case] exp_version: u8,
496 ) {
497 let mut target = Metadata::builder()
498 .version(1)
499 .dataset(Dataset::OpraPillar)
500 .schema(Some(Schema::Mbp1))
501 .start(0)
502 .stype_in(None)
503 .stype_out(SType::InstrumentId)
504 .build();
505 assert_eq!(target.version, 1);
506 target.upgrade(upgrade_policy);
507 assert_eq!(target.version, exp_version);
508 }
509}