1use super::urls::api;
2use crate::client::YahooClient;
8use crate::constants::Region;
9use crate::error::Result;
10use serde::{Deserialize, Serialize};
11use std::fmt;
12use tracing::info;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
16#[serde(rename_all = "lowercase")]
17pub enum LookupType {
18 #[default]
20 All,
21 Equity,
23 #[serde(rename = "mutualfund")]
25 MutualFund,
26 #[serde(rename = "etf")]
28 Etf,
29 Index,
31 Future,
33 Currency,
35 Cryptocurrency,
37}
38
39impl fmt::Display for LookupType {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 LookupType::All => write!(f, "all"),
43 LookupType::Equity => write!(f, "equity"),
44 LookupType::MutualFund => write!(f, "mutualfund"),
45 LookupType::Etf => write!(f, "etf"),
46 LookupType::Index => write!(f, "index"),
47 LookupType::Future => write!(f, "future"),
48 LookupType::Currency => write!(f, "currency"),
49 LookupType::Cryptocurrency => write!(f, "cryptocurrency"),
50 }
51 }
52}
53
54#[derive(Debug, Clone)]
56pub struct LookupOptions {
57 pub lookup_type: LookupType,
59 pub count: u32,
61 pub include_logo: bool,
64 pub fetch_pricing_data: bool,
66 pub region: Option<Region>,
68}
69
70impl Default for LookupOptions {
71 fn default() -> Self {
72 Self {
73 lookup_type: LookupType::All,
74 count: 25,
75 include_logo: false,
76 fetch_pricing_data: true,
77 region: None,
78 }
79 }
80}
81
82impl LookupOptions {
83 pub fn new() -> Self {
85 Self::default()
86 }
87
88 pub fn lookup_type(mut self, lookup_type: LookupType) -> Self {
90 self.lookup_type = lookup_type;
91 self
92 }
93
94 pub fn count(mut self, count: u32) -> Self {
96 self.count = count;
97 self
98 }
99
100 pub fn include_logo(mut self, include: bool) -> Self {
103 self.include_logo = include;
104 self
105 }
106
107 pub fn fetch_pricing_data(mut self, fetch: bool) -> Self {
109 self.fetch_pricing_data = fetch;
110 self
111 }
112
113 pub fn region(mut self, region: Region) -> Self {
115 self.region = Some(region);
116 self
117 }
118}
119
120pub async fn fetch(
143 client: &YahooClient,
144 query: &str,
145 options: &LookupOptions,
146) -> Result<serde_json::Value> {
147 if query.trim().is_empty() {
148 return Err(crate::error::YahooError::InvalidParameter {
149 param: "query".to_string(),
150 reason: "Empty lookup query".to_string(),
151 });
152 }
153
154 info!(
155 "Looking up: {} (type: {}, count: {}, include_logo: {})",
156 query, options.lookup_type, options.count, options.include_logo
157 );
158
159 let count = options.count.to_string();
160 let lookup_type = options.lookup_type.to_string();
161 let fetch_pricing = options.fetch_pricing_data.to_string();
162
163 let lang = options
165 .region
166 .as_ref()
167 .map(|c| c.lang().to_string())
168 .unwrap_or_else(|| client.config().lang.clone());
169 let region = options
170 .region
171 .as_ref()
172 .map(|c| c.region().to_string())
173 .unwrap_or_else(|| client.config().region.clone());
174
175 let params = [
176 ("query", query),
177 ("type", &lookup_type),
178 ("start", "0"),
179 ("count", &count),
180 ("formatted", "false"),
181 ("fetchPricingData", &fetch_pricing),
182 ("lang", &lang),
183 ("region", ®ion),
184 ];
185
186 let response = client.request_with_params(api::LOOKUP, ¶ms).await?;
187
188 let mut json: serde_json::Value = response.json().await?;
189
190 if options.include_logo {
192 json = enrich_with_logos(client, json).await?;
193 }
194
195 Ok(json)
196}
197
198async fn enrich_with_logos(
200 client: &YahooClient,
201 mut json: serde_json::Value,
202) -> Result<serde_json::Value> {
203 let symbols: Vec<String> = json
205 .get("finance")
206 .and_then(|f| f.get("result"))
207 .and_then(|r| r.as_array())
208 .and_then(|arr| arr.first())
209 .and_then(|first| first.get("documents"))
210 .and_then(|docs| docs.as_array())
211 .map(|docs| {
212 docs.iter()
213 .filter_map(|doc| doc.get("symbol").and_then(|s| s.as_str()))
214 .map(String::from)
215 .collect()
216 })
217 .unwrap_or_default();
218
219 if symbols.is_empty() {
220 return Ok(json);
221 }
222
223 info!("Fetching logos for {} symbols", symbols.len());
224
225 let symbol_refs: Vec<&str> = symbols.iter().map(|s| s.as_str()).collect();
227 let logo_fields = ["logoUrl", "companyLogoUrl"];
228 let logos_json = crate::endpoints::quotes::fetch_with_fields(
229 client,
230 &symbol_refs,
231 Some(&logo_fields),
232 false,
233 true, )
235 .await?;
236
237 let logo_map: std::collections::HashMap<String, (Option<String>, Option<String>)> = logos_json
239 .get("quoteResponse")
240 .and_then(|qr| qr.get("result"))
241 .and_then(|r| r.as_array())
242 .map(|quotes| {
243 quotes
244 .iter()
245 .filter_map(|q| {
246 let symbol = q.get("symbol")?.as_str()?.to_string();
247 let logo_url = q.get("logoUrl").and_then(|u| u.as_str()).map(String::from);
248 let company_logo_url = q
249 .get("companyLogoUrl")
250 .and_then(|u| u.as_str())
251 .map(String::from);
252 Some((symbol, (logo_url, company_logo_url)))
253 })
254 .collect()
255 })
256 .unwrap_or_default();
257
258 if let Some(documents) = json
260 .get_mut("finance")
261 .and_then(|f| f.get_mut("result"))
262 .and_then(|r| r.as_array_mut())
263 .and_then(|arr| arr.first_mut())
264 .and_then(|first| first.get_mut("documents"))
265 .and_then(|docs| docs.as_array_mut())
266 {
267 for doc in documents.iter_mut() {
268 if let Some(symbol) = doc.get("symbol").and_then(|s| s.as_str())
269 && let Some((logo_url, company_logo_url)) = logo_map.get(symbol)
270 {
271 if let Some(url) = logo_url {
272 doc.as_object_mut()
273 .map(|obj| obj.insert("logoUrl".to_string(), serde_json::json!(url)));
274 }
275 if let Some(url) = company_logo_url {
276 doc.as_object_mut().map(|obj| {
277 obj.insert("companyLogoUrl".to_string(), serde_json::json!(url))
278 });
279 }
280 }
281 }
282 }
283
284 Ok(json)
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use crate::client::ClientConfig;
291
292 #[tokio::test]
293 #[ignore] async fn test_fetch_lookup() {
295 let client = YahooClient::new(ClientConfig::default()).await.unwrap();
296 let options = LookupOptions::new().count(5);
297 let result = fetch(&client, "Apple", &options).await;
298 assert!(result.is_ok());
299 let json = result.unwrap();
300 assert!(json.get("finance").is_some());
301 }
302
303 #[tokio::test]
304 #[ignore] async fn test_fetch_lookup_equity() {
306 let client = YahooClient::new(ClientConfig::default()).await.unwrap();
307 let options = LookupOptions::new()
308 .lookup_type(LookupType::Equity)
309 .count(5);
310 let result = fetch(&client, "NVDA", &options).await;
311 assert!(result.is_ok());
312 }
313
314 #[tokio::test]
315 #[ignore] async fn test_fetch_lookup_with_logo() {
317 let client = YahooClient::new(ClientConfig::default()).await.unwrap();
318 let options = LookupOptions::new()
319 .lookup_type(LookupType::Equity)
320 .count(3)
321 .include_logo(true);
322 let result = fetch(&client, "Apple", &options).await;
323 assert!(result.is_ok());
324 let json = result.unwrap();
326 if let Some(doc) = json
327 .get("finance")
328 .and_then(|f| f.get("result"))
329 .and_then(|r| r.as_array())
330 .and_then(|arr| arr.first())
331 .and_then(|first| first.get("documents"))
332 .and_then(|docs| docs.as_array())
333 .and_then(|docs| docs.first())
334 {
335 println!("Document with logo: {:?}", doc);
337 }
338 }
339
340 #[tokio::test]
341 #[ignore = "requires network access - validation tested in common::tests"]
342 async fn test_empty_query() {
343 let client = YahooClient::new(ClientConfig::default()).await.unwrap();
344 let options = LookupOptions::new();
345 let result = fetch(&client, "", &options).await;
346 assert!(result.is_err());
347 }
348}