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
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct DetailedQuote {
116 pub symbol: String,
117 pub name: String,
118 pub price: String,
119 #[serde(skip_serializing_if = "Option::is_none", rename = "preMarketPrice")]
120 pub pre_market_price: Option<String>,
121 #[serde(skip_serializing_if = "Option::is_none", rename = "afterHoursPrice")]
122 pub after_hours_price: Option<String>,
123 pub change: String,
124 #[serde(rename = "percentChange")]
125 pub percent_change: String,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub open: Option<String>,
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub high: Option<String>,
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub low: Option<String>,
132 #[serde(skip_serializing_if = "Option::is_none", rename = "yearHigh")]
133 pub year_high: Option<String>,
134 #[serde(skip_serializing_if = "Option::is_none", rename = "yearLow")]
135 pub year_low: Option<String>,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub volume: Option<i64>,
138 #[serde(skip_serializing_if = "Option::is_none", rename = "avgVolume")]
139 pub avg_volume: Option<i64>,
140 #[serde(skip_serializing_if = "Option::is_none", rename = "marketCap")]
141 pub market_cap: Option<String>,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub beta: Option<String>,
144 #[serde(skip_serializing_if = "Option::is_none")]
145 pub pe: Option<String>,
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub eps: Option<String>,
148 #[serde(skip_serializing_if = "Option::is_none")]
149 pub dividend: Option<String>,
150 #[serde(skip_serializing_if = "Option::is_none", rename = "yield")]
151 pub dividend_yield: Option<String>,
152 #[serde(skip_serializing_if = "Option::is_none", rename = "exDividend")]
153 pub ex_dividend: Option<String>,
154 #[serde(skip_serializing_if = "Option::is_none", rename = "netAssets")]
155 pub net_assets: Option<String>,
156 #[serde(skip_serializing_if = "Option::is_none")]
157 pub nav: Option<String>,
158 #[serde(skip_serializing_if = "Option::is_none", rename = "expenseRatio")]
159 pub expense_ratio: Option<String>,
160 #[serde(skip_serializing_if = "Option::is_none")]
161 pub category: Option<String>,
162 #[serde(skip_serializing_if = "Option::is_none", rename = "lastCapitalGain")]
163 pub last_capital_gain: Option<String>,
164 #[serde(skip_serializing_if = "Option::is_none", rename = "morningstarRating")]
165 pub morningstar_rating: Option<String>,
166 #[serde(skip_serializing_if = "Option::is_none", rename = "morningstarRiskRating")]
167 pub morningstar_risk_rating: Option<String>,
168 #[serde(skip_serializing_if = "Option::is_none", rename = "holdingsTurnover")]
169 pub holdings_turnover: Option<String>,
170 #[serde(skip_serializing_if = "Option::is_none", rename = "earningsDate")]
171 pub earnings_date: Option<String>,
172 #[serde(skip_serializing_if = "Option::is_none", rename = "lastDividend")]
173 pub last_dividend: Option<String>,
174 #[serde(skip_serializing_if = "Option::is_none", rename = "inceptionDate")]
175 pub inception_date: Option<String>,
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub sector: Option<String>,
178 #[serde(skip_serializing_if = "Option::is_none")]
179 pub industry: Option<String>,
180 #[serde(skip_serializing_if = "Option::is_none")]
181 pub about: Option<String>,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub employees: Option<String>,
184 #[serde(skip_serializing_if = "Option::is_none", rename = "fiveDaysReturn")]
185 pub five_days_return: Option<String>,
186 #[serde(skip_serializing_if = "Option::is_none", rename = "oneMonthReturn")]
187 pub one_month_return: Option<String>,
188 #[serde(skip_serializing_if = "Option::is_none", rename = "threeMonthReturn")]
189 pub three_month_return: Option<String>,
190 #[serde(skip_serializing_if = "Option::is_none", rename = "sixMonthReturn")]
191 pub six_month_return: Option<String>,
192 #[serde(skip_serializing_if = "Option::is_none", rename = "ytdReturn")]
193 pub ytd_return: Option<String>,
194 #[serde(skip_serializing_if = "Option::is_none", rename = "yearReturn")]
195 pub year_return: Option<String>,
196 #[serde(skip_serializing_if = "Option::is_none", rename = "threeYearReturn")]
197 pub three_year_return: Option<String>,
198 #[serde(skip_serializing_if = "Option::is_none", rename = "fiveYearReturn")]
199 pub five_year_return: Option<String>,
200 #[serde(skip_serializing_if = "Option::is_none", rename = "tenYearReturn")]
201 pub ten_year_return: Option<String>,
202 #[serde(skip_serializing_if = "Option::is_none", rename = "maxReturn")]
203 pub max_return: Option<String>,
204 #[serde(skip_serializing_if = "Option::is_none")]
205 pub logo: Option<String>,
206}
207
208impl From<Quote> for DetailedQuote {
209 fn from(quote: Quote) -> Self {
210 DetailedQuote {
211 symbol: quote.symbol,
212 name: quote.name,
213 price: quote.price,
214 pre_market_price: quote.pre_market_price,
215 after_hours_price: quote.after_hours_price,
216 change: quote.change,
217 percent_change: quote.percent_change,
218 open: quote.open,
219 high: quote.high,
220 low: quote.low,
221 year_high: quote.year_high,
222 year_low: quote.year_low,
223 volume: quote.volume,
224 avg_volume: quote.avg_volume,
225 market_cap: quote.market_cap,
226 beta: quote.beta,
227 pe: quote.pe,
228 eps: quote.eps,
229 dividend: quote.dividend,
230 dividend_yield: quote.dividend_yield,
231 ex_dividend: quote.ex_dividend,
232 net_assets: quote.net_assets,
233 nav: quote.nav,
234 expense_ratio: quote.expense_ratio,
235 category: quote.category,
236 last_capital_gain: quote.last_capital_gain,
237 morningstar_rating: quote.morningstar_rating,
238 morningstar_risk_rating: quote.morningstar_risk_rating,
239 holdings_turnover: quote.holdings_turnover,
240 earnings_date: quote.earnings_date,
241 last_dividend: quote.last_dividend,
242 inception_date: quote.inception_date,
243 sector: quote.sector,
244 industry: quote.industry,
245 about: quote.about,
246 employees: quote.employees,
247 five_days_return: quote.five_days_return,
248 one_month_return: quote.one_month_return,
249 three_month_return: quote.three_month_return,
250 six_month_return: quote.six_month_return,
251 ytd_return: quote.ytd_return,
252 year_return: quote.year_return,
253 three_year_return: quote.three_year_return,
254 five_year_return: quote.five_year_return,
255 ten_year_return: quote.ten_year_return,
256 max_return: quote.max_return,
257 logo: quote.logo,
258 }
259 }
260}
261
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266 use proptest::prelude::*;
267
268 fn optional_string() -> impl Strategy<Value = Option<String>> {
270 proptest::option::of("[a-zA-Z0-9 .-]{0,20}")
271 }
272
273 fn optional_i64() -> impl Strategy<Value = Option<i64>> {
275 proptest::option::of(0i64..1_000_000_000i64)
276 }
277
278 proptest! {
281 #![proptest_config(ProptestConfig::with_cases(100))]
282
283 #[test]
284 fn simple_quote_roundtrip(
285 symbol in "[A-Z]{1,5}",
286 name in "[A-Za-z ]{1,50}",
287 price in "[0-9]{1,4}\\.[0-9]{2}",
288 pre_market_price in optional_string(),
289 after_hours_price in optional_string(),
290 change in "-?[0-9]{1,3}\\.[0-9]{2}",
291 percent_change in "-?[0-9]{1,3}\\.[0-9]{2}%",
292 logo in optional_string(),
293 ) {
294 let quote = SimpleQuote {
295 symbol: symbol.clone(),
296 name: name.clone(),
297 price: price.clone(),
298 pre_market_price: pre_market_price.clone(),
299 after_hours_price: after_hours_price.clone(),
300 change: change.clone(),
301 percent_change: percent_change.clone(),
302 logo: logo.clone(),
303 };
304
305 let json = serde_json::to_string("e).unwrap();
306 let parsed: SimpleQuote = serde_json::from_str(&json).unwrap();
307
308 prop_assert_eq!(quote.symbol, parsed.symbol);
309 prop_assert_eq!(quote.name, parsed.name);
310 prop_assert_eq!(quote.price, parsed.price);
311 prop_assert_eq!(quote.pre_market_price, parsed.pre_market_price);
312 prop_assert_eq!(quote.after_hours_price, parsed.after_hours_price);
313 prop_assert_eq!(quote.change, parsed.change);
314 prop_assert_eq!(quote.percent_change, parsed.percent_change);
315 prop_assert_eq!(quote.logo, parsed.logo);
316 }
317
318 #[test]
319 fn quote_roundtrip(
320 symbol in "[A-Z]{1,5}",
321 name in "[A-Za-z ]{1,50}",
322 price in "[0-9]{1,4}\\.[0-9]{2}",
323 pre_market_price in optional_string(),
324 after_hours_price in optional_string(),
325 change in "-?[0-9]{1,3}\\.[0-9]{2}",
326 percent_change in "-?[0-9]{1,3}\\.[0-9]{2}%",
327 open in optional_string(),
328 high in optional_string(),
329 low in optional_string(),
330 year_high in optional_string(),
331 year_low in optional_string(),
332 volume in optional_i64(),
333 avg_volume in optional_i64(),
334 market_cap in optional_string(),
335 logo in optional_string(),
336 ) {
337 let quote = Quote {
338 symbol: symbol.clone(),
339 name: name.clone(),
340 price: price.clone(),
341 pre_market_price: pre_market_price.clone(),
342 after_hours_price: after_hours_price.clone(),
343 change: change.clone(),
344 percent_change: percent_change.clone(),
345 open: open.clone(),
346 high: high.clone(),
347 low: low.clone(),
348 year_high: year_high.clone(),
349 year_low: year_low.clone(),
350 volume,
351 avg_volume,
352 market_cap: market_cap.clone(),
353 beta: None,
354 pe: None,
355 eps: None,
356 dividend: None,
357 dividend_yield: None,
358 ex_dividend: None,
359 net_assets: None,
360 nav: None,
361 expense_ratio: None,
362 category: None,
363 last_capital_gain: None,
364 morningstar_rating: None,
365 morningstar_risk_rating: None,
366 holdings_turnover: None,
367 earnings_date: None,
368 last_dividend: None,
369 inception_date: None,
370 sector: None,
371 industry: None,
372 about: None,
373 employees: None,
374 five_days_return: None,
375 one_month_return: None,
376 three_month_return: None,
377 six_month_return: None,
378 ytd_return: None,
379 year_return: None,
380 three_year_return: None,
381 five_year_return: None,
382 ten_year_return: None,
383 max_return: None,
384 logo: logo.clone(),
385 };
386
387 let json = serde_json::to_string("e).unwrap();
388 let parsed: Quote = serde_json::from_str(&json).unwrap();
389
390 prop_assert_eq!(quote.symbol, parsed.symbol);
391 prop_assert_eq!(quote.name, parsed.name);
392 prop_assert_eq!(quote.price, parsed.price);
393 prop_assert_eq!(quote.pre_market_price, parsed.pre_market_price);
394 prop_assert_eq!(quote.after_hours_price, parsed.after_hours_price);
395 prop_assert_eq!(quote.change, parsed.change);
396 prop_assert_eq!(quote.percent_change, parsed.percent_change);
397 prop_assert_eq!(quote.open, parsed.open);
398 prop_assert_eq!(quote.high, parsed.high);
399 prop_assert_eq!(quote.low, parsed.low);
400 prop_assert_eq!(quote.year_high, parsed.year_high);
401 prop_assert_eq!(quote.year_low, parsed.year_low);
402 prop_assert_eq!(quote.volume, parsed.volume);
403 prop_assert_eq!(quote.avg_volume, parsed.avg_volume);
404 prop_assert_eq!(quote.market_cap, parsed.market_cap);
405 prop_assert_eq!(quote.logo, parsed.logo);
406 }
407 }
408}