finance_query/adapters/fmp/
fund_holdings.rs1use serde::{Deserialize, Serialize};
4
5use crate::adapters::common::encode_path_segment;
6use crate::error::Result;
7
8use super::build_client;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
16#[non_exhaustive]
17pub struct EtfSectorWeighting {
18 pub sector: Option<String>,
20 #[serde(rename = "weightPercentage")]
22 pub weight_percentage: Option<String>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27#[non_exhaustive]
28pub struct EtfCountryWeighting {
29 pub country: Option<String>,
31 #[serde(rename = "weightPercentage")]
33 pub weight_percentage: Option<String>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38#[non_exhaustive]
39pub struct EtfHolding {
40 pub asset: Option<String>,
42 #[serde(rename = "sharesNumber")]
44 pub shares_number: Option<f64>,
45 #[serde(rename = "weightPercentage")]
47 pub weight_percentage: Option<f64>,
48 #[serde(rename = "marketValue")]
50 pub market_value: Option<f64>,
51 pub updated: Option<String>,
53}
54
55pub async fn etf_sector_weightings(symbol: &str) -> Result<Vec<EtfSectorWeighting>> {
61 let client = build_client()?;
62 let path = format!(
63 "/api/v3/etf-sector-weightings/{}",
64 encode_path_segment(symbol)
65 );
66 client.get(&path, &[]).await
67}
68
69pub async fn etf_country_weightings(symbol: &str) -> Result<Vec<EtfCountryWeighting>> {
71 let client = build_client()?;
72 let path = format!(
73 "/api/v3/etf-country-weightings/{}",
74 encode_path_segment(symbol)
75 );
76 client.get(&path, &[]).await
77}
78
79pub async fn etf_holdings(symbol: &str) -> Result<Vec<EtfHolding>> {
81 let client = build_client()?;
82 let path = format!("/api/v3/etf-holder/{}", encode_path_segment(symbol));
83 client.get(&path, &[]).await
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[tokio::test]
91 async fn test_etf_sector_weightings_mock() {
92 let mut server = mockito::Server::new_async().await;
93 let _mock = server
94 .mock("GET", "/api/v3/etf-sector-weightings/SPY")
95 .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
96 "apikey".into(),
97 "test-key".into(),
98 )]))
99 .with_status(200)
100 .with_body(
101 serde_json::json!([
102 {
103 "sector": "Technology",
104 "weightPercentage": "29.50%"
105 },
106 {
107 "sector": "Healthcare",
108 "weightPercentage": "13.20%"
109 }
110 ])
111 .to_string(),
112 )
113 .create_async()
114 .await;
115
116 let client = super::super::build_test_client(&server.url()).unwrap();
117 let resp: Vec<EtfSectorWeighting> = client
118 .get("/api/v3/etf-sector-weightings/SPY", &[])
119 .await
120 .unwrap();
121 assert_eq!(resp.len(), 2);
122 assert_eq!(resp[0].sector.as_deref(), Some("Technology"));
123 assert_eq!(resp[0].weight_percentage.as_deref(), Some("29.50%"));
124 }
125
126 #[tokio::test]
127 async fn test_etf_country_weightings_mock() {
128 let mut server = mockito::Server::new_async().await;
129 let _mock = server
130 .mock("GET", "/api/v3/etf-country-weightings/VEU")
131 .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
132 "apikey".into(),
133 "test-key".into(),
134 )]))
135 .with_status(200)
136 .with_body(
137 serde_json::json!([
138 {
139 "country": "Japan",
140 "weightPercentage": "15.80%"
141 }
142 ])
143 .to_string(),
144 )
145 .create_async()
146 .await;
147
148 let client = super::super::build_test_client(&server.url()).unwrap();
149 let resp: Vec<EtfCountryWeighting> = client
150 .get("/api/v3/etf-country-weightings/VEU", &[])
151 .await
152 .unwrap();
153 assert_eq!(resp.len(), 1);
154 assert_eq!(resp[0].country.as_deref(), Some("Japan"));
155 }
156}