finance_query_core/models/
sectors.rs

1use serde::{Deserialize, Serialize};
2use std::str::FromStr;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
6pub enum Sector {
7    #[serde(rename = "Basic Materials")]
8    BasicMaterials,
9    #[serde(rename = "Communication Services")]
10    Communication,
11    #[serde(rename = "Consumer Cyclical")]
12    ConsumerCyclical,
13    #[serde(rename = "Consumer Defensive")]
14    ConsumerDefensive,
15    Energy,
16    #[serde(rename = "Financial Services")]
17    FinancialServices,
18    Healthcare,
19    Industrials,
20    #[serde(rename = "Real Estate")]
21    RealEstate,
22    Technology,
23    Utilities,
24}
25
26impl Sector {
27    pub fn as_str(&self) -> &'static str {
28        match self {
29            Sector::BasicMaterials => "Basic Materials",
30            Sector::Communication => "Communication Services",
31            Sector::ConsumerCyclical => "Consumer Cyclical",
32            Sector::ConsumerDefensive => "Consumer Defensive",
33            Sector::Energy => "Energy",
34            Sector::FinancialServices => "Financial Services",
35            Sector::Healthcare => "Healthcare",
36            Sector::Industrials => "Industrials",
37            Sector::RealEstate => "Real Estate",
38            Sector::Technology => "Technology",
39            Sector::Utilities => "Utilities",
40        }
41    }
42
43    #[allow(dead_code)]
44    pub fn url_path(&self) -> &'static str {
45        match self {
46            Sector::BasicMaterials => "basic-materials",
47            Sector::Communication => "communication-services",
48            Sector::ConsumerCyclical => "consumer-cyclical",
49            Sector::ConsumerDefensive => "consumer-defensive",
50            Sector::Energy => "energy",
51            Sector::FinancialServices => "financial-services",
52            Sector::Healthcare => "healthcare",
53            Sector::Industrials => "industrials",
54            Sector::RealEstate => "real-estate",
55            Sector::Technology => "technology",
56            Sector::Utilities => "utilities",
57        }
58    }
59
60    #[allow(dead_code)]
61    pub fn all() -> Vec<Sector> {
62        vec![
63            Sector::Technology,
64            Sector::Healthcare,
65            Sector::FinancialServices,
66            Sector::ConsumerCyclical,
67            Sector::Industrials,
68            Sector::ConsumerDefensive,
69            Sector::Energy,
70            Sector::RealEstate,
71            Sector::Utilities,
72            Sector::BasicMaterials,
73            Sector::Communication,
74        ]
75    }
76}
77
78impl FromStr for Sector {
79    type Err = String;
80
81    fn from_str(s: &str) -> Result<Self, Self::Err> {
82        match s {
83            "Basic Materials" => Ok(Sector::BasicMaterials),
84            "Communication Services" => Ok(Sector::Communication),
85            "Consumer Cyclical" => Ok(Sector::ConsumerCyclical),
86            "Consumer Defensive" => Ok(Sector::ConsumerDefensive),
87            "Energy" => Ok(Sector::Energy),
88            "Financial Services" => Ok(Sector::FinancialServices),
89            "Healthcare" => Ok(Sector::Healthcare),
90            "Industrials" => Ok(Sector::Industrials),
91            "Real Estate" => Ok(Sector::RealEstate),
92            "Technology" => Ok(Sector::Technology),
93            "Utilities" => Ok(Sector::Utilities),
94            _ => Err(format!("Invalid sector: {}", s)),
95        }
96    }
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct MarketSector {
101    pub sector: String,
102    #[serde(rename = "dayReturn")]
103    pub day_return: String,
104    #[serde(rename = "ytdReturn")]
105    pub ytd_return: String,
106    #[serde(rename = "yearReturn")]
107    pub year_return: String,
108    #[serde(rename = "threeYearReturn")]
109    pub three_year_return: String,
110    #[serde(rename = "fiveYearReturn")]
111    pub five_year_return: String,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct MarketSectorDetails {
116    pub sector: String,
117    #[serde(rename = "dayReturn")]
118    pub day_return: String,
119    #[serde(rename = "ytdReturn")]
120    pub ytd_return: String,
121    #[serde(rename = "yearReturn")]
122    pub year_return: String,
123    #[serde(rename = "threeYearReturn")]
124    pub three_year_return: String,
125    #[serde(rename = "fiveYearReturn")]
126    pub five_year_return: String,
127    #[serde(rename = "marketCap")]
128    pub market_cap: String,
129    #[serde(rename = "marketWeight")]
130    pub market_weight: String,
131    pub industries: i32,
132    pub companies: i32,
133    #[serde(rename = "topIndustries")]
134    pub top_industries: Vec<String>,
135    #[serde(rename = "topCompanies")]
136    pub top_companies: Vec<String>,
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use proptest::prelude::*;
143
144    // **Feature: crate-extraction, Property 1: Model Serialization Round-Trip**
145    // **Validates: Requirements 2.2**
146    proptest! {
147        #![proptest_config(ProptestConfig::with_cases(100))]
148
149        #[test]
150        fn market_sector_roundtrip(
151            sector in "[A-Za-z ]{1,30}",
152            day_return in "-?[0-9]{1,3}\\.[0-9]{2}%",
153            ytd_return in "-?[0-9]{1,3}\\.[0-9]{2}%",
154            year_return in "-?[0-9]{1,3}\\.[0-9]{2}%",
155            three_year_return in "-?[0-9]{1,3}\\.[0-9]{2}%",
156            five_year_return in "-?[0-9]{1,3}\\.[0-9]{2}%",
157        ) {
158            let ms = MarketSector {
159                sector: sector.clone(),
160                day_return: day_return.clone(),
161                ytd_return: ytd_return.clone(),
162                year_return: year_return.clone(),
163                three_year_return: three_year_return.clone(),
164                five_year_return: five_year_return.clone(),
165            };
166
167            let json = serde_json::to_string(&ms).unwrap();
168            let parsed: MarketSector = serde_json::from_str(&json).unwrap();
169
170            prop_assert_eq!(ms.sector, parsed.sector);
171            prop_assert_eq!(ms.day_return, parsed.day_return);
172            prop_assert_eq!(ms.ytd_return, parsed.ytd_return);
173            prop_assert_eq!(ms.year_return, parsed.year_return);
174        }
175
176        #[test]
177        fn sector_roundtrip(sector in prop_oneof![
178            Just(Sector::BasicMaterials),
179            Just(Sector::Communication),
180            Just(Sector::ConsumerCyclical),
181            Just(Sector::ConsumerDefensive),
182            Just(Sector::Energy),
183            Just(Sector::FinancialServices),
184            Just(Sector::Healthcare),
185            Just(Sector::Industrials),
186            Just(Sector::RealEstate),
187            Just(Sector::Technology),
188            Just(Sector::Utilities),
189        ]) {
190            let json = serde_json::to_string(&sector).unwrap();
191            let parsed: Sector = serde_json::from_str(&json).unwrap();
192
193            prop_assert_eq!(sector.as_str(), parsed.as_str());
194        }
195    }
196}