use super::urls::api;
use crate::client::YahooClient;
use crate::constants::Region;
use crate::error::Result;
use serde::{Deserialize, Serialize};
use std::fmt;
use tracing::info;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LookupType {
#[default]
All,
Equity,
#[serde(rename = "mutualfund")]
MutualFund,
#[serde(rename = "etf")]
Etf,
Index,
Future,
Currency,
Cryptocurrency,
}
impl fmt::Display for LookupType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LookupType::All => write!(f, "all"),
LookupType::Equity => write!(f, "equity"),
LookupType::MutualFund => write!(f, "mutualfund"),
LookupType::Etf => write!(f, "etf"),
LookupType::Index => write!(f, "index"),
LookupType::Future => write!(f, "future"),
LookupType::Currency => write!(f, "currency"),
LookupType::Cryptocurrency => write!(f, "cryptocurrency"),
}
}
}
#[derive(Debug, Clone)]
pub struct LookupOptions {
pub lookup_type: LookupType,
pub count: u32,
pub include_logo: bool,
pub fetch_pricing_data: bool,
pub region: Option<Region>,
}
impl Default for LookupOptions {
fn default() -> Self {
Self {
lookup_type: LookupType::All,
count: 25,
include_logo: false,
fetch_pricing_data: true,
region: None,
}
}
}
impl LookupOptions {
pub fn new() -> Self {
Self::default()
}
pub fn lookup_type(mut self, lookup_type: LookupType) -> Self {
self.lookup_type = lookup_type;
self
}
pub fn count(mut self, count: u32) -> Self {
self.count = count;
self
}
pub fn include_logo(mut self, include: bool) -> Self {
self.include_logo = include;
self
}
pub fn fetch_pricing_data(mut self, fetch: bool) -> Self {
self.fetch_pricing_data = fetch;
self
}
pub fn region(mut self, region: Region) -> Self {
self.region = Some(region);
self
}
}
pub async fn fetch(
client: &YahooClient,
query: &str,
options: &LookupOptions,
) -> Result<serde_json::Value> {
if query.trim().is_empty() {
return Err(crate::error::FinanceError::InvalidParameter {
param: "query".to_string(),
reason: "Empty lookup query".to_string(),
});
}
info!(
"Looking up: {} (type: {}, count: {}, include_logo: {})",
query, options.lookup_type, options.count, options.include_logo
);
let count = options.count.to_string();
let lookup_type = options.lookup_type.to_string();
let fetch_pricing = options.fetch_pricing_data.to_string();
let lang = options
.region
.as_ref()
.map(|c| c.lang().to_string())
.unwrap_or_else(|| client.config().lang.clone());
let region = options
.region
.as_ref()
.map(|c| c.region().to_string())
.unwrap_or_else(|| client.config().region.clone());
let params = [
("query", query),
("type", &lookup_type),
("start", "0"),
("count", &count),
("formatted", "false"),
("fetchPricingData", &fetch_pricing),
("lang", &lang),
("region", ®ion),
];
let response = client.request_with_params(api::LOOKUP, ¶ms).await?;
let mut json: serde_json::Value = response.json().await?;
if options.include_logo {
json = enrich_with_logos(client, json).await?;
}
Ok(json)
}
async fn enrich_with_logos(
client: &YahooClient,
mut json: serde_json::Value,
) -> Result<serde_json::Value> {
let symbols: Vec<String> = json
.get("finance")
.and_then(|f| f.get("result"))
.and_then(|r| r.as_array())
.and_then(|arr| arr.first())
.and_then(|first| first.get("documents"))
.and_then(|docs| docs.as_array())
.map(|docs| {
docs.iter()
.filter_map(|doc| doc.get("symbol").and_then(|s| s.as_str()))
.map(String::from)
.collect()
})
.unwrap_or_default();
if symbols.is_empty() {
return Ok(json);
}
info!("Fetching logos for {} symbols", symbols.len());
let symbol_refs: Vec<&str> = symbols.iter().map(|s| s.as_str()).collect();
let logo_fields = ["logoUrl", "companyLogoUrl"];
let logos_json = crate::endpoints::quotes::fetch_with_fields(
client,
&symbol_refs,
Some(&logo_fields),
false,
true, )
.await?;
let logo_map: std::collections::HashMap<String, (Option<String>, Option<String>)> = logos_json
.get("quoteResponse")
.and_then(|qr| qr.get("result"))
.and_then(|r| r.as_array())
.map(|quotes| {
quotes
.iter()
.filter_map(|q| {
let symbol = q.get("symbol")?.as_str()?.to_string();
let logo_url = q.get("logoUrl").and_then(|u| u.as_str()).map(String::from);
let company_logo_url = q
.get("companyLogoUrl")
.and_then(|u| u.as_str())
.map(String::from);
Some((symbol, (logo_url, company_logo_url)))
})
.collect()
})
.unwrap_or_default();
if let Some(documents) = json
.get_mut("finance")
.and_then(|f| f.get_mut("result"))
.and_then(|r| r.as_array_mut())
.and_then(|arr| arr.first_mut())
.and_then(|first| first.get_mut("documents"))
.and_then(|docs| docs.as_array_mut())
{
for doc in documents.iter_mut() {
if let Some(symbol) = doc.get("symbol").and_then(|s| s.as_str())
&& let Some((logo_url, company_logo_url)) = logo_map.get(symbol)
{
if let Some(url) = logo_url {
doc.as_object_mut()
.map(|obj| obj.insert("logoUrl".to_string(), serde_json::json!(url)));
}
if let Some(url) = company_logo_url {
doc.as_object_mut().map(|obj| {
obj.insert("companyLogoUrl".to_string(), serde_json::json!(url))
});
}
}
}
}
Ok(json)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::ClientConfig;
#[tokio::test]
#[ignore] async fn test_fetch_lookup() {
let client = YahooClient::new(ClientConfig::default()).await.unwrap();
let options = LookupOptions::new().count(5);
let result = fetch(&client, "Apple", &options).await;
assert!(result.is_ok());
let json = result.unwrap();
assert!(json.get("finance").is_some());
}
#[tokio::test]
#[ignore] async fn test_fetch_lookup_equity() {
let client = YahooClient::new(ClientConfig::default()).await.unwrap();
let options = LookupOptions::new()
.lookup_type(LookupType::Equity)
.count(5);
let result = fetch(&client, "NVDA", &options).await;
assert!(result.is_ok());
}
#[tokio::test]
#[ignore] async fn test_fetch_lookup_with_logo() {
let client = YahooClient::new(ClientConfig::default()).await.unwrap();
let options = LookupOptions::new()
.lookup_type(LookupType::Equity)
.count(3)
.include_logo(true);
let result = fetch(&client, "Apple", &options).await;
assert!(result.is_ok());
let json = result.unwrap();
if let Some(doc) = json
.get("finance")
.and_then(|f| f.get("result"))
.and_then(|r| r.as_array())
.and_then(|arr| arr.first())
.and_then(|first| first.get("documents"))
.and_then(|docs| docs.as_array())
.and_then(|docs| docs.first())
{
println!("Document with logo: {:?}", doc);
}
}
#[tokio::test]
#[ignore = "requires network access - validation tested in common::tests"]
async fn test_empty_query() {
let client = YahooClient::new(ClientConfig::default()).await.unwrap();
let options = LookupOptions::new();
let result = fetch(&client, "", &options).await;
assert!(result.is_err());
}
}