Skip to main content

openfigi_rs/model/response/
search_response.rs

1//! Response models for the OpenFIGI search endpoint.
2//!
3//! This module contains the data structures used to represent responses from the
4//! [OpenFIGI search endpoint](https://www.openfigi.com/api/documentation#v3-post-search).
5//! The search endpoint allows finding financial instruments using text-based queries
6//! and returns either successful FIGI results with optional pagination or error responses.
7//!
8//! # Response Structure
9//!
10//! The search endpoint returns either:
11//! - [`SearchData`] containing successful FIGI results with optional pagination metadata
12//! - [`crate::error::OpenFIGIError`] when the filter request fails
13//!
14//! # Examples
15//!
16//! ```rust
17//! use openfigi_rs::model::response::SearchData;
18//! use serde_json;
19//!
20//! // Successful search response with results
21//! let json = r#"{
22//!     "data": [
23//!         {"figi": "BBG000BLNNH6", "ticker": "AAPL", "name": "Apple Inc"},
24//!         {"figi": "BBG000B9XRY4", "ticker": "TSLA", "name": "Tesla Inc"}
25//!     ],
26//!     "next": "pagination_token_here"
27//! }"#;
28//! let response: SearchData = serde_json::from_str(json).unwrap();
29//! assert_eq!(response.data().len(), 2);
30//! assert!(response.next_page().is_some());
31//! ```
32//!
33//! Note: This module is not intended for direct use by consumers of the OpenFIGI API.
34
35use crate::model::response::common::FigiResult;
36use serde::{Deserialize, Serialize};
37
38/// Successful search result containing FIGI data and optional pagination information.
39///
40/// This structure represents the payload returned when a search query successfully
41/// finds matching financial instruments. The search endpoint can return multiple
42/// FIGI results for a single query, especially when the search term matches multiple
43/// instruments or variations of the same instrument across different exchanges.
44///
45/// # Field Descriptions
46///
47/// - `data`: Array of FIGI results matching the search query, ordered by relevance
48/// - `next`: Optional pagination token for retrieving additional search results
49///
50/// # Pagination
51///
52/// When the result set is large, the API may return only a subset of results along
53/// with a `next` token. This token can be used in subsequent requests to retrieve
54/// additional pages of results.
55#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
56pub struct SearchData {
57    /// Array of FIGI results matching the search query.
58    ///
59    /// Contains financial instruments that match the submitted search criteria.
60    /// Results are typically ordered by relevance, with the most likely matches
61    /// appearing first. The array may be empty if no instruments match the search query.
62    pub data: Vec<FigiResult>,
63
64    /// Pagination token for retrieving the next page of search results.
65    ///
66    /// This field is present when there are more search results available beyond
67    /// the current page. Use this token in subsequent search requests to retrieve
68    /// additional results. When `None`, this indicates the last page of results.
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub next: Option<String>,
71}
72
73impl SearchData {
74    /// Returns a slice of the FIGI results contained in this search response.
75    ///
76    /// Provides access to the financial instrument data returned by the search endpoint.
77    #[must_use]
78    pub fn data(&self) -> &[FigiResult] {
79        &self.data
80    }
81
82    /// Returns the pagination token for retrieving the next page of search results.
83    ///
84    /// Returns `Some(token)` if more search results are available, `None` if this is the last page.
85    /// The token can be used in subsequent search requests to continue pagination.
86    #[must_use]
87    pub fn next_page(&self) -> Option<&str> {
88        self.next.as_deref()
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::{model::response::ResponseResult, test_utils::load_test_data};
96
97    /// Type alias for the search response used in tests
98    pub type SearchResponse = ResponseResult<SearchData>;
99
100    #[test]
101    fn test_deserialize_query_example() {
102        let json_str = load_test_data("search", "query_example.json");
103        let search_response: SearchResponse =
104            serde_json::from_str(&json_str).expect("Failed to deserialize search response");
105
106        let search_data = match search_response {
107            ResponseResult::Success(ref data) => data,
108            ResponseResult::Error(ref err) => panic!("Expected success, got error: {err:?}"),
109        };
110        let figi_result = search_data.data();
111        assert!(!figi_result.is_empty());
112
113        // Check first IBM entry
114        let first_entry = &figi_result[0];
115        assert_eq!(first_entry.figi, "BBG0002ZTPP5");
116        assert_eq!(first_entry.ticker, Some("IBM 03/20/10 P130".to_string()));
117        assert_eq!(first_entry.display_name(), "March 10 Puts on IBM US");
118
119        // Check if composite_figi and share_class_figi exists
120        assert!(first_entry.has_composite_figi());
121        assert!(!first_entry.has_share_class_figi());
122
123        // Verify actual field values from real data
124        assert_eq!(first_entry.composite_figi, Some("BBG0002ZTPP5".to_string()));
125        assert_eq!(first_entry.share_class_figi, None);
126
127        // Verify pagination exists
128        assert!(search_data.next_page().is_some());
129    }
130
131    #[test]
132    fn test_deserialize_no_data() {
133        let json_str = load_test_data("search", "no_data.json");
134        let search_response: SearchResponse =
135            serde_json::from_str(&json_str).expect("Failed to deserialize search response");
136
137        let figi_result = match search_response {
138            ResponseResult::Success(ref data) => data.data(),
139            ResponseResult::Error(ref err) => panic!("Expected success, got error: {err:?}"),
140        };
141        assert!(figi_result.is_empty());
142    }
143}