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}