openfigi_rs/model/request/mapping_request.rs
1//! # Mapping Request Types
2//!
3//! Request structures for the OpenFIGI `/mapping` endpoint (see [here](https://www.openfigi.com/api/documentation#v3-post-mapping) for more details).
4//! Provides types for building identifier mapping requests with fluent builder patterns.
5//!
6//! ## Examples
7//!
8//! ### Basic mapping request
9//!
10//! ```rust
11//! use openfigi_rs::model::request::MappingRequest;
12//! use openfigi_rs::model::enums::IdType;
13//!
14//! let request = MappingRequest::new(IdType::ID_ISIN, "US4592001014");
15//! ```
16//!
17//! ### Mapping request with additional filters
18//!
19//! ```rust
20//! use openfigi_rs::model::request::MappingRequest;
21//! use openfigi_rs::model::enums::{IdType, Currency, ExchCode};
22//!
23//! let request = MappingRequest::builder()
24//! .id_type(IdType::ID_ISIN)
25//! .id_value("US4592001014")
26//! .currency(Currency::USD)
27//! .exch_code(ExchCode::US)
28//! .build()
29//! .unwrap();
30//! ```
31//!
32//! Note: This module is not intended for direct use by consumers of the OpenFIGI API.
33
34use crate::{
35 error::{OpenFIGIError, OtherErrorKind, Result},
36 impl_filter_builder,
37 model::{
38 enums::{
39 Currency, ExchCode, IdType, MarketSecDesc, MicCode, OptionType, SecurityType,
40 SecurityType2, StateCode,
41 },
42 request::common::RequestFilters,
43 },
44};
45use chrono::NaiveDate;
46use serde::{Deserialize, Serialize};
47
48/// Request structure for the OpenFIGI `/mapping` endpoint.
49///
50/// Represents a mapping request that converts third-party identifiers to FIGIs.
51/// Requires an identifier type and value, with optional filtering criteria.
52///
53/// # Required Fields
54///
55/// - `id_type`: The type of third-party identifier being mapped
56/// - `id_value`: The value of the identifier (string or number)
57///
58/// # Optional Fields
59///
60/// - `filters`: Additional filtering criteria (flattened into the request JSON)
61///
62/// # Validation Rules
63///
64/// - `security_type2` is required when `id_type` is `BASE_TICKER` or `ID_EXCH_SYMBOL`
65/// - All filter validation rules apply (see common filters documentation `RequestFilters::validate()`)
66///
67/// # Examples
68///
69/// ```rust
70/// use openfigi_rs::model::request::MappingRequest;
71/// use openfigi_rs::model::enums::{IdType, Currency};
72///
73/// // Simple identifier mapping
74/// let request = MappingRequest::new(IdType::ID_ISIN, "US4592001014");
75///
76/// // Mapping with additional filters
77/// let request = MappingRequest::builder()
78/// .id_type(IdType::TICKER)
79/// .id_value("AAPL")
80/// .currency(Currency::USD)
81/// .build()
82/// .unwrap();
83/// ```
84#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
85#[serde(rename_all = "camelCase")]
86pub struct MappingRequest {
87 /// Type of third party identifier.
88 pub id_type: IdType,
89 /// Value of the third party identifier.
90 pub id_value: serde_json::Value, // String or Number
91
92 /// Additional filtering criteria applied to the mapping request.
93 ///
94 /// These filters are flattened into the JSON structure and provide
95 /// optional constraints to refine the mapping results see `RequestFilters`.
96 #[serde(flatten)]
97 pub filters: RequestFilters,
98}
99
100impl MappingRequest {
101 /// Creates a new `MappingRequest` with required identifier information.
102 ///
103 /// All filter fields are initialized to their default values (typically `None`).
104 /// Use [`MappingRequest::builder()`] for a more convenient fluent API.
105 ///
106 /// # Arguments
107 ///
108 /// * `id_type` - The type of third-party identifier
109 /// * `id_value` - The identifier value (can be string or number)
110 ///
111 /// # Examples
112 ///
113 /// ```rust
114 /// use openfigi_rs::model::request::MappingRequest;
115 /// use openfigi_rs::model::enums::IdType;
116 ///
117 /// let request = MappingRequest::new(IdType::ID_ISIN, "US4592001014");
118 /// assert_eq!(request.id_type, IdType::ID_ISIN);
119 /// ```
120 #[must_use]
121 pub fn new<T: Into<serde_json::Value>>(id_type: IdType, id_value: T) -> Self {
122 Self {
123 id_type,
124 id_value: id_value.into(),
125 filters: RequestFilters::default(),
126 }
127 }
128
129 /// Creates a new `MappingRequestBuilder` for fluent request construction.
130 ///
131 /// Provides a convenient way to build mapping requests with method chaining.
132 ///
133 /// # Examples
134 ///
135 /// ```rust
136 /// use openfigi_rs::model::request::MappingRequest;
137 /// use openfigi_rs::model::enums::{IdType, Currency};
138 ///
139 /// let request = MappingRequest::builder()
140 /// .id_type(IdType::TICKER)
141 /// .id_value("AAPL")
142 /// .currency(Currency::USD)
143 /// .build()
144 /// .unwrap();
145 /// ```
146 #[must_use]
147 pub fn builder() -> MappingRequestBuilder {
148 MappingRequestBuilder::new()
149 }
150
151 /// Validates the mapping request.
152 ///
153 /// Ensures that:
154 /// - Required fields (`id_type`, `id_value`) are present
155 /// - `security_type2` is provided when required by certain identifier types
156 /// - All filter validation rules are satisfied
157 /// - No mutually exclusive parameters are set
158 ///
159 /// # Errors
160 ///
161 /// Returns [`OpenFIGIError`] with [`OtherErrorKind::Validation`] if validation fails.
162 ///
163 /// # Examples
164 ///
165 /// ```rust
166 /// use openfigi_rs::model::request::MappingRequest;
167 /// use openfigi_rs::model::enums::{IdType, SecurityType2};
168 ///
169 /// // This will fail validation - BASE_TICKER requires security_type2
170 /// let mut request = MappingRequest::new(IdType::BASE_TICKER, "AAPL");
171 /// assert!(request.validate().is_err());
172 ///
173 /// // Add required security_type2
174 /// request.filters.security_type2 = Some(SecurityType2::CommonStock);
175 /// assert!(request.validate().is_ok());
176 /// ```
177 pub fn validate(&self) -> Result<()> {
178 // Validate the `RequestFilters` fields
179 self.filters.validate()?;
180
181 // securityType2 is required when idType is BASE_TICKER or ID_EXCH_SYMBOL
182 if (self.id_type == IdType::BASE_TICKER || self.id_type == IdType::ID_EXCH_SYMBOL)
183 && self.filters.security_type2.is_none()
184 {
185 return Err(OpenFIGIError::other_error(
186 OtherErrorKind::Validation,
187 "securityType2 is required when idType is BASE_TICKER or ID_EXCH_SYMBOL",
188 ));
189 }
190 Ok(())
191 }
192}
193
194/// Builder for constructing [`MappingRequest`] instances.
195///
196/// Provides a fluent API for setting identifier information and filter parameters.
197/// All methods return `self` to enable method chaining.
198///
199/// # Required Fields
200///
201/// - `id_type`: Must be set via [`id_type()`](Self::id_type)
202/// - `id_value`: Must be set via [`id_value()`](Self::id_value)
203///
204/// # Examples
205///
206/// ```rust
207/// use openfigi_rs::model::request::MappingRequestBuilder;
208/// use openfigi_rs::model::enums::{IdType, Currency, ExchCode};
209///
210/// let request = MappingRequestBuilder::new()
211/// .id_type(IdType::ID_ISIN)
212/// .id_value("US4592001014")
213/// .currency(Currency::USD)
214/// .exch_code(ExchCode::US)
215/// .build()
216/// .unwrap();
217/// ```
218#[derive(Default)]
219pub struct MappingRequestBuilder {
220 id_type: Option<IdType>,
221 id_value: Option<serde_json::Value>,
222 filters: RequestFilters,
223}
224
225impl MappingRequestBuilder {
226 /// Creates a new `MappingRequestBuilder` with all fields unset.
227 ///
228 /// # Examples
229 ///
230 /// ```rust
231 /// use openfigi_rs::model::request::MappingRequestBuilder;
232 ///
233 /// let builder = MappingRequestBuilder::new();
234 /// ```
235 #[must_use]
236 pub fn new() -> Self {
237 Self::default()
238 }
239
240 /// Sets the identifier type for the mapping request.
241 ///
242 /// This field is required and specifies what type of third-party identifier
243 /// is being mapped to a FIGI.
244 ///
245 /// # Examples
246 ///
247 /// ```rust
248 /// use openfigi_rs::model::request::MappingRequestBuilder;
249 /// use openfigi_rs::model::enums::IdType;
250 ///
251 /// let builder = MappingRequestBuilder::new().id_type(IdType::ID_ISIN);
252 /// ```
253 #[must_use]
254 pub fn id_type(mut self, id_type: IdType) -> Self {
255 self.id_type = Some(id_type);
256 self
257 }
258
259 /// Sets the identifier value for the mapping request.
260 ///
261 /// This field is required and contains the actual identifier value to be mapped.
262 /// Can accept strings, numbers, or any value that converts to `serde_json::Value`.
263 ///
264 /// # Examples
265 ///
266 /// ```rust
267 /// use openfigi_rs::model::request::MappingRequestBuilder;
268 /// use serde_json::json;
269 ///
270 /// let builder = MappingRequestBuilder::new()
271 /// .id_value("US4592001014") // String
272 /// .id_value(json!("AAPL")) // JSON string
273 /// .id_value(12345); // Number
274 /// ```
275 #[must_use]
276 pub fn id_value<T: Into<serde_json::Value>>(mut self, id_value: T) -> Self {
277 self.id_value = Some(id_value.into());
278 self
279 }
280
281 /// Mutable access to the request filters.
282 pub fn filters_mut(&mut self) -> &mut RequestFilters {
283 &mut self.filters
284 }
285
286 // Bring in common builder methods for filtering logic
287 impl_filter_builder!();
288
289 /// Builds and validates the `MappingRequest`.
290 ///
291 /// Constructs the final request object and performs validation to ensure
292 /// all requirements are met.
293 ///
294 /// # Errors
295 ///
296 /// Returns [`OpenFIGIError`] if validation fails, such as:
297 /// - Missing required fields (`id_type` or `id_value`)
298 /// - Missing `security_type2` when required by identifier type
299 /// - Mutually exclusive parameters set
300 /// - Invalid parameter ranges
301 ///
302 /// # Examples
303 ///
304 /// ```rust
305 /// use openfigi_rs::model::request::MappingRequestBuilder;
306 /// use openfigi_rs::model::enums::IdType;
307 ///
308 /// let request = MappingRequestBuilder::new()
309 /// .id_type(IdType::ID_ISIN)
310 /// .id_value("US4592001014")
311 /// .build()
312 /// .unwrap();
313 /// ```
314 pub fn build(self) -> Result<MappingRequest> {
315 let id_type = self.id_type.ok_or_else(|| {
316 OpenFIGIError::other_error(OtherErrorKind::Validation, "id_type is required")
317 })?;
318 let id_value = self.id_value.ok_or_else(|| {
319 OpenFIGIError::other_error(OtherErrorKind::Validation, "id_value is required")
320 })?;
321 let request = MappingRequest {
322 id_type,
323 id_value,
324 filters: self.filters,
325 };
326 request.validate()?;
327 Ok(request)
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334 use crate::model::enums::{Currency, ExchCode, IdType, MicCode, SecurityType2};
335 use chrono::NaiveDate;
336 use serde_json::json;
337
338 #[test]
339 fn test_mapping_request_new_minimal() {
340 let request = MappingRequest::new(IdType::ID_ISIN, json!("US1234567890"));
341 assert_eq!(request.id_type, IdType::ID_ISIN);
342 assert_eq!(request.id_value, json!("US1234567890"));
343 assert!(request.filters.exch_code.is_none());
344 assert!(request.filters.mic_code.is_none());
345 }
346
347 #[test]
348 fn test_mapping_request_builder_minimal() {
349 let request = MappingRequest::builder()
350 .id_type(IdType::ID_ISIN)
351 .id_value("US1234567890")
352 .build()
353 .expect("Failed to build a valid mapping request");
354 assert_eq!(request.id_type, IdType::ID_ISIN);
355 assert_eq!(request.id_value, json!("US1234567890"));
356 }
357
358 #[test]
359 fn test_mapping_request_builder_with_currency() {
360 let request = MappingRequest::builder()
361 .id_type(IdType::ID_ISIN)
362 .id_value("US1234567890")
363 .currency(Currency::USD)
364 .build()
365 .expect("Failed to build a valid mapping request");
366 assert_eq!(request.filters.currency, Some(Currency::USD));
367 }
368
369 #[test]
370 fn test_mapping_request_validate_exch_and_mic_code_conflict() {
371 let mut request = MappingRequest::new(IdType::ID_ISIN, json!("US1234567890"));
372 request.filters.exch_code = Some(ExchCode::A0);
373 request.filters.mic_code = Some(MicCode::XCME);
374 let result = request.validate();
375 assert!(result.is_err());
376 let msg = format!("{}", result.unwrap_err());
377 assert!(msg.contains("Cannot set both exchCode and micCode"));
378 }
379
380 #[test]
381 fn test_mapping_request_validate_security_type2_required() {
382 let mut request = MappingRequest::new(IdType::BASE_TICKER, json!("IBM"));
383 request.filters.security_type2 = None;
384 let result = request.validate();
385 assert!(result.is_err());
386 let msg = format!("{}", result.unwrap_err());
387 assert!(msg.contains("securityType2 is required"));
388 }
389
390 #[test]
391 fn test_mapping_request_validate_strike_range() {
392 let mut request = MappingRequest::new(IdType::ID_ISIN, json!("US1234567890"));
393 request.filters.strike = Some([Some(10.0), Some(5.0)]);
394 let result = request.validate();
395 assert!(result.is_err());
396 let msg = format!("{}", result.unwrap_err());
397 assert!(msg.contains("strike: start value cannot be greater than end value"));
398 }
399
400 #[test]
401 fn test_mapping_request_validate_expiration_required_for_option() {
402 let mut request = MappingRequest::new(IdType::ID_ISIN, json!("US1234567890"));
403 request.filters.security_type2 = Some(SecurityType2::Option);
404 request.filters.expiration = None;
405 let result = request.validate();
406 assert!(result.is_err());
407 let msg = format!("{}", result.unwrap_err());
408 assert!(msg.contains("expiration is required for Option or Warrant security types"));
409 }
410
411 #[test]
412 fn test_mapping_request_validate_maturity_required_for_pool() {
413 let mut request = MappingRequest::new(IdType::ID_ISIN, json!("US1234567890"));
414 request.filters.security_type2 = Some(SecurityType2::Pool);
415 let result = request.validate();
416 assert!(result.is_err());
417 let msg = format!("{}", result.unwrap_err());
418 assert!(msg.contains("maturity is required for Pool security types"));
419 }
420
421 #[test]
422 fn test_mapping_request_validate_date_range_too_long() {
423 let mut request = MappingRequest::new(IdType::ID_ISIN, json!("US1234567890"));
424 let start = NaiveDate::from_ymd_opt(2025, 1, 1).expect("Should create a valid date");
425 let end = NaiveDate::from_ymd_opt(2026, 2, 1).expect("Should create a valid date");
426 request.filters.expiration = Some([Some(start), Some(end)]);
427 let result = request.validate();
428 assert!(result.is_err());
429 let msg = format!("{}", result.unwrap_err());
430 assert!(msg.contains("date range cannot exceed 1 year"));
431 }
432
433 #[test]
434 fn test_serialize_deserialize_mapping_request() {
435 let request = MappingRequest::builder()
436 .id_type(IdType::ID_ISIN)
437 .id_value("US1234567890")
438 .currency(Currency::USD)
439 .build()
440 .expect("Failed to build a valid mapping request");
441 let serialized =
442 serde_json::to_string(&request).expect("Failed to serialize mapping request");
443 let deserialized: MappingRequest =
444 serde_json::from_str(&serialized).expect("Failed to deserialize mapping request");
445 assert_eq!(request, deserialized);
446 }
447}