Skip to main content

finance_query_core/models/
indices.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub enum Region {
7    #[serde(rename = "US")]
8    UnitedStates,
9    #[serde(rename = "NA")]
10    NorthAmerica,
11    #[serde(rename = "SA")]
12    SouthAmerica,
13    #[serde(rename = "EU")]
14    Europe,
15    #[serde(rename = "AS")]
16    Asia,
17    #[serde(rename = "AF")]
18    Africa,
19    #[serde(rename = "ME")]
20    MiddleEast,
21    #[serde(rename = "OCE")]
22    Oceania,
23    #[serde(rename = "global")]
24    Global,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub enum Index {
30    // United States
31    #[serde(rename = "snp")]
32    Gspc, // S&P 500
33    #[serde(rename = "djia")]
34    Dji, // Dow Jones Industrial Average
35    #[serde(rename = "nasdaq")]
36    Ixic, // NASDAQ Composite
37    #[serde(rename = "nyse-composite")]
38    Nya, // NYSE Composite
39    #[serde(rename = "nyse-amex")]
40    Xax, // NYSE American Composite
41    #[serde(rename = "rut")]
42    Rut, // Russell 2000
43    #[serde(rename = "vix")]
44    Vix, // CBOE Volatility Index
45
46    // North America (excluding US)
47    #[serde(rename = "tsx-composite")]
48    Gsptse, // Toronto Stock Exchange
49
50    // South America
51    #[serde(rename = "ibovespa")]
52    Bvsp, // Brazil Bovespa
53    #[serde(rename = "ipc-mexico")]
54    Mxx, // Mexican IPC
55    #[serde(rename = "ipsa")]
56    Ipsa, // Chile IPSA
57    #[serde(rename = "merval")]
58    Merv, // Argentina Merval
59    #[serde(rename = "ivbx")]
60    Ivbx, // Brazil IVBX
61    #[serde(rename = "ibrx-50")]
62    Ibrx50, // Brazil IBrX-50
63
64    // Europe
65    #[serde(rename = "ftse-100")]
66    Ftse, // FTSE 100
67    #[serde(rename = "dax")]
68    Gdaxi, // German DAX
69    #[serde(rename = "cac-40")]
70    Fchi, // French CAC 40
71    #[serde(rename = "euro-stoxx-50")]
72    Stoxx50e, // Euro Stoxx 50
73    #[serde(rename = "euronext-100")]
74    N100, // Euronext 100
75    #[serde(rename = "bel-20")]
76    Bfx, // Belgian BEL 20
77    #[serde(rename = "moex")]
78    MoexMe, // Moscow Exchange
79    #[serde(rename = "aex")]
80    Aex, // Amsterdam Exchange
81    #[serde(rename = "ibex-35")]
82    Ibex, // Spanish IBEX 35
83    #[serde(rename = "ftse-mib")]
84    Ftsemib, // Italian FTSE MIB
85    #[serde(rename = "smi")]
86    Ssmi, // Swiss Market Index
87    #[serde(rename = "psi")]
88    Psi, // Portuguese PSI
89    #[serde(rename = "atx")]
90    Atx, // Austrian ATX
91    #[serde(rename = "omxs30")]
92    Omxs30, // Stockholm OMX 30
93    #[serde(rename = "omxc25")]
94    Omxc25, // Copenhagen OMX 25
95    #[serde(rename = "wig20")]
96    Wig20, // Warsaw WIG 20
97    #[serde(rename = "budapest-se")]
98    Bux, // Budapest Stock Exchange
99    #[serde(rename = "moex-russia")]
100    Imoex, // Moscow Exchange Russia
101    #[serde(rename = "rtsi")]
102    Rtsi, // Russian Trading System
103
104    // Asia
105    #[serde(rename = "hang-seng")]
106    Hsi, // Hong Kong Hang Seng
107    #[serde(rename = "sti")]
108    Sti, // Singapore Straits Times
109    #[serde(rename = "sensex")]
110    Bsesn, // BSE Sensex (India)
111    #[serde(rename = "idx-composite")]
112    Jkse, // Jakarta Composite
113    #[serde(rename = "ftse-bursa")]
114    Klse, // FTSE Bursa Malaysia
115    #[serde(rename = "kospi")]
116    Ks11, // Korea KOSPI
117    #[serde(rename = "twse")]
118    Twii, // Taiwan TAIEX
119    #[serde(rename = "nikkei-225")]
120    N225, // Nikkei 225
121    #[serde(rename = "shanghai")]
122    Shanghai, // Shanghai Composite
123    #[serde(rename = "szse-component")]
124    Szse, // Shenzhen Component
125    #[serde(rename = "set")]
126    Set, // Thailand SET
127    #[serde(rename = "nifty-50")]
128    Nsei, // NSE Nifty 50 (India)
129    #[serde(rename = "nifty-200")]
130    Cnx200, // NSE Nifty 200
131    #[serde(rename = "psei-composite")]
132    Psei, // Philippines PSEi Composite
133    #[serde(rename = "china-a50")]
134    ChinaA50, // FTSE China A50
135    #[serde(rename = "dj-shanghai")]
136    Djsh, // Dow Jones Shanghai
137    #[serde(rename = "india-vix")]
138    Indiavix, // India VIX
139
140    // Africa
141    #[serde(rename = "egx-30")]
142    Case30, // Egypt EGX 30
143    #[serde(rename = "jse-40")]
144    Jn0uJo, // FTSE JSE Top 40- USD Net TRI
145    #[serde(rename = "ftse-jse")]
146    Ftsejse, // FTSE/JSE SA Financials Index
147    #[serde(rename = "afr-40")]
148    Afr40, // All Africa 40 Rand Index
149    #[serde(rename = "raf-40")]
150    Raf40, // RAFI 40 Index
151    #[serde(rename = "sa-40")]
152    Sa40, // South Africa Top 40
153    #[serde(rename = "alt-15")]
154    Alt15, // Alternative 15
155
156    // Middle East
157    #[serde(rename = "ta-125")]
158    Ta125Ta, // Tel Aviv 125
159    #[serde(rename = "ta-35")]
160    Ta35, // Tel Aviv 35
161    #[serde(rename = "tadawul-all-share")]
162    Tasi, // Tadawul All Share
163    #[serde(rename = "tamayuz")]
164    Tamayuz, // Egyptian Tamayuz
165    #[serde(rename = "bist-100")]
166    Bist100, // Borsa Istanbul 100
167
168    // Oceania
169    #[serde(rename = "asx-200")]
170    Axjo, // ASX 200 (Australia)
171    #[serde(rename = "all-ordinaries")]
172    Aord, // All Ordinaries (Australia)
173    #[serde(rename = "nzx-50")]
174    Nz50, // NZX 50 (New Zealand)
175
176    // Global/Currency
177    #[serde(rename = "usd")]
178    DxYNyb, // US Dollar Index
179    #[serde(rename = "msci-europe")]
180    UsdStrd, // MSCI Europe USD
181    #[serde(rename = "gbp")]
182    Xdb, // British Pound
183    #[serde(rename = "euro")]
184    Xde, // Euro
185    #[serde(rename = "yen")]
186    Xdn, // Japanese Yen
187    #[serde(rename = "australian")]
188    Xda, // Australian Dollar
189    #[serde(rename = "msci-world")]
190    MsciWorld, // MSCI World Index
191    #[serde(rename = "cboe-uk-100")]
192    Buk100p, // CBOE UK 100
193}
194
195impl Index {
196    pub fn as_str(&self) -> &'static str {
197        match self {
198            Index::Gspc => "snp",
199            Index::Dji => "djia",
200            Index::Ixic => "nasdaq",
201            Index::Nya => "nyse-composite",
202            Index::Xax => "nyse-amex",
203            Index::Rut => "rut",
204            Index::Vix => "vix",
205            Index::Gsptse => "tsx-composite",
206            Index::Bvsp => "ibovespa",
207            Index::Mxx => "ipc-mexico",
208            Index::Ipsa => "ipsa",
209            Index::Merv => "merval",
210            Index::Ivbx => "ivbx",
211            Index::Ibrx50 => "ibrx-50",
212            Index::Ftse => "ftse-100",
213            Index::Gdaxi => "dax",
214            Index::Fchi => "cac-40",
215            Index::Stoxx50e => "euro-stoxx-50",
216            Index::N100 => "euronext-100",
217            Index::Bfx => "bel-20",
218            Index::MoexMe => "moex",
219            Index::Aex => "aex",
220            Index::Ibex => "ibex-35",
221            Index::Ftsemib => "ftse-mib",
222            Index::Ssmi => "smi",
223            Index::Psi => "psi",
224            Index::Atx => "atx",
225            Index::Omxs30 => "omxs30",
226            Index::Omxc25 => "omxc25",
227            Index::Wig20 => "wig20",
228            Index::Bux => "budapest-se",
229            Index::Imoex => "moex-russia",
230            Index::Rtsi => "rtsi",
231            Index::Hsi => "hang-seng",
232            Index::Sti => "sti",
233            Index::Bsesn => "sensex",
234            Index::Jkse => "idx-composite",
235            Index::Klse => "ftse-bursa",
236            Index::Ks11 => "kospi",
237            Index::Twii => "twse",
238            Index::N225 => "nikkei-225",
239            Index::Shanghai => "shanghai",
240            Index::Szse => "szse-component",
241            Index::Set => "set",
242            Index::Nsei => "nifty-50",
243            Index::Cnx200 => "nifty-200",
244            Index::Psei => "psei-composite",
245            Index::ChinaA50 => "china-a50",
246            Index::Djsh => "dj-shanghai",
247            Index::Indiavix => "india-vix",
248            Index::Case30 => "egx-30",
249            Index::Jn0uJo => "jse-40",
250            Index::Ftsejse => "ftse-jse",
251            Index::Afr40 => "afr-40",
252            Index::Raf40 => "raf-40",
253            Index::Sa40 => "sa-40",
254            Index::Alt15 => "alt-15",
255            Index::Ta125Ta => "ta-125",
256            Index::Ta35 => "ta-35",
257            Index::Tasi => "tadawul-all-share",
258            Index::Tamayuz => "tamayuz",
259            Index::Bist100 => "bist-100",
260            Index::Axjo => "asx-200",
261            Index::Aord => "all-ordinaries",
262            Index::Nz50 => "nzx-50",
263            Index::DxYNyb => "usd",
264            Index::UsdStrd => "msci-europe",
265            Index::Xdb => "gbp",
266            Index::Xde => "euro",
267            Index::Xdn => "yen",
268            Index::Xda => "australian",
269            Index::MsciWorld => "msci-world",
270            Index::Buk100p => "cboe-uk-100",
271        }
272    }
273
274    pub fn parse(s: &str) -> Option<Self> {
275        match s {
276            "snp" => Some(Index::Gspc),
277            "djia" => Some(Index::Dji),
278            "nasdaq" => Some(Index::Ixic),
279            "nyse-composite" => Some(Index::Nya),
280            "nyse-amex" => Some(Index::Xax),
281            "rut" => Some(Index::Rut),
282            "vix" => Some(Index::Vix),
283            "tsx-composite" => Some(Index::Gsptse),
284            "ibovespa" => Some(Index::Bvsp),
285            "ipc-mexico" => Some(Index::Mxx),
286            "ipsa" => Some(Index::Ipsa),
287            "merval" => Some(Index::Merv),
288            "ivbx" => Some(Index::Ivbx),
289            "ibrx-50" => Some(Index::Ibrx50),
290            "ftse-100" => Some(Index::Ftse),
291            "dax" => Some(Index::Gdaxi),
292            "cac-40" => Some(Index::Fchi),
293            "euro-stoxx-50" => Some(Index::Stoxx50e),
294            "euronext-100" => Some(Index::N100),
295            "bel-20" => Some(Index::Bfx),
296            "moex" => Some(Index::MoexMe),
297            "aex" => Some(Index::Aex),
298            "ibex-35" => Some(Index::Ibex),
299            "ftse-mib" => Some(Index::Ftsemib),
300            "smi" => Some(Index::Ssmi),
301            "psi" => Some(Index::Psi),
302            "atx" => Some(Index::Atx),
303            "omxs30" => Some(Index::Omxs30),
304            "omxc25" => Some(Index::Omxc25),
305            "wig20" => Some(Index::Wig20),
306            "budapest-se" => Some(Index::Bux),
307            "moex-russia" => Some(Index::Imoex),
308            "rtsi" => Some(Index::Rtsi),
309            "hang-seng" => Some(Index::Hsi),
310            "sti" => Some(Index::Sti),
311            "sensex" => Some(Index::Bsesn),
312            "idx-composite" => Some(Index::Jkse),
313            "ftse-bursa" => Some(Index::Klse),
314            "kospi" => Some(Index::Ks11),
315            "twse" => Some(Index::Twii),
316            "nikkei-225" => Some(Index::N225),
317            "shanghai" => Some(Index::Shanghai),
318            "szse-component" => Some(Index::Szse),
319            "set" => Some(Index::Set),
320            "nifty-50" => Some(Index::Nsei),
321            "nifty-200" => Some(Index::Cnx200),
322            "psei-composite" => Some(Index::Psei),
323            "china-a50" => Some(Index::ChinaA50),
324            "dj-shanghai" => Some(Index::Djsh),
325            "india-vix" => Some(Index::Indiavix),
326            "egx-30" => Some(Index::Case30),
327            "jse-40" => Some(Index::Jn0uJo),
328            "ftse-jse" => Some(Index::Ftsejse),
329            "afr-40" => Some(Index::Afr40),
330            "raf-40" => Some(Index::Raf40),
331            "sa-40" => Some(Index::Sa40),
332            "alt-15" => Some(Index::Alt15),
333            "ta-125" => Some(Index::Ta125Ta),
334            "ta-35" => Some(Index::Ta35),
335            "tadawul-all-share" => Some(Index::Tasi),
336            "tamayuz" => Some(Index::Tamayuz),
337            "bist-100" => Some(Index::Bist100),
338            "asx-200" => Some(Index::Axjo),
339            "all-ordinaries" => Some(Index::Aord),
340            "nzx-50" => Some(Index::Nz50),
341            "usd" => Some(Index::DxYNyb),
342            "msci-europe" => Some(Index::UsdStrd),
343            "gbp" => Some(Index::Xdb),
344            "euro" => Some(Index::Xde),
345            "yen" => Some(Index::Xdn),
346            "australian" => Some(Index::Xda),
347            "msci-world" => Some(Index::MsciWorld),
348            "cboe-uk-100" => Some(Index::Buk100p),
349            _ => None,
350        }
351    }
352
353    pub fn all() -> Vec<Index> {
354        vec![
355            Index::Gspc,
356            Index::Dji,
357            Index::Ixic,
358            Index::Nya,
359            Index::Xax,
360            Index::Rut,
361            Index::Vix,
362            Index::Gsptse,
363            Index::Bvsp,
364            Index::Mxx,
365            Index::Ipsa,
366            Index::Merv,
367            Index::Ivbx,
368            Index::Ibrx50,
369            Index::Ftse,
370            Index::Gdaxi,
371            Index::Fchi,
372            Index::Stoxx50e,
373            Index::N100,
374            Index::Bfx,
375            Index::MoexMe,
376            Index::Aex,
377            Index::Ibex,
378            Index::Ftsemib,
379            Index::Ssmi,
380            Index::Psi,
381            Index::Atx,
382            Index::Omxs30,
383            Index::Omxc25,
384            Index::Wig20,
385            Index::Bux,
386            Index::Imoex,
387            Index::Rtsi,
388            Index::Hsi,
389            Index::Sti,
390            Index::Bsesn,
391            Index::Jkse,
392            Index::Klse,
393            Index::Ks11,
394            Index::Twii,
395            Index::N225,
396            Index::Shanghai,
397            Index::Szse,
398            Index::Set,
399            Index::Nsei,
400            Index::Cnx200,
401            Index::Psei,
402            Index::ChinaA50,
403            Index::Djsh,
404            Index::Indiavix,
405            Index::Case30,
406            Index::Jn0uJo,
407            Index::Ftsejse,
408            Index::Afr40,
409            Index::Raf40,
410            Index::Sa40,
411            Index::Alt15,
412            Index::Ta125Ta,
413            Index::Ta35,
414            Index::Tasi,
415            Index::Tamayuz,
416            Index::Bist100,
417            Index::Axjo,
418            Index::Aord,
419            Index::Nz50,
420            Index::DxYNyb,
421            Index::UsdStrd,
422            Index::Xdb,
423            Index::Xde,
424            Index::Xdn,
425            Index::Xda,
426            Index::MsciWorld,
427            Index::Buk100p,
428        ]
429    }
430}
431
432pub fn get_index_regions() -> HashMap<Index, Region> {
433    let mut map = HashMap::new();
434    // United States
435    map.insert(Index::Gspc, Region::UnitedStates);
436    map.insert(Index::Dji, Region::UnitedStates);
437    map.insert(Index::Ixic, Region::UnitedStates);
438    map.insert(Index::Nya, Region::UnitedStates);
439    map.insert(Index::Xax, Region::UnitedStates);
440    map.insert(Index::Rut, Region::UnitedStates);
441    map.insert(Index::Vix, Region::UnitedStates);
442    // North America (excluding US)
443    map.insert(Index::Gsptse, Region::NorthAmerica);
444    // South America
445    map.insert(Index::Bvsp, Region::SouthAmerica);
446    map.insert(Index::Mxx, Region::SouthAmerica);
447    map.insert(Index::Ipsa, Region::SouthAmerica);
448    map.insert(Index::Merv, Region::SouthAmerica);
449    map.insert(Index::Ivbx, Region::SouthAmerica);
450    map.insert(Index::Ibrx50, Region::SouthAmerica);
451    // Europe
452    map.insert(Index::Ftse, Region::Europe);
453    map.insert(Index::Gdaxi, Region::Europe);
454    map.insert(Index::Fchi, Region::Europe);
455    map.insert(Index::Stoxx50e, Region::Europe);
456    map.insert(Index::N100, Region::Europe);
457    map.insert(Index::Bfx, Region::Europe);
458    map.insert(Index::MoexMe, Region::Europe);
459    map.insert(Index::Aex, Region::Europe);
460    map.insert(Index::Ibex, Region::Europe);
461    map.insert(Index::Ftsemib, Region::Europe);
462    map.insert(Index::Ssmi, Region::Europe);
463    map.insert(Index::Psi, Region::Europe);
464    map.insert(Index::Atx, Region::Europe);
465    map.insert(Index::Omxs30, Region::Europe);
466    map.insert(Index::Omxc25, Region::Europe);
467    map.insert(Index::Wig20, Region::Europe);
468    map.insert(Index::Bux, Region::Europe);
469    map.insert(Index::Imoex, Region::Europe);
470    map.insert(Index::Rtsi, Region::Europe);
471    // Asia
472    map.insert(Index::Hsi, Region::Asia);
473    map.insert(Index::Sti, Region::Asia);
474    map.insert(Index::Bsesn, Region::Asia);
475    map.insert(Index::Jkse, Region::Asia);
476    map.insert(Index::Klse, Region::Asia);
477    map.insert(Index::Ks11, Region::Asia);
478    map.insert(Index::Twii, Region::Asia);
479    map.insert(Index::N225, Region::Asia);
480    map.insert(Index::Shanghai, Region::Asia);
481    map.insert(Index::Szse, Region::Asia);
482    map.insert(Index::Set, Region::Asia);
483    map.insert(Index::Nsei, Region::Asia);
484    map.insert(Index::Cnx200, Region::Asia);
485    map.insert(Index::Psei, Region::Asia);
486    map.insert(Index::ChinaA50, Region::Asia);
487    map.insert(Index::Djsh, Region::Asia);
488    map.insert(Index::Indiavix, Region::Asia);
489    // Africa
490    map.insert(Index::Case30, Region::Africa);
491    map.insert(Index::Jn0uJo, Region::Africa);
492    map.insert(Index::Ftsejse, Region::Africa);
493    map.insert(Index::Afr40, Region::Africa);
494    map.insert(Index::Sa40, Region::Africa);
495    map.insert(Index::Raf40, Region::Africa);
496    map.insert(Index::Alt15, Region::Africa);
497    // Middle East
498    map.insert(Index::Ta125Ta, Region::MiddleEast);
499    map.insert(Index::Ta35, Region::MiddleEast);
500    map.insert(Index::Tasi, Region::MiddleEast);
501    map.insert(Index::Tamayuz, Region::MiddleEast);
502    map.insert(Index::Bist100, Region::MiddleEast);
503    // Oceania
504    map.insert(Index::Axjo, Region::Oceania);
505    map.insert(Index::Aord, Region::Oceania);
506    map.insert(Index::Nz50, Region::Oceania);
507    // Global/Currency
508    map.insert(Index::DxYNyb, Region::Global);
509    map.insert(Index::UsdStrd, Region::Global);
510    map.insert(Index::Xdb, Region::Global);
511    map.insert(Index::Xde, Region::Global);
512    map.insert(Index::Xdn, Region::Global);
513    map.insert(Index::Xda, Region::Global);
514    map.insert(Index::MsciWorld, Region::Global);
515    map.insert(Index::Buk100p, Region::Global);
516    map
517}
518
519#[derive(Debug, Clone, Serialize, Deserialize)]
520#[serde(rename_all = "snake_case")]
521pub struct MarketIndex {
522    pub name: String,
523    pub value: f64,
524    pub change: String,
525    #[serde(rename = "percentChange")]
526    pub percent_change: String,
527    #[serde(skip_serializing_if = "Option::is_none", rename = "fiveDaysReturn")]
528    pub five_days_return: Option<String>,
529    #[serde(skip_serializing_if = "Option::is_none", rename = "oneMonthReturn")]
530    pub one_month_return: Option<String>,
531    #[serde(skip_serializing_if = "Option::is_none", rename = "threeMonthReturn")]
532    pub three_month_return: Option<String>,
533    #[serde(skip_serializing_if = "Option::is_none", rename = "sixMonthReturn")]
534    pub six_month_return: Option<String>,
535    #[serde(skip_serializing_if = "Option::is_none", rename = "ytdReturn")]
536    pub ytd_return: Option<String>,
537    #[serde(skip_serializing_if = "Option::is_none", rename = "yearReturn")]
538    pub year_return: Option<String>,
539    #[serde(skip_serializing_if = "Option::is_none", rename = "threeYearReturn")]
540    pub three_year_return: Option<String>,
541    #[serde(skip_serializing_if = "Option::is_none", rename = "fiveYearReturn")]
542    pub five_year_return: Option<String>,
543    #[serde(skip_serializing_if = "Option::is_none", rename = "tenYearReturn")]
544    pub ten_year_return: Option<String>,
545    #[serde(skip_serializing_if = "Option::is_none", rename = "maxReturn")]
546    pub max_return: Option<String>,
547}
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552    use proptest::prelude::*;
553
554    fn optional_string() -> impl Strategy<Value = Option<String>> {
555        proptest::option::of("[A-Za-z0-9 .%-]{0,20}")
556    }
557
558    // **Feature: crate-extraction, Property 1: Model Serialization Round-Trip**
559    // **Validates: Requirements 2.2**
560    proptest! {
561        #![proptest_config(ProptestConfig::with_cases(100))]
562
563        #[test]
564        fn market_index_roundtrip(
565            name in "[A-Za-z0-9 ]{1,30}",
566            value in 0.01f64..100000.0f64,
567            change in "-?[0-9]{1,5}\\.[0-9]{2}",
568            percent_change in "-?[0-9]{1,3}\\.[0-9]{2}%",
569            five_days_return in optional_string(),
570            one_month_return in optional_string(),
571        ) {
572            let index = MarketIndex {
573                name: name.clone(),
574                value,
575                change: change.clone(),
576                percent_change: percent_change.clone(),
577                five_days_return: five_days_return.clone(),
578                one_month_return: one_month_return.clone(),
579                three_month_return: None,
580                six_month_return: None,
581                ytd_return: None,
582                year_return: None,
583                three_year_return: None,
584                five_year_return: None,
585                ten_year_return: None,
586                max_return: None,
587            };
588
589            let json = serde_json::to_string(&index).unwrap();
590            let parsed: MarketIndex = serde_json::from_str(&json).unwrap();
591
592            prop_assert_eq!(index.name, parsed.name);
593            prop_assert!((index.value - parsed.value).abs() < 1e-10);
594            prop_assert_eq!(index.change, parsed.change);
595            prop_assert_eq!(index.percent_change, parsed.percent_change);
596        }
597
598        #[test]
599        fn region_roundtrip(region in prop_oneof![
600            Just(Region::UnitedStates),
601            Just(Region::NorthAmerica),
602            Just(Region::SouthAmerica),
603            Just(Region::Europe),
604            Just(Region::Asia),
605            Just(Region::Africa),
606            Just(Region::MiddleEast),
607            Just(Region::Oceania),
608            Just(Region::Global),
609        ]) {
610            let json = serde_json::to_string(&region).unwrap();
611            let parsed: Region = serde_json::from_str(&json).unwrap();
612
613            prop_assert_eq!(region, parsed);
614        }
615    }
616}