use crate::{
DEFAULT_ENDPOINT_MAPPING,
client::OpenFIGIClient,
error::{OpenFIGIError, OtherErrorKind, Result},
impl_filter_builder,
model::{
enums::{
Currency, ExchCode, IdType, MarketSecDesc, MicCode, OptionType, SecurityType,
SecurityType2, StateCode,
},
request::{MappingRequest, MappingRequestBuilder, RequestFilters},
response::{MappingData, MappingResponses},
},
};
use chrono::NaiveDate;
use reqwest::Method;
pub struct SingleMappingRequestBuilder {
client: OpenFIGIClient,
request_builder: MappingRequestBuilder,
}
impl SingleMappingRequestBuilder {
#[must_use]
pub fn id_type(mut self, id_type: IdType) -> Self {
self.request_builder = self.request_builder.id_type(id_type);
self
}
#[must_use]
pub fn id_value<T: Into<serde_json::Value>>(mut self, id_value: T) -> Self {
self.request_builder = self.request_builder.id_value(id_value);
self
}
pub fn filters_mut(&mut self) -> &mut RequestFilters {
self.request_builder.filters_mut()
}
impl_filter_builder!();
pub async fn send_raw(self) -> Result<reqwest::Response> {
let request = self.request_builder.build()?;
let requests = vec![request];
self.client
.request(DEFAULT_ENDPOINT_MAPPING, Method::POST)
.body(&requests)
.send()
.await
}
#[expect(clippy::missing_panics_doc)]
pub async fn send(self) -> Result<MappingData> {
let client = self.client.clone();
let raw_response = self.send_raw().await?;
let mut results = client.parse_list_response(raw_response).await?;
if results.len() == 1 {
results.pop().unwrap()
} else {
Err(OpenFIGIError::other_error(
OtherErrorKind::UnexpectedApiResponse,
format!(
"Expected 1 result for single mapping, but got {}",
results.len()
),
))
}
}
}
pub struct BulkMappingRequestBuilder {
client: OpenFIGIClient,
requests: Vec<MappingRequest>,
}
impl BulkMappingRequestBuilder {
#[must_use]
pub fn add_request(mut self, request: MappingRequest) -> Self {
self.requests.push(request);
self
}
#[must_use]
pub fn add_requests(mut self, requests: Vec<MappingRequest>) -> Self {
self.requests.extend(requests);
self
}
pub fn add_request_with<F>(mut self, config: F) -> Result<Self>
where
F: FnOnce(MappingRequestBuilder) -> MappingRequestBuilder,
{
let builder = MappingRequest::builder();
let configured_builder = config(builder);
let request = configured_builder.build()?;
self.requests.push(request);
Ok(self)
}
pub async fn send_raw(self) -> Result<reqwest::Response> {
if self.requests.is_empty() {
return Err(OpenFIGIError::other_error(
OtherErrorKind::Validation,
"No requests to send",
));
} else if !self.client.has_api_key() && self.requests.len() > 5 {
return Err(OpenFIGIError::other_error(
OtherErrorKind::Validation,
"Bulk mapping request cannot exceed 5 requests without an API key",
));
} else if self.requests.len() > 100 {
return Err(OpenFIGIError::other_error(
OtherErrorKind::Validation,
"Bulk mapping request cannot exceed 100 requests",
));
}
self.client
.request(DEFAULT_ENDPOINT_MAPPING, Method::POST)
.body(&self.requests)
.send()
.await
}
pub async fn send(self) -> Result<MappingResponses> {
let client = self.client.clone();
let raw_response = self.send_raw().await?;
let results = client.parse_list_response(raw_response).await?;
Ok(MappingResponses::new(results))
}
}
impl OpenFIGIClient {
#[must_use]
pub fn mapping<T: Into<serde_json::Value>>(
&self,
id_type: IdType,
id_value: T,
) -> SingleMappingRequestBuilder {
SingleMappingRequestBuilder {
client: self.clone(),
request_builder: MappingRequestBuilder::new()
.id_type(id_type)
.id_value(id_value),
}
}
#[must_use]
pub fn bulk_mapping(&self) -> BulkMappingRequestBuilder {
BulkMappingRequestBuilder {
client: self.clone(),
requests: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::OpenFIGIClient;
use serde_json::json;
fn create_test_client() -> OpenFIGIClient {
OpenFIGIClient::new()
}
fn create_test_client_with_api_key() -> OpenFIGIClient {
OpenFIGIClient::builder()
.api_key("test_key")
.build()
.expect("Failed to create test client")
}
#[test]
fn test_single_mapping_request_builder_creation() {
let client = create_test_client();
let builder = client.mapping(IdType::ID_ISIN, json!("US4592001014"));
assert_eq!(builder.client.base_url(), client.base_url());
assert_eq!(builder.client.has_api_key(), client.has_api_key());
let request_result = builder.request_builder.build();
assert!(
request_result.is_ok(),
"Builder should create a valid mapping request"
);
let request = request_result.unwrap();
assert_eq!(request.id_type, IdType::ID_ISIN);
assert_eq!(request.id_value, json!("US4592001014"));
}
#[test]
fn test_single_mapping_request_builder_chaining() {
let client = create_test_client();
let builder = client
.mapping(IdType::ID_ISIN, json!("US4592001014"))
.exch_code(ExchCode::US)
.currency(Currency::USD)
.market_sec_des(MarketSecDesc::Equity)
.security_type(SecurityType::CommonStock)
.include_unlisted_equities(true);
let request = builder
.request_builder
.build()
.expect("Should build valid mapping request");
assert_eq!(request.id_type, IdType::ID_ISIN);
assert_eq!(request.id_value, json!("US4592001014"));
assert_eq!(request.filters.exch_code, Some(ExchCode::US));
assert_eq!(request.filters.currency, Some(Currency::USD));
assert_eq!(request.filters.market_sec_des, Some(MarketSecDesc::Equity));
assert_eq!(
request.filters.security_type,
Some(SecurityType::CommonStock)
);
assert_eq!(request.filters.include_unlisted_equities, Some(true));
assert_eq!(builder.client.base_url(), client.base_url());
}
#[test]
fn test_single_mapping_request_builder_option_fields() {
let client = create_test_client();
let builder = client
.mapping(IdType::TICKER, json!("AAPL"))
.option_type(OptionType::Call)
.strike([Some(150.0), Some(200.0)])
.contract_size([Some(100.0), None])
.coupon([None, Some(5.0)]);
let request = builder
.request_builder
.build()
.expect("Should build valid mapping request");
assert_eq!(request.id_type, IdType::TICKER);
assert_eq!(request.id_value, json!("AAPL"));
assert_eq!(request.filters.option_type, Some(OptionType::Call));
assert_eq!(request.filters.strike, Some([Some(150.0), Some(200.0)]));
assert_eq!(request.filters.contract_size, Some([Some(100.0), None]));
assert_eq!(request.filters.coupon, Some([None, Some(5.0)]));
assert_eq!(builder.client.base_url(), client.base_url());
}
#[test]
fn test_single_mapping_request_builder_date_fields() {
let client = create_test_client();
let expiration_start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
let expiration_end = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
let maturity_start = NaiveDate::from_ymd_opt(2025, 1, 1).unwrap();
let builder = client
.mapping(IdType::ID_CUSIP, json!("037833100"))
.expiration([Some(expiration_start), Some(expiration_end)])
.maturity([Some(maturity_start), None])
.state_code(StateCode::CA);
let request = builder
.request_builder
.build()
.expect("Should build valid mapping request");
assert_eq!(request.id_type, IdType::ID_CUSIP);
assert_eq!(request.id_value, json!("037833100"));
assert_eq!(
request.filters.expiration,
Some([Some(expiration_start), Some(expiration_end)])
);
assert_eq!(request.filters.maturity, Some([Some(maturity_start), None]));
assert_eq!(request.filters.state_code, Some(StateCode::CA));
assert_eq!(builder.client.base_url(), client.base_url());
}
#[test]
fn test_bulk_mapping_request_builder_creation() {
let client = create_test_client();
let builder = client.bulk_mapping();
assert_eq!(builder.requests.len(), 0);
assert_eq!(builder.client.base_url(), client.base_url());
}
#[test]
fn test_bulk_mapping_request_builder_add_request() {
let client = create_test_client();
let request = MappingRequest::new(IdType::ID_ISIN, json!("US4592001014"));
let builder = client.bulk_mapping().add_request(request);
assert_eq!(builder.requests.len(), 1);
let added_request = &builder.requests[0];
assert_eq!(added_request.id_type, IdType::ID_ISIN);
assert_eq!(added_request.id_value, json!("US4592001014"));
assert_eq!(builder.client.base_url(), client.base_url());
assert_eq!(builder.client.has_api_key(), client.has_api_key());
}
#[test]
fn test_bulk_mapping_request_builder_add_requests() {
let client = create_test_client();
let requests = vec![
MappingRequest::new(IdType::ID_ISIN, json!("US4592001014")),
MappingRequest::new(IdType::ID_ISIN, json!("US0378331005")),
MappingRequest::new(IdType::TICKER, json!("MSFT")),
];
let builder = client.bulk_mapping().add_requests(requests);
assert_eq!(builder.requests.len(), 3);
assert_eq!(builder.requests[0].id_type, IdType::ID_ISIN);
assert_eq!(builder.requests[0].id_value, json!("US4592001014"));
assert_eq!(builder.requests[1].id_type, IdType::ID_ISIN);
assert_eq!(builder.requests[1].id_value, json!("US0378331005"));
assert_eq!(builder.requests[2].id_type, IdType::TICKER);
assert_eq!(builder.requests[2].id_value, json!("MSFT"));
assert_eq!(builder.client.base_url(), client.base_url());
assert_eq!(builder.client.has_api_key(), client.has_api_key());
}
#[test]
fn test_bulk_mapping_request_builder_chaining() {
let client = create_test_client();
let request1 = MappingRequest::new(IdType::ID_ISIN, json!("US4592001014"));
let request2 = MappingRequest::new(IdType::ID_ISIN, json!("US0378331005"));
let additional_requests = vec![
MappingRequest::new(IdType::TICKER, json!("MSFT")),
MappingRequest::new(IdType::TICKER, json!("GOOGL")),
];
let builder = client
.bulk_mapping()
.add_request(request1)
.add_request(request2)
.add_requests(additional_requests);
assert_eq!(builder.requests.len(), 4);
assert_eq!(builder.requests[0].id_type, IdType::ID_ISIN);
assert_eq!(builder.requests[0].id_value, json!("US4592001014"));
assert_eq!(builder.requests[1].id_type, IdType::ID_ISIN);
assert_eq!(builder.requests[1].id_value, json!("US0378331005"));
assert_eq!(builder.requests[2].id_type, IdType::TICKER);
assert_eq!(builder.requests[2].id_value, json!("MSFT"));
assert_eq!(builder.requests[3].id_type, IdType::TICKER);
assert_eq!(builder.requests[3].id_value, json!("GOOGL"));
assert_eq!(builder.client.base_url(), client.base_url());
assert_eq!(builder.client.has_api_key(), client.has_api_key());
}
#[tokio::test]
async fn test_bulk_mapping_empty_requests_error() {
let client = create_test_client();
let builder = client.bulk_mapping();
let result = builder.send().await;
assert!(result.is_err());
if let Err(OpenFIGIError::OtherError { kind, .. }) = result {
assert_eq!(kind, OtherErrorKind::Validation);
} else {
panic!("Expected validation error for empty requests");
}
}
#[tokio::test]
async fn test_bulk_mapping_too_many_requests_without_api_key() {
let client = create_test_client(); let requests = (0..6)
.map(|i| MappingRequest::new(IdType::TICKER, json!(format!("TEST{}", i))))
.collect();
let builder = client.bulk_mapping().add_requests(requests);
let result = builder.send_raw().await;
assert!(result.is_err());
if let Err(OpenFIGIError::OtherError { kind, .. }) = result {
assert_eq!(kind, OtherErrorKind::Validation);
} else {
panic!("Expected validation error for too many requests without API key");
}
}
#[tokio::test]
async fn test_bulk_mapping_too_many_requests_with_api_key() {
let client = create_test_client_with_api_key();
let requests = (0..101)
.map(|i| MappingRequest::new(IdType::TICKER, json!(format!("TEST{}", i))))
.collect();
let builder = client.bulk_mapping().add_requests(requests);
let result = builder.send_raw().await;
assert!(result.is_err());
if let Err(OpenFIGIError::OtherError { kind, .. }) = result {
assert_eq!(kind, OtherErrorKind::Validation);
} else {
panic!("Expected validation error for too many requests even with API key");
}
}
}