use crate::{
error::{OpenFIGIError, OtherErrorKind, Result},
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::BaseTicker || self.id_type == IdType::IdExchSymbol)
&& 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
}
#[must_use]
pub fn exch_code(mut self, exch_code: ExchCode) -> Self {
self.filters.exch_code = Some(exch_code);
self
}
#[must_use]
pub fn mic_code(mut self, mic_code: MicCode) -> Self {
self.filters.mic_code = Some(mic_code);
self
}
#[must_use]
pub fn currency(mut self, currency: Currency) -> Self {
self.filters.currency = Some(currency);
self
}
#[must_use]
pub fn market_sec_des(mut self, market_sec_des: MarketSecDesc) -> Self {
self.filters.market_sec_des = Some(market_sec_des);
self
}
#[must_use]
pub fn security_type(mut self, security_type: SecurityType) -> Self {
self.filters.security_type = Some(security_type);
self
}
#[must_use]
pub fn security_type2(mut self, security_type2: SecurityType2) -> Self {
self.filters.security_type2 = Some(security_type2);
self
}
#[must_use]
pub fn include_unlisted_equities(mut self, val: bool) -> Self {
self.filters.include_unlisted_equities = Some(val);
self
}
#[must_use]
pub fn option_type(mut self, option_type: OptionType) -> Self {
self.filters.option_type = Some(option_type);
self
}
#[must_use]
pub fn strike(mut self, strike: [Option<f64>; 2]) -> Self {
self.filters.strike = Some(strike);
self
}
#[must_use]
pub fn contract_size(mut self, contract_size: [Option<f64>; 2]) -> Self {
self.filters.contract_size = Some(contract_size);
self
}
#[must_use]
pub fn coupon(mut self, coupon: [Option<f64>; 2]) -> Self {
self.filters.coupon = Some(coupon);
self
}
#[must_use]
pub fn expiration(mut self, expiration: [Option<NaiveDate>; 2]) -> Self {
self.filters.expiration = Some(expiration);
self
}
#[must_use]
pub fn maturity(mut self, maturity: [Option<NaiveDate>; 2]) -> Self {
self.filters.maturity = Some(maturity);
self
}
#[must_use]
pub fn state_code(mut self, state_code: StateCode) -> Self {
self.filters.state_code = Some(state_code);
self
}
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::IdIsin, json!("US1234567890"));
assert_eq!(request.id_type, IdType::IdIsin);
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::IdIsin)
.id_value("US1234567890")
.build()
.unwrap();
assert_eq!(request.id_type, IdType::IdIsin);
assert_eq!(request.id_value, json!("US1234567890"));
}
#[test]
fn test_mapping_request_builder_with_currency() {
let request = MappingRequest::builder()
.id_type(IdType::IdIsin)
.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::IdIsin, 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::BaseTicker, 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::IdIsin, 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::IdIsin, 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::IdIsin, 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::IdIsin, 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::IdIsin)
.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);
}
}