1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
4#[serde(rename_all = "snake_case")]
5pub struct Quote {
6 pub symbol: String,
7 pub name: String,
8 pub price: String,
9 #[serde(skip_serializing_if = "Option::is_none")]
10 pub pre_market_price: Option<String>,
11 #[serde(skip_serializing_if = "Option::is_none")]
12 pub after_hours_price: Option<String>,
13 pub change: String,
14 pub percent_change: String,
15 #[serde(skip_serializing_if = "Option::is_none")]
16 pub open: Option<String>,
17 #[serde(skip_serializing_if = "Option::is_none")]
18 pub high: Option<String>,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub low: Option<String>,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub year_high: Option<String>,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub year_low: Option<String>,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub volume: Option<i64>,
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub avg_volume: Option<i64>,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub market_cap: Option<String>,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub beta: Option<String>,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub pe: Option<String>,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub eps: Option<String>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub dividend: Option<String>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub dividend_yield: Option<String>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub ex_dividend: Option<String>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub net_assets: Option<String>,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub nav: Option<String>,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub expense_ratio: Option<String>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub category: Option<String>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub last_capital_gain: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub morningstar_rating: Option<String>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub morningstar_risk_rating: Option<String>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub holdings_turnover: Option<String>,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub earnings_date: Option<String>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub last_dividend: Option<String>,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub inception_date: Option<String>,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub sector: Option<String>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub industry: Option<String>,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub about: Option<String>,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub employees: Option<String>,
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub five_days_return: Option<String>,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub one_month_return: Option<String>,
77 #[serde(skip_serializing_if = "Option::is_none")]
78 pub three_month_return: Option<String>,
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub six_month_return: Option<String>,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub ytd_return: Option<String>,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub year_return: Option<String>,
85 #[serde(skip_serializing_if = "Option::is_none")]
86 pub three_year_return: Option<String>,
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub five_year_return: Option<String>,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub ten_year_return: Option<String>,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 pub max_return: Option<String>,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub logo: Option<String>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98#[serde(rename_all = "snake_case")]
99pub struct SimpleQuote {
100 pub symbol: String,
101 pub name: String,
102 pub price: String,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub pre_market_price: Option<String>,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub after_hours_price: Option<String>,
107 pub change: String,
108 pub percent_change: String,
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub logo: Option<String>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct DetailedQuote {
115 pub symbol: String,
116 pub name: String,
117 pub price: String,
118 #[serde(skip_serializing_if = "Option::is_none", rename = "preMarketPrice")]
119 pub pre_market_price: Option<String>,
120 #[serde(skip_serializing_if = "Option::is_none", rename = "afterHoursPrice")]
121 pub after_hours_price: Option<String>,
122 pub change: String,
123 #[serde(rename = "percentChange")]
124 pub percent_change: String,
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub open: Option<String>,
127 #[serde(skip_serializing_if = "Option::is_none")]
128 pub high: Option<String>,
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub low: Option<String>,
131 #[serde(skip_serializing_if = "Option::is_none", rename = "yearHigh")]
132 pub year_high: Option<String>,
133 #[serde(skip_serializing_if = "Option::is_none", rename = "yearLow")]
134 pub year_low: Option<String>,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub volume: Option<i64>,
137 #[serde(skip_serializing_if = "Option::is_none", rename = "avgVolume")]
138 pub avg_volume: Option<i64>,
139 #[serde(skip_serializing_if = "Option::is_none", rename = "marketCap")]
140 pub market_cap: Option<String>,
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub beta: Option<String>,
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub pe: Option<String>,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub eps: Option<String>,
147 #[serde(skip_serializing_if = "Option::is_none")]
148 pub dividend: Option<String>,
149 #[serde(skip_serializing_if = "Option::is_none", rename = "yield")]
150 pub dividend_yield: Option<String>,
151 #[serde(skip_serializing_if = "Option::is_none", rename = "exDividend")]
152 pub ex_dividend: Option<String>,
153 #[serde(skip_serializing_if = "Option::is_none", rename = "netAssets")]
154 pub net_assets: Option<String>,
155 #[serde(skip_serializing_if = "Option::is_none")]
156 pub nav: Option<String>,
157 #[serde(skip_serializing_if = "Option::is_none", rename = "expenseRatio")]
158 pub expense_ratio: Option<String>,
159 #[serde(skip_serializing_if = "Option::is_none")]
160 pub category: Option<String>,
161 #[serde(skip_serializing_if = "Option::is_none", rename = "lastCapitalGain")]
162 pub last_capital_gain: Option<String>,
163 #[serde(skip_serializing_if = "Option::is_none", rename = "morningstarRating")]
164 pub morningstar_rating: Option<String>,
165 #[serde(
166 skip_serializing_if = "Option::is_none",
167 rename = "morningstarRiskRating"
168 )]
169 pub morningstar_risk_rating: Option<String>,
170 #[serde(skip_serializing_if = "Option::is_none", rename = "holdingsTurnover")]
171 pub holdings_turnover: Option<String>,
172 #[serde(skip_serializing_if = "Option::is_none", rename = "earningsDate")]
173 pub earnings_date: Option<String>,
174 #[serde(skip_serializing_if = "Option::is_none", rename = "lastDividend")]
175 pub last_dividend: Option<String>,
176 #[serde(skip_serializing_if = "Option::is_none", rename = "inceptionDate")]
177 pub inception_date: Option<String>,
178 #[serde(skip_serializing_if = "Option::is_none")]
179 pub sector: Option<String>,
180 #[serde(skip_serializing_if = "Option::is_none")]
181 pub industry: Option<String>,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub about: Option<String>,
184 #[serde(skip_serializing_if = "Option::is_none")]
185 pub employees: Option<String>,
186 #[serde(skip_serializing_if = "Option::is_none", rename = "fiveDaysReturn")]
187 pub five_days_return: Option<String>,
188 #[serde(skip_serializing_if = "Option::is_none", rename = "oneMonthReturn")]
189 pub one_month_return: Option<String>,
190 #[serde(skip_serializing_if = "Option::is_none", rename = "threeMonthReturn")]
191 pub three_month_return: Option<String>,
192 #[serde(skip_serializing_if = "Option::is_none", rename = "sixMonthReturn")]
193 pub six_month_return: Option<String>,
194 #[serde(skip_serializing_if = "Option::is_none", rename = "ytdReturn")]
195 pub ytd_return: Option<String>,
196 #[serde(skip_serializing_if = "Option::is_none", rename = "yearReturn")]
197 pub year_return: Option<String>,
198 #[serde(skip_serializing_if = "Option::is_none", rename = "threeYearReturn")]
199 pub three_year_return: Option<String>,
200 #[serde(skip_serializing_if = "Option::is_none", rename = "fiveYearReturn")]
201 pub five_year_return: Option<String>,
202 #[serde(skip_serializing_if = "Option::is_none", rename = "tenYearReturn")]
203 pub ten_year_return: Option<String>,
204 #[serde(skip_serializing_if = "Option::is_none", rename = "maxReturn")]
205 pub max_return: Option<String>,
206 #[serde(skip_serializing_if = "Option::is_none")]
207 pub logo: Option<String>,
208}
209
210impl From<Quote> for DetailedQuote {
211 fn from(quote: Quote) -> Self {
212 DetailedQuote {
213 symbol: quote.symbol,
214 name: quote.name,
215 price: quote.price,
216 pre_market_price: quote.pre_market_price,
217 after_hours_price: quote.after_hours_price,
218 change: quote.change,
219 percent_change: quote.percent_change,
220 open: quote.open,
221 high: quote.high,
222 low: quote.low,
223 year_high: quote.year_high,
224 year_low: quote.year_low,
225 volume: quote.volume,
226 avg_volume: quote.avg_volume,
227 market_cap: quote.market_cap,
228 beta: quote.beta,
229 pe: quote.pe,
230 eps: quote.eps,
231 dividend: quote.dividend,
232 dividend_yield: quote.dividend_yield,
233 ex_dividend: quote.ex_dividend,
234 net_assets: quote.net_assets,
235 nav: quote.nav,
236 expense_ratio: quote.expense_ratio,
237 category: quote.category,
238 last_capital_gain: quote.last_capital_gain,
239 morningstar_rating: quote.morningstar_rating,
240 morningstar_risk_rating: quote.morningstar_risk_rating,
241 holdings_turnover: quote.holdings_turnover,
242 earnings_date: quote.earnings_date,
243 last_dividend: quote.last_dividend,
244 inception_date: quote.inception_date,
245 sector: quote.sector,
246 industry: quote.industry,
247 about: quote.about,
248 employees: quote.employees,
249 five_days_return: quote.five_days_return,
250 one_month_return: quote.one_month_return,
251 three_month_return: quote.three_month_return,
252 six_month_return: quote.six_month_return,
253 ytd_return: quote.ytd_return,
254 year_return: quote.year_return,
255 three_year_return: quote.three_year_return,
256 five_year_return: quote.five_year_return,
257 ten_year_return: quote.ten_year_return,
258 max_return: quote.max_return,
259 logo: quote.logo,
260 }
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267 use proptest::prelude::*;
268
269 fn optional_string() -> impl Strategy<Value = Option<String>> {
271 proptest::option::of("[a-zA-Z0-9 .-]{0,20}")
272 }
273
274 fn optional_i64() -> impl Strategy<Value = Option<i64>> {
276 proptest::option::of(0i64..1_000_000_000i64)
277 }
278
279 proptest! {
282 #![proptest_config(ProptestConfig::with_cases(100))]
283
284 #[test]
285 fn simple_quote_roundtrip(
286 symbol in "[A-Z]{1,5}",
287 name in "[A-Za-z ]{1,50}",
288 price in "[0-9]{1,4}\\.[0-9]{2}",
289 pre_market_price in optional_string(),
290 after_hours_price in optional_string(),
291 change in "-?[0-9]{1,3}\\.[0-9]{2}",
292 percent_change in "-?[0-9]{1,3}\\.[0-9]{2}%",
293 logo in optional_string(),
294 ) {
295 let quote = SimpleQuote {
296 symbol: symbol.clone(),
297 name: name.clone(),
298 price: price.clone(),
299 pre_market_price: pre_market_price.clone(),
300 after_hours_price: after_hours_price.clone(),
301 change: change.clone(),
302 percent_change: percent_change.clone(),
303 logo: logo.clone(),
304 };
305
306 let json = serde_json::to_string("e).unwrap();
307 let parsed: SimpleQuote = serde_json::from_str(&json).unwrap();
308
309 prop_assert_eq!(quote.symbol, parsed.symbol);
310 prop_assert_eq!(quote.name, parsed.name);
311 prop_assert_eq!(quote.price, parsed.price);
312 prop_assert_eq!(quote.pre_market_price, parsed.pre_market_price);
313 prop_assert_eq!(quote.after_hours_price, parsed.after_hours_price);
314 prop_assert_eq!(quote.change, parsed.change);
315 prop_assert_eq!(quote.percent_change, parsed.percent_change);
316 prop_assert_eq!(quote.logo, parsed.logo);
317 }
318
319 #[test]
320 fn quote_roundtrip(
321 symbol in "[A-Z]{1,5}",
322 name in "[A-Za-z ]{1,50}",
323 price in "[0-9]{1,4}\\.[0-9]{2}",
324 pre_market_price in optional_string(),
325 after_hours_price in optional_string(),
326 change in "-?[0-9]{1,3}\\.[0-9]{2}",
327 percent_change in "-?[0-9]{1,3}\\.[0-9]{2}%",
328 open in optional_string(),
329 high in optional_string(),
330 low in optional_string(),
331 year_high in optional_string(),
332 year_low in optional_string(),
333 volume in optional_i64(),
334 avg_volume in optional_i64(),
335 market_cap in optional_string(),
336 logo in optional_string(),
337 ) {
338 let quote = Quote {
339 symbol: symbol.clone(),
340 name: name.clone(),
341 price: price.clone(),
342 pre_market_price: pre_market_price.clone(),
343 after_hours_price: after_hours_price.clone(),
344 change: change.clone(),
345 percent_change: percent_change.clone(),
346 open: open.clone(),
347 high: high.clone(),
348 low: low.clone(),
349 year_high: year_high.clone(),
350 year_low: year_low.clone(),
351 volume,
352 avg_volume,
353 market_cap: market_cap.clone(),
354 beta: None,
355 pe: None,
356 eps: None,
357 dividend: None,
358 dividend_yield: None,
359 ex_dividend: None,
360 net_assets: None,
361 nav: None,
362 expense_ratio: None,
363 category: None,
364 last_capital_gain: None,
365 morningstar_rating: None,
366 morningstar_risk_rating: None,
367 holdings_turnover: None,
368 earnings_date: None,
369 last_dividend: None,
370 inception_date: None,
371 sector: None,
372 industry: None,
373 about: None,
374 employees: None,
375 five_days_return: None,
376 one_month_return: None,
377 three_month_return: None,
378 six_month_return: None,
379 ytd_return: None,
380 year_return: None,
381 three_year_return: None,
382 five_year_return: None,
383 ten_year_return: None,
384 max_return: None,
385 logo: logo.clone(),
386 };
387
388 let json = serde_json::to_string("e).unwrap();
389 let parsed: Quote = serde_json::from_str(&json).unwrap();
390
391 prop_assert_eq!(quote.symbol, parsed.symbol);
392 prop_assert_eq!(quote.name, parsed.name);
393 prop_assert_eq!(quote.price, parsed.price);
394 prop_assert_eq!(quote.pre_market_price, parsed.pre_market_price);
395 prop_assert_eq!(quote.after_hours_price, parsed.after_hours_price);
396 prop_assert_eq!(quote.change, parsed.change);
397 prop_assert_eq!(quote.percent_change, parsed.percent_change);
398 prop_assert_eq!(quote.open, parsed.open);
399 prop_assert_eq!(quote.high, parsed.high);
400 prop_assert_eq!(quote.low, parsed.low);
401 prop_assert_eq!(quote.year_high, parsed.year_high);
402 prop_assert_eq!(quote.year_low, parsed.year_low);
403 prop_assert_eq!(quote.volume, parsed.volume);
404 prop_assert_eq!(quote.avg_volume, parsed.avg_volume);
405 prop_assert_eq!(quote.market_cap, parsed.market_cap);
406 prop_assert_eq!(quote.logo, parsed.logo);
407 }
408 }
409}