Skip to content

Commit de1c714

Browse files
authored
refactor: Wave 2 god-file splits — decode/protocol/config/conditions + tdbe::time lift (closes #500-wave2) (#506)
Five structural changes: 1. crates/thetadatadx/src/decode.rs (2177 LoC) split into mdds/decode/ {error,headers,transport,extract,cell,v3}. Public API unchanged via mdds::decode::* re-exports. 2. Eastern-time + DST primitives lifted into tdbe::time as the canonical module reused by mdds, fpss, flatfiles. tdbe 0.12.9 → 0.12.10. 3. crates/thetadatadx/src/fpss/protocol.rs (1613 LoC) split into fpss/protocol/{contract,wire,subscription}. mod.rs keeps constants + re-exports; contract.rs holds Contract + Display + FromStr + OCC-21 parser; wire.rs holds payload builders/parsers; subscription.rs holds SubscriptionKind. 4. crates/thetadatadx/src/config.rs (1396 LoC, 30 flat fields) refactored into 7 nested typed sub-configs. DirectConfig now contains mdds, fpss, reconnect, retry, auth, metrics, runtime. Field-read accessors preserved for back-compat; field-write callers migrate to nested form (config.fpss.queue_depth = …). Adds mdds.connect_timeout_secs default 10s. 5. crates/tdbe/src/conditions.rs (2749 LoC) refactored to TOML-driven codegen. Source-of-truth at crates/tdbe/data/{trade,quote}_conditions.toml (149 + 75 entries). build.rs reads them and emits tdbe/src/conditions/tables_generated.rs with compile-time const arrays. Public surface unchanged; new condition_tables_pin test pins 12 known entries against the const arrays for round-trip protection. Bumps thetadatadx 8.0.35 → 8.0.36 and tdbe 0.12.9 → 0.12.10. All workspace + sub-manifest CI gates green locally.
1 parent 1f4087f commit de1c714

64 files changed

Lines changed: 7876 additions & 4499 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,40 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [8.0.36] - 2026-05-07
9+
10+
### Changed
11+
12+
- **`crates/thetadatadx/src/decode.rs` (2177 LoC) split into 7 modules**
13+
under `mdds/decode/{error,headers,transport,extract,cell,v3}`. Pure
14+
structural refactor; public API unchanged via `mdds::decode::*` re-exports.
15+
- **Eastern-time + DST primitives lifted to `tdbe::time`.**
16+
`eastern_offset_ms`, `march_second_sunday_utc`, `november_first_sunday_utc`,
17+
`april_first_sunday_utc`, `october_last_sunday_utc`, `civil_to_epoch_days`,
18+
`timestamp_to_ms_of_day`, `timestamp_to_date` — single canonical module
19+
reused by mdds, fpss, flatfiles. tdbe 0.12.9 → 0.12.10.
20+
- **`crates/thetadatadx/src/fpss/protocol.rs` (1613 LoC) split into 4 modules**
21+
under `fpss/protocol/`. `mod.rs` keeps constants and re-exports;
22+
`contract.rs` holds `Contract` + 6 constructors + `Display` + `FromStr` +
23+
OCC-21 parser; `wire.rs` holds payload builders / parsers; `subscription.rs`
24+
holds `SubscriptionKind`.
25+
- **`crates/thetadatadx/src/config.rs` (1396 LoC, 30 flat fields) refactored
26+
into 7 nested typed sub-configs.** `DirectConfig` now contains `mdds`,
27+
`fpss`, `reconnect`, `retry`, `auth`, `metrics`, `runtime`. Field-read
28+
accessors preserved on `DirectConfig` for back-compat (`config.mdds_host()`
29+
etc still work). Field-write callers must migrate to nested form
30+
(`config.fpss.queue_depth = ...`). Adds `mdds.connect_timeout_secs`
31+
(default 10s, covers prior LOW finding).
32+
- **`crates/tdbe/src/conditions.rs` (2749 LoC) refactored to TOML-driven
33+
codegen.** Source-of-truth at `crates/tdbe/data/{trade,quote}_conditions.toml`
34+
(149 + 75 entries). `crates/tdbe/build.rs` reads the TOMLs and emits
35+
`crates/tdbe/src/conditions/tables_generated.rs` with compile-time
36+
const arrays. Public surface unchanged; new `condition_tables_pin`
37+
test pins 12 known entries against the const arrays for round-trip
38+
protection.
39+
40+
Refs #500.
41+
842
## [8.0.35] - 2026-05-07
943

1044
### Documentation

Cargo.lock

Lines changed: 6 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/tdbe/Cargo.toml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "tdbe"
3-
version = "0.12.9"
3+
version = "0.12.10"
44
edition.workspace = true
55
rust-version.workspace = true
66
authors.workspace = true
@@ -11,6 +11,14 @@ repository.workspace = true
1111
license.workspace = true
1212
keywords = ["thetadata", "encoding", "market-data", "codec", "greeks"]
1313
categories = ["encoding", "finance"]
14+
build = "build.rs"
15+
include = [
16+
"src/**/*.rs",
17+
"data/*.toml",
18+
"build.rs",
19+
"Cargo.toml",
20+
"README.md",
21+
]
1422

1523
[lints]
1624
workspace = true
@@ -19,6 +27,13 @@ workspace = true
1927
sonic-rs = "0.5.8"
2028
thiserror = "2.0.18"
2129

30+
[build-dependencies]
31+
# Build-time only: parses `data/{trade,quote}_conditions.toml` into the
32+
# committed `src/conditions/tables_generated.rs`. Does not ship to
33+
# runtime consumers -- `tdbe`'s runtime dep graph is unchanged.
34+
serde = { version = "1.0", features = ["derive"] }
35+
toml = "1.1"
36+
2237
[dev-dependencies]
2338
criterion = { version = "0.8.2", features = ["html_reports"] }
2439

crates/tdbe/build.rs

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
//! Build-time codegen for trade and quote condition tables.
2+
//!
3+
//! Reads `data/trade_conditions.toml` (149 entries) and
4+
//! `data/quote_conditions.toml` (75 entries) and emits
5+
//! `src/conditions/tables_generated.rs` containing the
6+
//! `TRADE_CONDITIONS` and `QUOTE_CONDITIONS` const arrays.
7+
//!
8+
//! The generated file is committed so downstream consumers building
9+
//! `tdbe` from crates.io don't have to re-run codegen unless they edit
10+
//! the TOML source-of-truth files locally.
11+
12+
use std::env;
13+
use std::fs;
14+
use std::path::PathBuf;
15+
16+
use serde::Deserialize;
17+
use toml::Value;
18+
19+
#[derive(Deserialize)]
20+
struct TradeFile {
21+
trade: Vec<TradeRow>,
22+
}
23+
24+
#[derive(Deserialize)]
25+
struct QuoteFile {
26+
quote: Vec<QuoteRow>,
27+
}
28+
29+
#[derive(Deserialize)]
30+
struct TradeRow {
31+
code: i32,
32+
name: String,
33+
description: String,
34+
cancel: bool,
35+
late_report: bool,
36+
auto_executed: bool,
37+
open_report: bool,
38+
volume: bool,
39+
high: bool,
40+
low: bool,
41+
last: bool,
42+
}
43+
44+
#[derive(Deserialize)]
45+
struct QuoteRow {
46+
code: i32,
47+
name: String,
48+
description: String,
49+
firm: bool,
50+
halted: bool,
51+
}
52+
53+
fn rust_string_literal(s: &str) -> String {
54+
let mut out = String::with_capacity(s.len() + 2);
55+
out.push('"');
56+
for ch in s.chars() {
57+
match ch {
58+
'\\' => out.push_str("\\\\"),
59+
'"' => out.push_str("\\\""),
60+
'\n' => out.push_str("\\n"),
61+
'\r' => out.push_str("\\r"),
62+
'\t' => out.push_str("\\t"),
63+
c if (c as u32) < 0x20 => {
64+
use std::fmt::Write;
65+
let _ = write!(out, "\\u{{{:x}}}", c as u32);
66+
}
67+
c => out.push(c),
68+
}
69+
}
70+
out.push('"');
71+
out
72+
}
73+
74+
fn main() {
75+
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
76+
let trade_toml = manifest_dir.join("data/trade_conditions.toml");
77+
let quote_toml = manifest_dir.join("data/quote_conditions.toml");
78+
let out = manifest_dir.join("src/conditions/tables_generated.rs");
79+
80+
println!("cargo:rerun-if-changed=data/trade_conditions.toml");
81+
println!("cargo:rerun-if-changed=data/quote_conditions.toml");
82+
println!("cargo:rerun-if-changed=build.rs");
83+
84+
let trade_src = fs::read_to_string(&trade_toml)
85+
.unwrap_or_else(|e| panic!("read {}: {e}", trade_toml.display()));
86+
let quote_src = fs::read_to_string(&quote_toml)
87+
.unwrap_or_else(|e| panic!("read {}: {e}", quote_toml.display()));
88+
89+
// Parse via `toml::Value` first to give precise spec errors, then re-deserialize
90+
// into typed rows.
91+
let _: Value = toml::from_str(&trade_src).expect("trade_conditions.toml: invalid TOML");
92+
let _: Value = toml::from_str(&quote_src).expect("quote_conditions.toml: invalid TOML");
93+
94+
let trades: TradeFile =
95+
toml::from_str(&trade_src).expect("trade_conditions.toml: schema mismatch");
96+
let quotes: QuoteFile =
97+
toml::from_str(&quote_src).expect("quote_conditions.toml: schema mismatch");
98+
99+
assert_eq!(
100+
trades.trade.len(),
101+
149,
102+
"trade_conditions.toml must have exactly 149 entries"
103+
);
104+
assert_eq!(
105+
quotes.quote.len(),
106+
75,
107+
"quote_conditions.toml must have exactly 75 entries"
108+
);
109+
for (i, t) in trades.trade.iter().enumerate() {
110+
assert_eq!(
111+
t.code as usize, i,
112+
"trade_conditions.toml[{i}] has code {} (must equal index)",
113+
t.code
114+
);
115+
}
116+
for (i, q) in quotes.quote.iter().enumerate() {
117+
assert_eq!(
118+
q.code as usize, i,
119+
"quote_conditions.toml[{i}] has code {} (must equal index)",
120+
q.code
121+
);
122+
}
123+
124+
let mut s = String::new();
125+
s.push_str("// @generated DO NOT EDIT — regenerated by build.rs from data/*.toml\n");
126+
s.push_str("//\n");
127+
s.push_str("// Source-of-truth: crates/tdbe/data/trade_conditions.toml\n");
128+
s.push_str("// crates/tdbe/data/quote_conditions.toml\n\n");
129+
s.push_str("use super::{QuoteCondition, TradeCondition};\n\n");
130+
131+
s.push_str("/// All 149 trade condition codes (0..148).\n");
132+
s.push_str("pub const TRADE_CONDITIONS: [TradeCondition; 149] = [\n");
133+
for t in &trades.trade {
134+
s.push_str(" TradeCondition {\n");
135+
s.push_str(&format!(" code: {},\n", t.code));
136+
s.push_str(&format!(
137+
" name: {},\n",
138+
rust_string_literal(&t.name)
139+
));
140+
s.push_str(&format!(
141+
" description: {},\n",
142+
rust_string_literal(&t.description)
143+
));
144+
s.push_str(&format!(" cancel: {},\n", t.cancel));
145+
s.push_str(&format!(" late_report: {},\n", t.late_report));
146+
s.push_str(&format!(" auto_executed: {},\n", t.auto_executed));
147+
s.push_str(&format!(" open_report: {},\n", t.open_report));
148+
s.push_str(&format!(" volume: {},\n", t.volume));
149+
s.push_str(&format!(" high: {},\n", t.high));
150+
s.push_str(&format!(" low: {},\n", t.low));
151+
s.push_str(&format!(" last: {},\n", t.last));
152+
s.push_str(" },\n");
153+
}
154+
s.push_str("];\n\n");
155+
156+
s.push_str("/// All 75 quote condition codes (0..74).\n");
157+
s.push_str("pub const QUOTE_CONDITIONS: [QuoteCondition; 75] = [\n");
158+
for q in &quotes.quote {
159+
s.push_str(" QuoteCondition {\n");
160+
s.push_str(&format!(" code: {},\n", q.code));
161+
s.push_str(&format!(
162+
" name: {},\n",
163+
rust_string_literal(&q.name)
164+
));
165+
s.push_str(&format!(
166+
" description: {},\n",
167+
rust_string_literal(&q.description)
168+
));
169+
s.push_str(&format!(" firm: {},\n", q.firm));
170+
s.push_str(&format!(" halted: {},\n", q.halted));
171+
s.push_str(" },\n");
172+
}
173+
s.push_str("];\n");
174+
175+
// Write only if changed, to avoid touching mtime and triggering downstream rebuilds.
176+
let needs_write = match fs::read_to_string(&out) {
177+
Ok(existing) => existing != s,
178+
Err(_) => true,
179+
};
180+
if needs_write {
181+
if let Some(parent) = out.parent() {
182+
fs::create_dir_all(parent).expect("create conditions dir");
183+
}
184+
fs::write(&out, s).expect("write tables_generated.rs");
185+
}
186+
}

0 commit comments

Comments
 (0)