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 let value = entry.value.parse::<u8>().map_err(|_| {
78 crate::error::FinanceError::ResponseStructureError {
79 field: "value".to_string(),
80 context: format!("Cannot parse '{}' as u8", entry.value),
81 }
82 })?;
83
84 let classification = parse_classification(&entry.value_classification)?;
85
86 let timestamp = entry.timestamp.parse::<i64>().map_err(|_| {
87 crate::error::FinanceError::ResponseStructureError {
88 field: "timestamp".to_string(),
89 context: format!("Cannot parse '{}' as i64", entry.timestamp),
90 }
91 })?;
92
93 Ok(Self {
94 value,
95 classification,
96 timestamp,
97 })
98 }
99}
100
101pub(crate) fn parse_classification(s: &str) -> Result<FearGreedLabel, crate::error::FinanceError> {
102 match s {
103 "Extreme Fear" => Ok(FearGreedLabel::ExtremeFear),
104 "Fear" => Ok(FearGreedLabel::Fear),
105 "Neutral" => Ok(FearGreedLabel::Neutral),
106 "Greed" => Ok(FearGreedLabel::Greed),
107 "Extreme Greed" => Ok(FearGreedLabel::ExtremeGreed),
108 other => Err(crate::error::FinanceError::ResponseStructureError {
109 field: "value_classification".to_string(),
110 context: format!("Unknown classification '{other}'"),
111 }),
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn test_parse_classification() {
121 assert_eq!(
122 parse_classification("Extreme Fear").unwrap(),
123 FearGreedLabel::ExtremeFear
124 );
125 assert_eq!(parse_classification("Fear").unwrap(), FearGreedLabel::Fear);
126 assert_eq!(
127 parse_classification("Neutral").unwrap(),
128 FearGreedLabel::Neutral
129 );
130 assert_eq!(
131 parse_classification("Greed").unwrap(),
132 FearGreedLabel::Greed
133 );
134 assert_eq!(
135 parse_classification("Extreme Greed").unwrap(),
136 FearGreedLabel::ExtremeGreed
137 );
138 assert!(parse_classification("unknown").is_err());
139 }
140
141 #[test]
142 fn test_fear_greed_from_response() {
143 let resp = FearAndGreedApiResponse {
144 data: vec![FearAndGreedEntry {
145 value: "25".to_string(),
146 value_classification: "Fear".to_string(),
147 timestamp: "1700000000".to_string(),
148 }],
149 };
150 let fg = FearAndGreed::from_response(resp).unwrap();
151 assert_eq!(fg.value, 25);
152 assert_eq!(fg.classification, FearGreedLabel::Fear);
153 assert_eq!(fg.timestamp, 1700000000);
154 }
155
156 #[test]
157 fn test_empty_data_returns_error() {
158 let resp = FearAndGreedApiResponse { data: vec![] };
159 assert!(FearAndGreed::from_response(resp).is_err());
160 }
161}