px_core/exchange/manifest.rs
1use crate::models::MarketStatus;
2
3/// Complete auditable manifest for an exchange.
4/// When opened, shows SOURCE and TRANSFORMATION side-by-side.
5#[derive(Debug, Clone)]
6pub struct ExchangeManifest {
7 // ========================================
8 // SECTION 1: CONNECTION AUDIT (Where do we go?)
9 // ========================================
10 /// Exchange identifier (e.g., "kalshi", "polymarket")
11 pub id: &'static str,
12
13 /// Human-readable exchange name
14 pub name: &'static str,
15
16 /// Base API URL
17 pub base_url: &'static str,
18
19 /// Markets list endpoint (relative to base_url)
20 pub markets_endpoint: &'static str,
21
22 /// Pagination configuration
23 pub pagination: PaginationConfig,
24
25 /// Rate limiting configuration
26 pub rate_limit: RateLimitConfig,
27
28 // ========================================
29 // SECTION 2: DATA AUDIT (How do we map it?)
30 // ========================================
31 /// Field mappings from raw exchange JSON to Market
32 pub field_mappings: &'static [FieldMapping],
33
34 /// Status value mappings (exchange status -> MarketStatus)
35 pub status_map: &'static [(&'static str, MarketStatus)],
36}
37
38impl ExchangeManifest {
39 /// Look up the MarketStatus for a given exchange status string.
40 /// Status map entries should be lowercase at definition time for O(n) without allocation.
41 pub fn map_status(&self, exchange_status: &str) -> Option<MarketStatus> {
42 self.status_map
43 .iter()
44 .find(|(s, _)| s.eq_ignore_ascii_case(exchange_status))
45 .map(|(_, status)| *status)
46 }
47
48 /// Get a field mapping by unified field name.
49 pub fn get_field_mapping(&self, unified_field: &str) -> Option<&FieldMapping> {
50 self.field_mappings
51 .iter()
52 .find(|m| m.unified_field == unified_field)
53 }
54}
55
56#[derive(Debug, Clone, Copy)]
57pub struct PaginationConfig {
58 /// Pagination style: Cursor, Offset, Page
59 pub style: PaginationStyle,
60 /// Maximum items per page
61 pub max_page_size: usize,
62 /// Query param name for limit
63 pub limit_param: &'static str,
64 /// Query param name for cursor/offset
65 pub cursor_param: &'static str,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum PaginationStyle {
70 /// Cursor-based pagination (Kalshi)
71 Cursor,
72 /// Offset-based pagination (Polymarket)
73 Offset,
74 /// Page-number pagination (Opinion, 1-indexed)
75 PageNumber,
76 /// No pagination supported - endpoint returns all data in single call
77 None,
78}
79
80#[derive(Debug, Clone, Copy)]
81pub struct RateLimitConfig {
82 /// Requests per second
83 pub requests_per_second: u32,
84 /// Burst limit
85 pub burst: u32,
86}
87
88/// Mapping from raw exchange JSON field to unified field.
89#[derive(Debug, Clone)]
90pub struct FieldMapping {
91 /// Target field in Market
92 pub unified_field: &'static str,
93 /// Source path(s) in raw JSON (fallback chain)
94 pub source_paths: &'static [&'static str],
95 /// Transformation to apply
96 pub transform: Transform,
97 /// Whether field can be null
98 pub nullable: bool,
99}
100
101/// Transformation to apply when mapping a field.
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum Transform {
104 /// No transformation - use value directly
105 Direct,
106 /// Divide by 100 (Kalshi prices: cents -> decimal)
107 CentsToDollars,
108 /// Unix timestamp (seconds) to DateTime
109 UnixSecsToDateTime,
110 /// Unix timestamp (milliseconds) to DateTime
111 UnixMillisToDateTime,
112 /// ISO8601 string to DateTime
113 Iso8601ToDateTime,
114 /// String/Float -> i64
115 ParseInt,
116 /// String -> f64
117 ParseFloat,
118 /// Extract element at index from JSON array
119 JsonArrayIndex(usize),
120 /// Nested path extraction (dot-notation handled by source_paths)
121 NestedPath,
122}