1use std::collections::HashMap;
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6#[cfg_attr(feature = "specta", derive(specta::Type))]
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9#[serde(rename_all = "camelCase")]
10pub struct Market {
11 pub id: String,
12 pub condition_id: String,
13 pub question_id: Option<String>,
14 pub slug: Option<String>,
15 #[serde(default)]
16 pub tokens: Vec<MarketToken>,
17 #[cfg_attr(feature = "specta", specta(type = Option<HashMap<String, String>>))]
18 pub rewards: Option<HashMap<String, serde_json::Value>>,
19 pub minimum_order_size: Option<String>,
20 pub minimum_tick_size: Option<String>,
21 pub description: String,
22 pub category: Option<String>,
23 pub end_date_iso: Option<String>,
24 pub start_date_iso: Option<String>,
25 pub question: String,
26 pub min_incentive_size: Option<String>,
27 pub max_incentive_spread: Option<String>,
28 pub submitted_by: Option<String>,
29 #[serde(rename = "volume24hr")] pub volume_24hr: Option<f64>,
31 #[serde(rename = "volume1wk")] pub volume_1wk: Option<f64>,
33 #[serde(rename = "volume1mo")] pub volume_1mo: Option<f64>,
35 #[serde(rename = "volume1yr")] pub volume_1yr: Option<f64>,
37 pub liquidity: Option<String>,
38 #[serde(default)]
39 pub tags: Vec<Tag>,
40 pub neg_risk: Option<bool>,
41 pub neg_risk_market_id: Option<String>,
42 pub neg_risk_request_id: Option<String>,
43 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
45 pub comment_count: Option<i64>,
46 pub twitter_card_image: Option<String>,
47 pub resolution_source: Option<String>,
48 pub amm_type: Option<String>,
49 pub sponsor_name: Option<String>,
50 pub sponsor_image: Option<String>,
51 pub x_axis_value: Option<String>,
52 pub y_axis_value: Option<String>,
53 #[serde(rename = "denomationToken")]
54 pub denomination_token: Option<String>,
55 pub fee: Option<String>,
56 pub image: Option<String>,
57 pub icon: Option<String>,
58 pub lower_bound: Option<String>,
59 pub upper_bound: Option<String>,
60 pub outcomes: Option<String>,
61 pub outcome_prices: Option<String>,
62 pub volume: Option<String>,
63 pub active: Option<bool>,
64 pub market_type: Option<String>,
65 pub format_type: Option<String>,
66 pub lower_bound_date: Option<String>,
67 pub upper_bound_date: Option<String>,
68 pub closed: Option<bool>,
69 pub market_maker_address: String,
70 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
71 pub created_by: Option<i64>,
72 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
73 pub updated_by: Option<i64>,
74 pub created_at: Option<String>,
75 pub updated_at: Option<String>,
76 pub closed_time: Option<String>,
77 pub wide_format: Option<bool>,
78 pub new: Option<bool>,
79 pub mailchimp_tag: Option<String>,
80 pub featured: Option<bool>,
81 pub archived: Option<bool>,
82 pub resolved_by: Option<String>,
83 pub restricted: Option<bool>,
84 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
85 pub market_group: Option<i64>,
86 pub group_item_title: Option<String>,
87 pub group_item_threshold: Option<String>,
88 pub uma_end_date: Option<String>,
89 pub uma_resolution_status: Option<String>,
90 pub uma_end_date_iso: Option<String>,
91 pub uma_resolution_statuses: Option<String>,
92 pub enable_order_book: Option<bool>,
93 pub order_price_min_tick_size: Option<f64>,
94 pub order_min_size: Option<f64>,
95 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
96 pub curation_order: Option<i64>,
97 pub volume_num: Option<f64>,
98 pub liquidity_num: Option<f64>,
99 pub has_review_dates: Option<bool>,
100 pub ready_for_cron: Option<bool>,
101 pub comments_enabled: Option<bool>,
102 pub game_start_time: Option<String>,
103 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
104 pub seconds_delay: Option<i64>,
105 pub clob_token_ids: Option<String>,
106 pub disqus_thread: Option<String>,
107 pub short_outcomes: Option<String>,
108 pub team_aid: Option<String>,
109 pub team_bid: Option<String>,
110 pub uma_bond: Option<String>,
111 pub uma_reward: Option<String>,
112 pub fpmm_live: Option<bool>,
113 #[serde(rename = "volume24hrAmm")] pub volume_24hr_amm: Option<f64>,
115 #[serde(rename = "volume1wkAmm")]
116 pub volume_1wk_amm: Option<f64>,
117 #[serde(rename = "volume1moAmm")]
118 pub volume_1mo_amm: Option<f64>,
119 #[serde(rename = "volume1yrAmm")]
120 pub volume_1yr_amm: Option<f64>,
121 #[serde(rename = "volume24hrClob")]
122 pub volume_24hr_clob: Option<f64>,
123 #[serde(rename = "volume1wkClob")]
124 pub volume_1wk_clob: Option<f64>,
125 #[serde(rename = "volume1moClob")]
126 pub volume_1mo_clob: Option<f64>,
127 #[serde(rename = "volume1yrClob")]
128 pub volume_1yr_clob: Option<f64>,
129 pub volume_amm: Option<f64>,
130 pub volume_clob: Option<f64>,
131 pub liquidity_amm: Option<f64>,
132 pub liquidity_clob: Option<f64>,
133 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
134 pub maker_base_fee: Option<i64>,
135 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
136 pub taker_base_fee: Option<i64>,
137 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
138 pub custom_liveness: Option<i64>,
139 pub accepting_orders: Option<bool>,
140 pub notifications_enabled: Option<bool>,
141 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
142 pub score: Option<i64>,
143 pub creator: Option<String>,
144 pub ready: Option<bool>,
145 pub funded: Option<bool>,
146 pub past_slugs: Option<String>,
147 pub ready_timestamp: Option<String>,
148 pub funded_timestamp: Option<String>,
149 pub accepting_orders_timestamp: Option<String>,
150 pub competitive: Option<f64>,
151 pub rewards_min_size: Option<f64>,
152 pub rewards_max_spreads: Option<f64>,
153 pub spread: Option<f64>,
154 pub automatically_resolved: Option<bool>,
155 pub automatically_active: Option<bool>,
156 pub one_day_price_change: Option<f64>,
157 pub one_hour_price_change: Option<f64>,
158 pub one_week_price_change: Option<f64>,
159 pub one_month_price_change: Option<f64>,
160 pub one_year_price_change: Option<f64>,
161 pub last_trade_price: Option<f64>,
162 pub best_bid: Option<f64>,
163 pub best_ask: Option<f64>,
164 pub clear_book_on_start: Option<bool>,
165 pub chart_color: Option<String>,
166 pub series_color: Option<String>,
167 pub show_gmp_series: Option<bool>,
168 pub show_gmp_outcome: Option<bool>,
169 pub manual_activation: Option<bool>,
170 pub neg_risk_other: Option<bool>,
171 pub game_id: Option<String>,
172 pub group_item_range: Option<String>,
173 pub sports_market_type: Option<String>,
174 pub line: Option<f64>,
175 pub pending_deployment: Option<bool>,
176 pub deploying: Option<bool>,
177 pub deploying_timestamp: Option<String>,
178 pub schedule_deployment_timestamp: Option<String>,
179 pub rfq_enabled: Option<bool>,
180 pub event_start_time: Option<String>,
181}
182
183#[cfg_attr(feature = "specta", derive(specta::Type))]
185#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
186#[serde(rename_all = "camelCase")]
187pub struct MarketToken {
188 pub token_id: String,
189 pub outcome: String,
190 pub price: Option<String>,
191 pub winner: Option<bool>,
192}
193
194#[cfg_attr(feature = "specta", derive(specta::Type))]
195#[derive(Debug, Clone, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub struct Event {
198 pub id: String,
199 pub ticker: Option<String>,
200 pub slug: Option<String>,
201 pub title: Option<String>,
202 pub subtitle: Option<String>,
203 pub description: Option<String>,
204 pub resolution_source: Option<String>,
205 pub start_date: Option<String>,
206 pub creation_date: Option<String>,
207 pub end_date: Option<String>,
208 pub image: Option<String>,
209 pub icon: Option<String>,
210 pub start_date_iso: Option<String>,
211 pub end_date_iso: Option<String>,
212 pub active: Option<bool>,
213 pub closed: Option<bool>,
214 pub archived: Option<bool>,
215 pub new: Option<bool>,
216 pub featured: Option<bool>,
217 pub restricted: Option<bool>,
218 pub liquidity: Option<f64>,
219 pub open_interest: Option<f64>,
220 pub sort_by: Option<String>,
221 pub category: Option<String>,
222 pub subcategory: Option<String>,
223 pub is_template: Option<bool>,
224 pub template_variables: Option<String>,
225 pub published_at: Option<String>,
226 pub created_by: Option<String>,
227 pub updated_by: Option<String>,
228 pub created_at: Option<String>,
229 pub updated_at: Option<String>,
230 pub comments_enabled: Option<bool>,
231 pub competitive: Option<f64>,
232 #[serde(rename = "volume24h")] pub volume_24hr: Option<f64>,
234 #[serde(rename = "volume1wk")]
235 pub volume_1wk: Option<f64>,
236 #[serde(rename = "volume1mo")]
237 pub volume_1mo: Option<f64>,
238 #[serde(rename = "volume1yr")]
239 pub volume_1yr: Option<f64>,
240 pub featured_image: Option<String>,
241 pub disqus_thread: Option<String>,
242 pub parent_event: Option<String>,
243 pub enable_order_book: Option<bool>,
244 pub liquidity_amm: Option<f64>,
245 pub liquidity_clob: Option<f64>,
246 pub neg_risk: Option<bool>,
247 pub neg_risk_market_id: Option<String>,
248 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
249 pub neg_risk_fee_bips: Option<i64>,
250 #[serde(default)]
251 pub sub_events: Vec<String>,
252 #[serde(default)]
253 pub markets: Vec<Market>,
254 #[serde(default)]
255 pub tags: Vec<Tag>,
256 #[serde(default)]
257 pub series: Vec<SeriesInfo>,
258 pub cyom: Option<bool>,
259 pub closed_time: Option<String>,
260 pub show_all_outcomes: Option<bool>,
261 pub show_market_images: Option<bool>,
262 pub automatically_resolved: Option<bool>,
263 #[serde(rename = "enalbeNegRisk")]
264 pub enable_neg_risk: Option<bool>,
265 pub automatically_active: Option<bool>,
266 pub event_date: Option<String>,
267 pub start_time: Option<String>,
268 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
269 pub event_week: Option<i64>,
270 pub series_slug: Option<String>,
271 pub score: Option<String>,
272 pub elapsed: Option<String>,
273 pub period: Option<String>,
274 pub live: Option<bool>,
275 pub ended: Option<bool>,
276 pub finished_timestamp: Option<String>,
277 pub gmp_chart_mode: Option<String>,
278 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
279 pub tweet_count: Option<i64>,
280 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
281 pub featured_order: Option<i64>,
282 pub estimate_value: Option<bool>,
283 pub cant_estimate: Option<bool>,
284 pub spreads_main_line: Option<f64>,
285 pub totals_main_line: Option<f64>,
286 pub carousel_map: Option<String>,
287 pub pending_deployment: Option<bool>,
288 pub deploying: Option<bool>,
289 pub deploying_timestamp: Option<String>,
290 pub schedule_deployment_timestamp: Option<String>,
291 pub game_status: Option<String>,
292}
293
294#[cfg_attr(feature = "specta", derive(specta::Type))]
296#[derive(Debug, Clone, Serialize, Deserialize)]
297#[serde(rename_all = "camelCase")]
298pub struct SeriesInfo {
299 pub id: String,
300 pub slug: String,
301 pub title: String,
302}
303
304#[cfg_attr(feature = "specta", derive(specta::Type))]
306#[derive(Debug, Clone, Serialize, Deserialize)]
307#[serde(rename_all = "camelCase")]
308pub struct SeriesData {
309 pub id: String,
310 pub slug: String,
311 pub title: String,
312 pub description: Option<String>,
313 pub image: Option<String>,
314 pub icon: Option<String>,
315 pub active: bool,
316 pub closed: bool,
317 pub archived: bool,
318 #[serde(default)]
319 pub tags: Vec<String>,
320 pub volume: Option<f64>,
321 pub liquidity: Option<f64>,
322 #[serde(default)]
323 pub events: Vec<Event>,
324 pub competitive: Option<String>,
325}
326
327#[cfg_attr(feature = "specta", derive(specta::Type))]
329#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
330#[serde(rename_all = "camelCase")]
331pub struct Tag {
332 pub id: String,
333 pub slug: String,
334 pub label: String,
335 pub force_show: Option<bool>,
336 pub published_at: Option<String>,
337 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
338 pub created_by: Option<u64>,
339 #[cfg_attr(feature = "specta", specta(type = Option<f64>))]
340 pub updated_by: Option<u64>,
341 pub created_at: Option<String>,
342 pub updated_at: Option<String>,
343 pub force_hide: Option<bool>,
344 pub is_carousel: Option<bool>,
345}
346
347#[cfg_attr(feature = "specta", derive(specta::Type))]
349#[derive(Debug, Clone, Serialize, Deserialize)]
350#[serde(rename_all = "camelCase")]
351pub struct SportMetadata {
352 #[cfg_attr(feature = "specta", specta(type = f64))]
353 pub id: u64,
354 pub sport: String,
355 pub image: Option<String>,
356 pub resolution: Option<String>,
357 pub ordering: Option<String>,
358 pub tags: Option<String>,
359 pub series: Option<String>,
360 pub created_at: Option<String>,
361}
362
363#[cfg_attr(feature = "specta", derive(specta::Type))]
365#[derive(Debug, Clone, Serialize, Deserialize)]
366#[serde(rename_all = "camelCase")]
367pub struct Team {
368 #[cfg_attr(feature = "specta", specta(type = f64))]
369 pub id: i64,
370 pub name: Option<String>,
371 pub league: Option<String>,
372 pub record: Option<String>,
373 pub logo: Option<String>,
374 pub abbreviation: Option<String>,
375 pub alias: Option<String>,
376 pub created_at: Option<DateTime<Utc>>,
377 pub updated_at: Option<DateTime<Utc>>,
378}
379
380#[cfg_attr(feature = "specta", derive(specta::Type))]
382#[derive(Debug, Clone, Serialize, Deserialize)]
383#[serde(rename_all = "camelCase")]
384pub struct Comment {
385 pub id: String,
386 pub body: String,
387 pub created_at: DateTime<Utc>,
388 pub updated_at: DateTime<Utc>,
389 pub deleted_at: Option<DateTime<Utc>>,
390 pub user: CommentUser,
391 pub market_id: Option<String>,
392 pub event_id: Option<String>,
393 pub series_id: Option<String>,
394 pub parent_id: Option<String>,
395 #[serde(default)]
396 pub reactions: Vec<CommentReaction>,
397 #[serde(default)]
398 pub positions: Vec<CommentPosition>,
399 #[cfg_attr(feature = "specta", specta(type = f64))]
400 pub like_count: u32,
401 #[cfg_attr(feature = "specta", specta(type = f64))]
402 pub dislike_count: u32,
403 #[cfg_attr(feature = "specta", specta(type = f64))]
404 pub reply_count: u32,
405}
406
407#[cfg_attr(feature = "specta", derive(specta::Type))]
409#[derive(Debug, Clone, Serialize, Deserialize)]
410#[serde(rename_all = "camelCase")]
411pub struct CommentUser {
412 pub id: String,
413 pub name: String,
414 pub avatar: Option<String>,
415}
416
417#[cfg_attr(feature = "specta", derive(specta::Type))]
419#[derive(Debug, Clone, Serialize, Deserialize)]
420#[serde(rename_all = "camelCase")]
421pub struct CommentReaction {
422 pub user_id: String,
423 pub reaction_type: String,
424}
425
426#[cfg_attr(feature = "specta", derive(specta::Type))]
428#[derive(Debug, Clone, Serialize, Deserialize)]
429#[serde(rename_all = "camelCase")]
430pub struct CommentPosition {
431 pub token_id: String,
432 pub outcome: String,
433 pub shares: String,
434}
435
436#[cfg_attr(feature = "specta", derive(specta::Type))]
438#[derive(Debug, Clone, Serialize, Deserialize)]
439#[serde(rename_all = "camelCase")]
440pub struct Cursor {
441 pub next_cursor: Option<String>,
442}
443
444#[cfg_attr(feature = "specta", derive(specta::Type))]
446#[derive(Debug, Clone, Serialize, Deserialize)]
447#[serde(rename_all = "camelCase")]
448pub struct PaginatedResponse<T> {
449 pub data: Vec<T>,
450 pub next_cursor: Option<String>,
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456
457 #[test]
460 fn test_market_token_deserialization() {
461 let json = r#"{
462 "tokenId": "71321045679252212594626385532706912750332728571942532289631379312455583992563",
463 "outcome": "Yes",
464 "price": "0.55",
465 "winner": false
466 }"#;
467 let token: MarketToken = serde_json::from_str(json).unwrap();
468 assert_eq!(token.outcome, "Yes");
469 assert_eq!(token.price.as_deref(), Some("0.55"));
470 assert_eq!(token.winner, Some(false));
471 }
472
473 #[test]
474 fn test_market_token_optional_fields() {
475 let json = r#"{"tokenId": "123", "outcome": "No"}"#;
476 let token: MarketToken = serde_json::from_str(json).unwrap();
477 assert!(token.price.is_none());
478 assert!(token.winner.is_none());
479 }
480
481 #[test]
484 fn test_tag_deserialization() {
485 let json = r#"{
486 "id": "42",
487 "slug": "politics",
488 "label": "Politics",
489 "forceShow": true,
490 "publishedAt": "2024-01-01T00:00:00Z",
491 "createdBy": 1,
492 "updatedBy": 2,
493 "createdAt": "2024-01-01T00:00:00Z",
494 "updatedAt": "2024-06-01T00:00:00Z",
495 "forceHide": false,
496 "isCarousel": true
497 }"#;
498 let tag: Tag = serde_json::from_str(json).unwrap();
499 assert_eq!(tag.slug, "politics");
500 assert_eq!(tag.force_show, Some(true));
501 assert_eq!(tag.is_carousel, Some(true));
502 }
503
504 #[test]
505 fn test_tag_minimal() {
506 let json = r#"{"id": "1", "slug": "test", "label": "Test"}"#;
507 let tag: Tag = serde_json::from_str(json).unwrap();
508 assert_eq!(tag.label, "Test");
509 assert!(tag.force_show.is_none());
510 assert!(tag.created_by.is_none());
511 }
512
513 #[test]
516 fn test_market_minimal_deserialization() {
517 let json = r#"{
518 "id": "12345",
519 "conditionId": "0xabc",
520 "description": "Will X happen?",
521 "question": "Will X happen by end of 2025?",
522 "marketMakerAddress": "0x1234567890abcdef"
523 }"#;
524 let market: Market = serde_json::from_str(json).unwrap();
525 assert_eq!(market.id, "12345");
526 assert_eq!(market.condition_id, "0xabc");
527 assert!(market.tokens.is_empty()); assert!(market.tags.is_empty()); assert!(market.slug.is_none());
530 assert!(market.volume_24hr.is_none());
531 }
532
533 #[test]
534 fn test_market_with_tokens() {
535 let json = r#"{
536 "id": "1",
537 "conditionId": "0xcond",
538 "description": "Test",
539 "question": "Test?",
540 "marketMakerAddress": "0xaddr",
541 "tokens": [
542 {"tokenId": "t1", "outcome": "Yes", "price": "0.7", "winner": true},
543 {"tokenId": "t2", "outcome": "No", "price": "0.3", "winner": false}
544 ]
545 }"#;
546 let market: Market = serde_json::from_str(json).unwrap();
547 assert_eq!(market.tokens.len(), 2);
548 assert_eq!(market.tokens[0].outcome, "Yes");
549 assert_eq!(market.tokens[1].price.as_deref(), Some("0.3"));
550 }
551
552 #[test]
553 fn test_market_volume_fields() {
554 let json = r#"{
555 "id": "1",
556 "conditionId": "0xcond",
557 "description": "Test",
558 "question": "Test?",
559 "marketMakerAddress": "0xaddr",
560 "volume24hr": 1500.5,
561 "volume1wk": 10000.0,
562 "volume1mo": 50000.0,
563 "volume1yr": 200000.0,
564 "volume24hrAmm": 100.0,
565 "volume1wkClob": 9900.0
566 }"#;
567 let market: Market = serde_json::from_str(json).unwrap();
568 assert_eq!(market.volume_24hr, Some(1500.5));
569 assert_eq!(market.volume_1wk, Some(10000.0));
570 assert_eq!(market.volume_24hr_amm, Some(100.0));
571 assert_eq!(market.volume_1wk_clob, Some(9900.0));
572 }
573
574 #[test]
575 fn test_market_denomination_token_rename() {
576 let json = r#"{
578 "id": "1",
579 "conditionId": "0xcond",
580 "description": "Test",
581 "question": "Test?",
582 "marketMakerAddress": "0xaddr",
583 "denomationToken": "USDC"
584 }"#;
585 let market: Market = serde_json::from_str(json).unwrap();
586 assert_eq!(market.denomination_token.as_deref(), Some("USDC"));
587 }
588
589 #[test]
590 fn test_market_rewards_as_map() {
591 let json = r#"{
592 "id": "1",
593 "conditionId": "0xcond",
594 "description": "Test",
595 "question": "Test?",
596 "marketMakerAddress": "0xaddr",
597 "rewards": {"min_size": "100", "max_spread": "0.05"}
598 }"#;
599 let market: Market = serde_json::from_str(json).unwrap();
600 assert!(market.rewards.is_some());
601 let rewards = market.rewards.unwrap();
602 assert_eq!(rewards["min_size"], "100");
603 }
604
605 #[test]
606 fn test_market_null_rewards() {
607 let json = r#"{
608 "id": "1",
609 "conditionId": "0xcond",
610 "description": "Test",
611 "question": "Test?",
612 "marketMakerAddress": "0xaddr",
613 "rewards": null
614 }"#;
615 let market: Market = serde_json::from_str(json).unwrap();
616 assert!(market.rewards.is_none());
617 }
618
619 #[test]
622 fn test_event_minimal() {
623 let json = r#"{"id": "evt-1"}"#;
624 let event: Event = serde_json::from_str(json).unwrap();
625 assert_eq!(event.id, "evt-1");
626 assert!(event.markets.is_empty()); assert!(event.tags.is_empty());
628 assert!(event.series.is_empty());
629 assert!(event.sub_events.is_empty());
630 }
631
632 #[test]
633 fn test_event_with_nested_markets() {
634 let json = r#"{
635 "id": "evt-1",
636 "title": "2025 Election",
637 "markets": [
638 {
639 "id": "mkt-1",
640 "conditionId": "0xabc",
641 "description": "Who wins?",
642 "question": "Who wins the election?",
643 "marketMakerAddress": "0xaddr"
644 }
645 ]
646 }"#;
647 let event: Event = serde_json::from_str(json).unwrap();
648 assert_eq!(event.markets.len(), 1);
649 assert_eq!(event.markets[0].id, "mkt-1");
650 }
651
652 #[test]
653 fn test_event_volume_24h_rename() {
654 let json = r#"{
656 "id": "evt-1",
657 "volume24h": 5000.0
658 }"#;
659 let event: Event = serde_json::from_str(json).unwrap();
660 assert_eq!(event.volume_24hr, Some(5000.0));
661 }
662
663 #[test]
664 fn test_event_enable_neg_risk_typo_rename() {
665 let json = r#"{
667 "id": "evt-1",
668 "enalbeNegRisk": true
669 }"#;
670 let event: Event = serde_json::from_str(json).unwrap();
671 assert_eq!(event.enable_neg_risk, Some(true));
672 }
673
674 #[test]
677 fn test_series_info() {
678 let json = r#"{"id": "s1", "slug": "nfl-2025", "title": "NFL 2025"}"#;
679 let si: SeriesInfo = serde_json::from_str(json).unwrap();
680 assert_eq!(si.slug, "nfl-2025");
681 assert_eq!(si.title, "NFL 2025");
682 }
683
684 #[test]
687 fn test_series_data_minimal() {
688 let json = r#"{
689 "id": "s1",
690 "slug": "nfl",
691 "title": "NFL",
692 "active": true,
693 "closed": false,
694 "archived": false
695 }"#;
696 let sd: SeriesData = serde_json::from_str(json).unwrap();
697 assert!(sd.active);
698 assert!(!sd.closed);
699 assert!(sd.events.is_empty()); assert!(sd.tags.is_empty());
701 }
702
703 #[test]
706 fn test_sport_metadata() {
707 let json = r#"{
708 "id": 1,
709 "sport": "Basketball",
710 "image": "https://example.com/nba.png",
711 "createdAt": "2024-01-01T00:00:00Z"
712 }"#;
713 let sm: SportMetadata = serde_json::from_str(json).unwrap();
714 assert_eq!(sm.id, 1);
715 assert_eq!(sm.sport, "Basketball");
716 }
717
718 #[test]
721 fn test_team() {
722 let json = r#"{
723 "id": 42,
724 "name": "Lakers",
725 "league": "NBA",
726 "abbreviation": "LAL",
727 "createdAt": "2024-01-01T00:00:00Z",
728 "updatedAt": "2024-06-15T12:00:00Z"
729 }"#;
730 let team: Team = serde_json::from_str(json).unwrap();
731 assert_eq!(team.id, 42);
732 assert_eq!(team.name.as_deref(), Some("Lakers"));
733 assert!(team.created_at.is_some());
734 }
735
736 #[test]
739 fn test_comment_deserialization() {
740 let json = r#"{
741 "id": "c1",
742 "body": "I think this market will resolve yes.",
743 "createdAt": "2024-06-01T10:00:00Z",
744 "updatedAt": "2024-06-01T10:00:00Z",
745 "deletedAt": null,
746 "user": {"id": "u1", "name": "trader1", "avatar": null},
747 "marketId": "mkt-1",
748 "eventId": null,
749 "seriesId": null,
750 "parentId": null,
751 "reactions": [],
752 "positions": [
753 {"tokenId": "t1", "outcome": "Yes", "shares": "100.5"}
754 ],
755 "likeCount": 5,
756 "dislikeCount": 1,
757 "replyCount": 3
758 }"#;
759 let comment: Comment = serde_json::from_str(json).unwrap();
760 assert_eq!(comment.id, "c1");
761 assert_eq!(comment.user.name, "trader1");
762 assert_eq!(comment.like_count, 5);
763 assert_eq!(comment.positions.len(), 1);
764 assert_eq!(comment.positions[0].shares, "100.5");
765 assert!(comment.deleted_at.is_none());
766 }
767
768 #[test]
771 fn test_user_response() {
772 let json = r#"{
773 "proxyWallet": "0xproxy",
774 "address": "0xsigner",
775 "id": "u1",
776 "name": "polytrader"
777 }"#;
778 let user: crate::api::user::UserResponse = serde_json::from_str(json).unwrap();
779 assert_eq!(user.proxy.as_deref(), Some("0xproxy"));
780 assert_eq!(user.name.as_deref(), Some("polytrader"));
781 }
782
783 #[test]
784 fn test_user_response_all_null() {
785 let json = r#"{}"#;
786 let user: crate::api::user::UserResponse = serde_json::from_str(json).unwrap();
787 assert!(user.proxy.is_none());
788 assert!(user.address.is_none());
789 assert!(user.id.is_none());
790 assert!(user.name.is_none());
791 }
792
793 #[test]
796 fn test_cursor_with_next() {
797 let json = r#"{"nextCursor": "abc123"}"#;
798 let cursor: Cursor = serde_json::from_str(json).unwrap();
799 assert_eq!(cursor.next_cursor.as_deref(), Some("abc123"));
800 }
801
802 #[test]
803 fn test_cursor_without_next() {
804 let json = r#"{"nextCursor": null}"#;
805 let cursor: Cursor = serde_json::from_str(json).unwrap();
806 assert!(cursor.next_cursor.is_none());
807 }
808
809 #[test]
810 fn test_paginated_response() {
811 let json = r#"{
812 "data": [{"tokenId": "t1", "outcome": "Yes"}],
813 "nextCursor": "page2"
814 }"#;
815 let resp: PaginatedResponse<MarketToken> = serde_json::from_str(json).unwrap();
816 assert_eq!(resp.data.len(), 1);
817 assert_eq!(resp.next_cursor.as_deref(), Some("page2"));
818 }
819
820 #[test]
821 fn test_paginated_response_empty() {
822 let json = r#"{"data": [], "nextCursor": null}"#;
823 let resp: PaginatedResponse<MarketToken> = serde_json::from_str(json).unwrap();
824 assert!(resp.data.is_empty());
825 assert!(resp.next_cursor.is_none());
826 }
827
828 #[test]
831 fn test_market_token_roundtrip() {
832 let token = MarketToken {
833 token_id: "123".into(),
834 outcome: "Yes".into(),
835 price: Some("0.75".into()),
836 winner: Some(true),
837 };
838 let json = serde_json::to_string(&token).unwrap();
839 let back: MarketToken = serde_json::from_str(&json).unwrap();
840 assert_eq!(token, back);
841 }
842
843 #[test]
844 fn test_tag_roundtrip() {
845 let tag = Tag {
846 id: "1".into(),
847 slug: "test".into(),
848 label: "Test".into(),
849 force_show: None,
850 published_at: None,
851 created_by: None,
852 updated_by: None,
853 created_at: None,
854 updated_at: None,
855 force_hide: None,
856 is_carousel: None,
857 };
858 let json = serde_json::to_string(&tag).unwrap();
859 let back: Tag = serde_json::from_str(&json).unwrap();
860 assert_eq!(tag, back);
861 }
862}