finance_query/adapters/yahoo/discovery/
search.rs1use crate::adapters::yahoo::client::YahooClient;
5use crate::adapters::yahoo::endpoints::api;
6use crate::constants::Region;
7use crate::error::Result;
8use tracing::info;
9
10#[derive(Debug, Clone)]
12pub struct SearchOptions {
13 pub quotes_count: u32,
15 pub news_count: u32,
17 pub enable_fuzzy_query: bool,
19 pub enable_logo_url: bool,
21 pub enable_research_reports: bool,
23 pub enable_cultural_assets: bool,
25 pub recommend_count: u32,
27 pub region: Option<Region>,
29}
30
31impl Default for SearchOptions {
32 fn default() -> Self {
33 Self {
34 quotes_count: 10,
35 news_count: 0,
36 enable_fuzzy_query: false,
37 enable_logo_url: true,
38 enable_research_reports: false,
39 enable_cultural_assets: false,
40 recommend_count: 5,
41 region: None,
42 }
43 }
44}
45
46impl SearchOptions {
47 pub fn new() -> Self {
49 Self::default()
50 }
51
52 pub fn quotes_count(mut self, count: u32) -> Self {
54 self.quotes_count = count;
55 self
56 }
57
58 pub fn news_count(mut self, count: u32) -> Self {
60 self.news_count = count;
61 self
62 }
63
64 pub fn enable_fuzzy_query(mut self, enable: bool) -> Self {
66 self.enable_fuzzy_query = enable;
67 self
68 }
69
70 pub fn enable_logo_url(mut self, enable: bool) -> Self {
72 self.enable_logo_url = enable;
73 self
74 }
75
76 pub fn enable_research_reports(mut self, enable: bool) -> Self {
78 self.enable_research_reports = enable;
79 self
80 }
81
82 pub fn enable_cultural_assets(mut self, enable: bool) -> Self {
84 self.enable_cultural_assets = enable;
85 self
86 }
87
88 pub fn recommend_count(mut self, count: u32) -> Self {
90 self.recommend_count = count;
91 self
92 }
93
94 pub fn region(mut self, region: Region) -> Self {
96 self.region = Some(region);
97 self
98 }
99}
100
101pub(crate) async fn fetch_bytes(
123 client: &YahooClient,
124 query: &str,
125 options: &SearchOptions,
126) -> Result<impl std::ops::Deref<Target = [u8]>> {
127 if query.trim().is_empty() {
128 return Err(crate::error::FinanceError::InvalidParameter {
129 param: "query".to_string(),
130 reason: "Empty search query".to_string(),
131 });
132 }
133
134 info!("Searching for: {} (options: {:?})", query, options);
135
136 let quotes_count = options.quotes_count.to_string();
137 let news_count = options.news_count.to_string();
138 let fuzzy = options.enable_fuzzy_query.to_string();
139 let logo = options.enable_logo_url.to_string();
140 let research = options.enable_research_reports.to_string();
141 let cultural = options.enable_cultural_assets.to_string();
142 let recommend = options.recommend_count.to_string();
143
144 let lang = options
146 .region
147 .as_ref()
148 .map(|c| c.lang().to_string())
149 .unwrap_or_else(|| client.config().lang.clone());
150 let region = options
151 .region
152 .as_ref()
153 .map(|c| c.region().to_string())
154 .unwrap_or_else(|| client.config().region.clone());
155
156 let params = [
157 ("q", query),
158 ("lang", &lang),
159 ("region", ®ion),
160 ("quotesCount", "es_count),
161 ("newsCount", &news_count),
162 ("enableFuzzyQuery", &fuzzy),
163 ("enableLogoUrl", &logo),
164 ("enableResearchReports", &research),
165 ("enableCulturalAssets", &cultural),
166 ("recommendedCount", &recommend),
167 ("listsCount", "0"), ("enableNavLinks", "false"), ("enableEnhancedTrivialQuery", "true"),
170 ];
171
172 let response = client.request_with_params(api::SEARCH, ¶ms).await?;
173
174 Ok(response.bytes().await?)
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use crate::adapters::yahoo::client::ClientConfig;
181
182 #[tokio::test]
183 #[ignore] async fn test_fetch_search() {
185 let client = YahooClient::new(ClientConfig::default()).await.unwrap();
186 let options = SearchOptions::new().quotes_count(5);
187 let bytes = fetch_bytes(&client, "Apple", &options).await.unwrap();
188 let results: crate::models::discovery::search::SearchResults =
189 serde_json::from_slice(&bytes).unwrap();
190 assert!(!results.quotes.0.is_empty());
191 }
192
193 #[tokio::test]
194 #[ignore] async fn test_fetch_search_with_news() {
196 let client = YahooClient::new(ClientConfig::default()).await.unwrap();
197 let options = SearchOptions::new()
198 .quotes_count(5)
199 .news_count(3)
200 .enable_research_reports(true);
201 let bytes = fetch_bytes(&client, "NVDA", &options).await.unwrap();
202 let results: crate::models::discovery::search::SearchResults =
203 serde_json::from_slice(&bytes).unwrap();
204 assert!(!results.quotes.0.is_empty());
205 }
206
207 #[test]
208 fn search_results_parse_from_slice() {
209 use crate::models::discovery::search::SearchResults;
210
211 let body = br#"{
215 "count": 2,
216 "quotes": [
217 {"symbol": "AAPL", "shortName": "Apple Inc.", "quoteType": "EQUITY"},
218 {"symbol": "AAPU"}
219 ],
220 "news": [],
221 "totalTime": 42
222 }"#;
223
224 let parsed: SearchResults = serde_json::from_slice(body).unwrap();
225 assert_eq!(parsed.result_count(), 2);
226 assert_eq!(parsed.quotes.len(), 2);
227 assert_eq!(parsed.quotes[0].symbol, "AAPL");
228 assert_eq!(parsed.quotes[1].short_name, None);
229 assert!(parsed.news.is_empty());
230 }
231
232 #[tokio::test]
233 #[ignore = "requires network access - validation tested in common::tests"]
234 async fn test_empty_query() {
235 let client = YahooClient::new(ClientConfig::default()).await.unwrap();
236 let options = SearchOptions::new();
237 let result = fetch_bytes(&client, "", &options).await;
238 assert!(result.is_err());
239 }
240}