finance_query/models/sentiment/
response.rs1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[non_exhaustive]
10pub enum FearGreedLabel {
11 #[serde(rename = "Extreme Fear")]
13 ExtremeFear,
14 Fear,
16 Neutral,
18 Greed,
20 #[serde(rename = "Extreme Greed")]
22 ExtremeGreed,
23}
24
25impl FearGreedLabel {
26 pub fn as_str(&self) -> &'static str {
28 match self {
29 Self::ExtremeFear => "Extreme Fear",
30 Self::Fear => "Fear",
31 Self::Neutral => "Neutral",
32 Self::Greed => "Greed",
33 Self::ExtremeGreed => "Extreme Greed",
34 }
35 }
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
42#[non_exhaustive]
43pub struct FearAndGreed {
44 pub value: u8,
46 pub classification: FearGreedLabel,
48 pub timestamp: i64,
50}
51
52#[derive(Debug, Deserialize)]
55pub(crate) struct FearAndGreedApiResponse {
56 pub data: Vec<FearAndGreedEntry>,
57}
58
59#[derive(Debug, Deserialize)]
60pub(crate) struct FearAndGreedEntry {
61 pub value: String,
62 pub value_classification: String,
63 pub timestamp: String,
64}
65
66impl FearAndGreed {
67 pub(crate) fn from_response(
68 resp: FearAndGreedApiResponse,
69 ) -> Result<Self, crate::error::FinanceError> {
70 let entry = resp.data.into_iter().next().ok_or_else(|| {
71 crate::error::FinanceError::ResponseStructureError {
72 field: "data".to_string(),
73 context: "Alternative.me API returned empty data array".to_string(),
74 }
75 })?;
76
77 Self::from_entry(entry)
78 }
79
80 pub(crate) fn vec_from_response(
83 resp: FearAndGreedApiResponse,
84 ) -> Result<Vec<Self>, crate::error::FinanceError> {
85 resp.data.into_iter().map(Self::from_entry).collect()
86 }
87
88 fn from_entry(entry: FearAndGreedEntry) -> Result<Self, crate::error::FinanceError> {
89 let value = entry.value.parse::<u8>().map_err(|_| {
90 crate::error::FinanceError::ResponseStructureError {
91 field: "value".to_string(),
92 context: format!("Cannot parse '{}' as u8", entry.value),
93 }
94 })?;
95
96 let classification = parse_classification(&entry.value_classification)?;
97
98 let timestamp = entry.timestamp.parse::<i64>().map_err(|_| {
99 crate::error::FinanceError::ResponseStructureError {
100 field: "timestamp".to_string(),
101 context: format!("Cannot parse '{}' as i64", entry.timestamp),
102 }
103 })?;
104
105 Ok(Self {
106 value,
107 classification,
108 timestamp,
109 })
110 }
111}
112
113pub(crate) fn parse_classification(s: &str) -> Result<FearGreedLabel, crate::error::FinanceError> {
114 match s {
115 "Extreme Fear" => Ok(FearGreedLabel::ExtremeFear),
116 "Fear" => Ok(FearGreedLabel::Fear),
117 "Neutral" => Ok(FearGreedLabel::Neutral),
118 "Greed" => Ok(FearGreedLabel::Greed),
119 "Extreme Greed" => Ok(FearGreedLabel::ExtremeGreed),
120 other => Err(crate::error::FinanceError::ResponseStructureError {
121 field: "value_classification".to_string(),
122 context: format!("Unknown classification '{other}'"),
123 }),
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn test_parse_classification() {
133 assert_eq!(
134 parse_classification("Extreme Fear").unwrap(),
135 FearGreedLabel::ExtremeFear
136 );
137 assert_eq!(parse_classification("Fear").unwrap(), FearGreedLabel::Fear);
138 assert_eq!(
139 parse_classification("Neutral").unwrap(),
140 FearGreedLabel::Neutral
141 );
142 assert_eq!(
143 parse_classification("Greed").unwrap(),
144 FearGreedLabel::Greed
145 );
146 assert_eq!(
147 parse_classification("Extreme Greed").unwrap(),
148 FearGreedLabel::ExtremeGreed
149 );
150 assert!(parse_classification("unknown").is_err());
151 }
152
153 #[test]
154 fn test_fear_greed_from_response() {
155 let resp = FearAndGreedApiResponse {
156 data: vec![FearAndGreedEntry {
157 value: "25".to_string(),
158 value_classification: "Fear".to_string(),
159 timestamp: "1700000000".to_string(),
160 }],
161 };
162 let fg = FearAndGreed::from_response(resp).unwrap();
163 assert_eq!(fg.value, 25);
164 assert_eq!(fg.classification, FearGreedLabel::Fear);
165 assert_eq!(fg.timestamp, 1700000000);
166 }
167
168 #[test]
169 fn test_empty_data_returns_error() {
170 let resp = FearAndGreedApiResponse { data: vec![] };
171 assert!(FearAndGreed::from_response(resp).is_err());
172 }
173}