1use crate::{
67 DEFAULT_ENDPOINT_MAPPING,
68 client::OpenFIGIClient,
69 error::{OpenFIGIError, OtherErrorKind, Result},
70 impl_filter_builder,
71 model::{
72 enums::{
73 Currency, ExchCode, IdType, MarketSecDesc, MicCode, OptionType, SecurityType,
74 SecurityType2, StateCode,
75 },
76 request::{MappingRequest, MappingRequestBuilder, RequestFilters},
77 response::{MappingData, MappingResponses},
78 },
79};
80use chrono::NaiveDate;
81use reqwest::Method;
82
83pub struct SingleMappingRequestBuilder {
107 client: OpenFIGIClient,
108 request_builder: MappingRequestBuilder,
109}
110
111impl SingleMappingRequestBuilder {
112 #[must_use]
114 pub fn id_type(mut self, id_type: IdType) -> Self {
115 self.request_builder = self.request_builder.id_type(id_type);
116 self
117 }
118
119 #[must_use]
121 pub fn id_value<T: Into<serde_json::Value>>(mut self, id_value: T) -> Self {
122 self.request_builder = self.request_builder.id_value(id_value);
123 self
124 }
125
126 pub fn filters_mut(&mut self) -> &mut RequestFilters {
128 self.request_builder.filters_mut()
129 }
130
131 impl_filter_builder!();
133
134 pub async fn send_raw(self) -> Result<reqwest::Response> {
143 let request = self.request_builder.build()?;
144 let requests = vec![request];
145 self.client
146 .request(DEFAULT_ENDPOINT_MAPPING, Method::POST)
147 .body(&requests)
148 .send()
149 .await
150 }
151
152 #[expect(clippy::missing_panics_doc)]
159 pub async fn send(self) -> Result<MappingData> {
160 let client = self.client.clone();
161 let raw_response = self.send_raw().await?;
162
163 let mut results = client.parse_list_response(raw_response).await?;
164
165 if results.len() == 1 {
167 results.pop().unwrap()
169 } else {
170 Err(OpenFIGIError::other_error(
171 OtherErrorKind::UnexpectedApiResponse,
172 format!(
173 "Expected 1 result for single mapping, but got {}",
174 results.len()
175 ),
176 ))
177 }
178 }
179}
180
181pub struct BulkMappingRequestBuilder {
215 client: OpenFIGIClient,
216 requests: Vec<MappingRequest>,
217}
218
219impl BulkMappingRequestBuilder {
220 #[must_use]
222 pub fn add_request(mut self, request: MappingRequest) -> Self {
223 self.requests.push(request);
224 self
225 }
226
227 #[must_use]
229 pub fn add_requests(mut self, requests: Vec<MappingRequest>) -> Self {
230 self.requests.extend(requests);
231 self
232 }
233
234 pub fn add_request_with<F>(mut self, config: F) -> Result<Self>
269 where
270 F: FnOnce(MappingRequestBuilder) -> MappingRequestBuilder,
271 {
272 let builder = MappingRequest::builder();
273 let configured_builder = config(builder);
274
275 let request = configured_builder.build()?;
277
278 self.requests.push(request);
280 Ok(self)
281 }
282
283 pub async fn send_raw(self) -> Result<reqwest::Response> {
292 if self.requests.is_empty() {
293 return Err(OpenFIGIError::other_error(
294 OtherErrorKind::Validation,
295 "No requests to send",
296 ));
297 } else if !self.client.has_api_key() && self.requests.len() > 5 {
298 return Err(OpenFIGIError::other_error(
299 OtherErrorKind::Validation,
300 "Bulk mapping request cannot exceed 5 requests without an API key",
301 ));
302 } else if self.requests.len() > 100 {
303 return Err(OpenFIGIError::other_error(
304 OtherErrorKind::Validation,
305 "Bulk mapping request cannot exceed 100 requests",
306 ));
307 }
308
309 self.client
310 .request(DEFAULT_ENDPOINT_MAPPING, Method::POST)
311 .body(&self.requests)
312 .send()
313 .await
314 }
315
316 pub async fn send(self) -> Result<MappingResponses> {
323 let client = self.client.clone();
324 let raw_response = self.send_raw().await?;
325
326 let results = client.parse_list_response(raw_response).await?;
327
328 Ok(MappingResponses::new(results))
329 }
330}
331
332impl OpenFIGIClient {
333 #[must_use]
350 pub fn mapping<T: Into<serde_json::Value>>(
351 &self,
352 id_type: IdType,
353 id_value: T,
354 ) -> SingleMappingRequestBuilder {
355 SingleMappingRequestBuilder {
356 client: self.clone(),
357 request_builder: MappingRequestBuilder::new()
358 .id_type(id_type)
359 .id_value(id_value),
360 }
361 }
362
363 #[must_use]
374 pub fn bulk_mapping(&self) -> BulkMappingRequestBuilder {
375 BulkMappingRequestBuilder {
376 client: self.clone(),
377 requests: Vec::new(),
378 }
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use crate::client::OpenFIGIClient;
386 use serde_json::json;
387
388 fn create_test_client() -> OpenFIGIClient {
389 OpenFIGIClient::new()
390 }
391
392 fn create_test_client_with_api_key() -> OpenFIGIClient {
393 OpenFIGIClient::builder()
394 .api_key("test_key")
395 .build()
396 .expect("Failed to create test client")
397 }
398
399 #[test]
400 fn test_single_mapping_request_builder_creation() {
401 let client = create_test_client();
402 let builder = client.mapping(IdType::ID_ISIN, json!("US4592001014"));
403
404 assert_eq!(builder.client.base_url(), client.base_url());
406 assert_eq!(builder.client.has_api_key(), client.has_api_key());
407
408 let request_result = builder.request_builder.build();
410 assert!(
411 request_result.is_ok(),
412 "Builder should create a valid mapping request"
413 );
414
415 let request = request_result.expect("Builder should create a valid mapping request");
416 assert_eq!(request.id_type, IdType::ID_ISIN);
417 assert_eq!(request.id_value, json!("US4592001014"));
418 }
419
420 #[test]
421 fn test_single_mapping_request_builder_chaining() {
422 let client = create_test_client();
423 let builder = client
424 .mapping(IdType::ID_ISIN, json!("US4592001014"))
425 .exch_code(ExchCode::US)
426 .currency(Currency::USD)
427 .market_sec_des(MarketSecDesc::Equity)
428 .security_type(SecurityType::CommonStock)
429 .include_unlisted_equities(true);
430
431 let request = builder
433 .request_builder
434 .build()
435 .expect("Should build valid mapping request");
436
437 assert_eq!(request.id_type, IdType::ID_ISIN);
439 assert_eq!(request.id_value, json!("US4592001014"));
440 assert_eq!(request.filters.exch_code, Some(ExchCode::US));
441 assert_eq!(request.filters.currency, Some(Currency::USD));
442 assert_eq!(request.filters.market_sec_des, Some(MarketSecDesc::Equity));
443 assert_eq!(
444 request.filters.security_type,
445 Some(SecurityType::CommonStock)
446 );
447 assert_eq!(request.filters.include_unlisted_equities, Some(true));
448
449 assert_eq!(builder.client.base_url(), client.base_url());
451 }
452
453 #[test]
454 fn test_single_mapping_request_builder_option_fields() {
455 let client = create_test_client();
456 let builder = client
457 .mapping(IdType::TICKER, json!("AAPL"))
458 .option_type(OptionType::Call)
459 .strike([Some(150.0), Some(200.0)])
460 .contract_size([Some(100.0), None])
461 .coupon([None, Some(5.0)]);
462
463 let request = builder
465 .request_builder
466 .build()
467 .expect("Should build valid mapping request");
468
469 assert_eq!(request.id_type, IdType::TICKER);
470 assert_eq!(request.id_value, json!("AAPL"));
471 assert_eq!(request.filters.option_type, Some(OptionType::Call));
472 assert_eq!(request.filters.strike, Some([Some(150.0), Some(200.0)]));
473 assert_eq!(request.filters.contract_size, Some([Some(100.0), None]));
474 assert_eq!(request.filters.coupon, Some([None, Some(5.0)]));
475
476 assert_eq!(builder.client.base_url(), client.base_url());
478 }
479
480 #[test]
481 fn test_single_mapping_request_builder_date_fields() {
482 let client = create_test_client();
483 let expiration_start =
484 NaiveDate::from_ymd_opt(2024, 1, 1).expect("Should create valid expiration_start date");
485 let expiration_end =
486 NaiveDate::from_ymd_opt(2024, 12, 31).expect("Should create valid expiration_end date");
487 let maturity_start =
488 NaiveDate::from_ymd_opt(2025, 1, 1).expect("Should create valid maturity_start date");
489
490 let builder = client
491 .mapping(IdType::ID_CUSIP, json!("037833100"))
492 .expiration([Some(expiration_start), Some(expiration_end)])
493 .maturity([Some(maturity_start), None])
494 .state_code(StateCode::CA);
495
496 let request = builder
498 .request_builder
499 .build()
500 .expect("Should build valid mapping request");
501
502 assert_eq!(request.id_type, IdType::ID_CUSIP);
503 assert_eq!(request.id_value, json!("037833100"));
504 assert_eq!(
505 request.filters.expiration,
506 Some([Some(expiration_start), Some(expiration_end)])
507 );
508 assert_eq!(request.filters.maturity, Some([Some(maturity_start), None]));
509 assert_eq!(request.filters.state_code, Some(StateCode::CA));
510
511 assert_eq!(builder.client.base_url(), client.base_url());
513 }
514
515 #[test]
516 fn test_bulk_mapping_request_builder_creation() {
517 let client = create_test_client();
518 let builder = client.bulk_mapping();
519
520 assert_eq!(builder.requests.len(), 0);
522 assert_eq!(builder.client.base_url(), client.base_url());
523 }
524
525 #[test]
526 fn test_bulk_mapping_request_builder_add_request() {
527 let client = create_test_client();
528 let request = MappingRequest::new(IdType::ID_ISIN, json!("US4592001014"));
529
530 let builder = client.bulk_mapping().add_request(request);
531
532 assert_eq!(builder.requests.len(), 1);
534
535 let added_request = &builder.requests[0];
537 assert_eq!(added_request.id_type, IdType::ID_ISIN);
538 assert_eq!(added_request.id_value, json!("US4592001014"));
539
540 assert_eq!(builder.client.base_url(), client.base_url());
542 assert_eq!(builder.client.has_api_key(), client.has_api_key());
543 }
544
545 #[test]
546 fn test_bulk_mapping_request_builder_add_requests() {
547 let client = create_test_client();
548 let requests = vec![
549 MappingRequest::new(IdType::ID_ISIN, json!("US4592001014")),
550 MappingRequest::new(IdType::ID_ISIN, json!("US0378331005")),
551 MappingRequest::new(IdType::TICKER, json!("MSFT")),
552 ];
553
554 let builder = client.bulk_mapping().add_requests(requests);
555
556 assert_eq!(builder.requests.len(), 3);
558
559 assert_eq!(builder.requests[0].id_type, IdType::ID_ISIN);
561 assert_eq!(builder.requests[0].id_value, json!("US4592001014"));
562
563 assert_eq!(builder.requests[1].id_type, IdType::ID_ISIN);
564 assert_eq!(builder.requests[1].id_value, json!("US0378331005"));
565
566 assert_eq!(builder.requests[2].id_type, IdType::TICKER);
567 assert_eq!(builder.requests[2].id_value, json!("MSFT"));
568
569 assert_eq!(builder.client.base_url(), client.base_url());
571 assert_eq!(builder.client.has_api_key(), client.has_api_key());
572 }
573
574 #[test]
575 fn test_bulk_mapping_request_builder_chaining() {
576 let client = create_test_client();
577 let request1 = MappingRequest::new(IdType::ID_ISIN, json!("US4592001014"));
578 let request2 = MappingRequest::new(IdType::ID_ISIN, json!("US0378331005"));
579 let additional_requests = vec![
580 MappingRequest::new(IdType::TICKER, json!("MSFT")),
581 MappingRequest::new(IdType::TICKER, json!("GOOGL")),
582 ];
583
584 let builder = client
585 .bulk_mapping()
586 .add_request(request1)
587 .add_request(request2)
588 .add_requests(additional_requests);
589
590 assert_eq!(builder.requests.len(), 4);
592
593 assert_eq!(builder.requests[0].id_type, IdType::ID_ISIN);
595 assert_eq!(builder.requests[0].id_value, json!("US4592001014"));
596
597 assert_eq!(builder.requests[1].id_type, IdType::ID_ISIN);
598 assert_eq!(builder.requests[1].id_value, json!("US0378331005"));
599
600 assert_eq!(builder.requests[2].id_type, IdType::TICKER);
601 assert_eq!(builder.requests[2].id_value, json!("MSFT"));
602
603 assert_eq!(builder.requests[3].id_type, IdType::TICKER);
604 assert_eq!(builder.requests[3].id_value, json!("GOOGL"));
605
606 assert_eq!(builder.client.base_url(), client.base_url());
608 assert_eq!(builder.client.has_api_key(), client.has_api_key());
609 }
610
611 #[tokio::test]
612 async fn test_bulk_mapping_empty_requests_error() {
613 let client = create_test_client();
614 let builder = client.bulk_mapping();
615
616 let result = builder.send().await;
617
618 assert!(result.is_err());
619 if let Err(OpenFIGIError::OtherError { kind, .. }) = result {
620 assert_eq!(kind, OtherErrorKind::Validation);
621 } else {
622 panic!("Expected validation error for empty requests");
623 }
624 }
625
626 #[tokio::test]
627 async fn test_bulk_mapping_too_many_requests_without_api_key() {
628 let client = create_test_client(); let requests = (0..6)
630 .map(|i| MappingRequest::new(IdType::TICKER, json!(format!("TEST{}", i))))
631 .collect();
632
633 let builder = client.bulk_mapping().add_requests(requests);
634
635 let result = builder.send_raw().await;
636
637 assert!(result.is_err());
638 if let Err(OpenFIGIError::OtherError { kind, .. }) = result {
639 assert_eq!(kind, OtherErrorKind::Validation);
640 } else {
641 panic!("Expected validation error for too many requests without API key");
642 }
643 }
644
645 #[tokio::test]
646 async fn test_bulk_mapping_too_many_requests_with_api_key() {
647 let client = create_test_client_with_api_key();
648 let requests = (0..101)
649 .map(|i| MappingRequest::new(IdType::TICKER, json!(format!("TEST{}", i))))
650 .collect();
651
652 let builder = client.bulk_mapping().add_requests(requests);
653
654 let result = builder.send_raw().await;
655
656 assert!(result.is_err());
657 if let Err(OpenFIGIError::OtherError { kind, .. }) = result {
658 assert_eq!(kind, OtherErrorKind::Validation);
659 } else {
660 panic!("Expected validation error for too many requests even with API key");
661 }
662 }
663}