use crate::{
error::{OpenFIGIError, OtherErrorKind, Result},
impl_filter_builder,
model::{
enums::{
Currency, ExchCode, IdType, MarketSecDesc, MicCode, OptionType, SecurityType,
SecurityType2, StateCode,
},
request::common::RequestFilters,
},
};
use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MappingRequest {
pub id_type: IdType,
pub id_value: serde_json::Value,
#[serde(flatten)]
pub filters: RequestFilters,
}
impl MappingRequest {
#[must_use]
pub fn new<T: Into<serde_json::Value>>(id_type: IdType, id_value: T) -> Self {
Self {
id_type,
id_value: id_value.into(),
filters: RequestFilters::default(),
}
}
#[must_use]
pub fn builder() -> MappingRequestBuilder {
MappingRequestBuilder::new()
}
pub fn validate(&self) -> Result<()> {
self.filters.validate()?;
if (self.id_type == IdType::BASE_TICKER || self.id_type == IdType::ID_EXCH_SYMBOL)
&& self.filters.security_type2.is_none()
{
return Err(OpenFIGIError::other_error(
OtherErrorKind::Validation,
"securityType2 is required when idType is BASE_TICKER or ID_EXCH_SYMBOL",
));
}
Ok(())
}
}
#[derive(Default)]
pub struct MappingRequestBuilder {
id_type: Option<IdType>,
id_value: Option<serde_json::Value>,
filters: RequestFilters,
}
impl MappingRequestBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn id_type(mut self, id_type: IdType) -> Self {
self.id_type = Some(id_type);
self
}
#[must_use]
pub fn id_value<T: Into<serde_json::Value>>(mut self, id_value: T) -> Self {
self.id_value = Some(id_value.into());
self
}
pub fn filters_mut(&mut self) -> &mut RequestFilters {
&mut self.filters
}
impl_filter_builder!();
pub fn build(self) -> Result<MappingRequest> {
let id_type = self.id_type.ok_or_else(|| {
OpenFIGIError::other_error(OtherErrorKind::Validation, "id_type is required")
})?;
let id_value = self.id_value.ok_or_else(|| {
OpenFIGIError::other_error(OtherErrorKind::Validation, "id_value is required")
})?;
let request = MappingRequest {
id_type,
id_value,
filters: self.filters,
};
request.validate()?;
Ok(request)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::enums::{Currency, ExchCode, IdType, MicCode, SecurityType2};
use chrono::NaiveDate;
use serde_json::json;
#[test]
fn test_mapping_request_new_minimal() {
let request = MappingRequest::new(IdType::ID_ISIN, json!("US1234567890"));
assert_eq!(request.id_type, IdType::ID_ISIN);
assert_eq!(request.id_value, json!("US1234567890"));
assert!(request.filters.exch_code.is_none());
assert!(request.filters.mic_code.is_none());
}
#[test]
fn test_mapping_request_builder_minimal() {
let request = MappingRequest::builder()
.id_type(IdType::ID_ISIN)
.id_value("US1234567890")
.build()
.unwrap();
assert_eq!(request.id_type, IdType::ID_ISIN);
assert_eq!(request.id_value, json!("US1234567890"));
}
#[test]
fn test_mapping_request_builder_with_currency() {
let request = MappingRequest::builder()
.id_type(IdType::ID_ISIN)
.id_value("US1234567890")
.currency(Currency::USD)
.build()
.unwrap();
assert_eq!(request.filters.currency, Some(Currency::USD));
}
#[test]
fn test_mapping_request_validate_exch_and_mic_code_conflict() {
let mut request = MappingRequest::new(IdType::ID_ISIN, json!("US1234567890"));
request.filters.exch_code = Some(ExchCode::A0);
request.filters.mic_code = Some(MicCode::XCME);
let result = request.validate();
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("Cannot set both exchCode and micCode"));
}
#[test]
fn test_mapping_request_validate_security_type2_required() {
let mut request = MappingRequest::new(IdType::BASE_TICKER, json!("IBM"));
request.filters.security_type2 = None;
let result = request.validate();
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("securityType2 is required"));
}
#[test]
fn test_mapping_request_validate_strike_range() {
let mut request = MappingRequest::new(IdType::ID_ISIN, json!("US1234567890"));
request.filters.strike = Some([Some(10.0), Some(5.0)]);
let result = request.validate();
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("strike: start value cannot be greater than end value"));
}
#[test]
fn test_mapping_request_validate_expiration_required_for_option() {
let mut request = MappingRequest::new(IdType::ID_ISIN, json!("US1234567890"));
request.filters.security_type2 = Some(SecurityType2::Option);
request.filters.expiration = None;
let result = request.validate();
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("expiration is required for Option or Warrant security types"));
}
#[test]
fn test_mapping_request_validate_maturity_required_for_pool() {
let mut request = MappingRequest::new(IdType::ID_ISIN, json!("US1234567890"));
request.filters.security_type2 = Some(SecurityType2::Pool);
let result = request.validate();
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("maturity is required for Pool security types"));
}
#[test]
fn test_mapping_request_validate_date_range_too_long() {
let mut request = MappingRequest::new(IdType::ID_ISIN, json!("US1234567890"));
let start = NaiveDate::from_ymd_opt(2025, 1, 1).unwrap();
let end = NaiveDate::from_ymd_opt(2026, 2, 1).unwrap();
request.filters.expiration = Some([Some(start), Some(end)]);
let result = request.validate();
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("date range cannot exceed 1 year"));
}
#[test]
fn test_serialize_deserialize_mapping_request() {
let request = MappingRequest::builder()
.id_type(IdType::ID_ISIN)
.id_value("US1234567890")
.currency(Currency::USD)
.build()
.unwrap();
let serialized = serde_json::to_string(&request).unwrap();
let deserialized: MappingRequest = serde_json::from_str(&serialized).unwrap();
assert_eq!(request, deserialized);
}
}