Skip to main content

polyoxide_gamma/api/
search.rs

1use polyoxide_core::{HttpClient, QueryBuilder, Request};
2use serde::{Deserialize, Serialize};
3
4use crate::{
5    error::GammaError,
6    types::{Event, Tag},
7};
8
9/// Search namespace for search operations
10#[derive(Clone)]
11pub struct Search {
12    pub(crate) http_client: HttpClient,
13}
14
15impl Search {
16    /// Search profiles, events, and tags
17    pub fn public_search(&self, query: impl Into<String>) -> PublicSearch {
18        let request =
19            Request::new(self.http_client.clone(), "/public-search").query("q", query.into());
20        PublicSearch { request }
21    }
22}
23
24/// Request builder for public search
25pub struct PublicSearch {
26    request: Request<SearchResponse, GammaError>,
27}
28
29impl PublicSearch {
30    /// Include profile results in search
31    pub fn search_profiles(mut self, include: bool) -> Self {
32        self.request = self.request.query("search_profiles", include);
33        self
34    }
35
36    /// Set maximum results per type
37    pub fn limit_per_type(mut self, limit: u32) -> Self {
38        self.request = self.request.query("limit_per_type", limit);
39        self
40    }
41
42    /// Set page number
43    pub fn page(mut self, page: u32) -> Self {
44        self.request = self.request.query("page", page);
45        self
46    }
47
48    /// Enable/disable caching
49    pub fn cache(mut self, cache: bool) -> Self {
50        self.request = self.request.query("cache", cache);
51        self
52    }
53
54    /// Filter by event status
55    pub fn events_status(mut self, status: impl Into<String>) -> Self {
56        self.request = self.request.query("events_status", status.into());
57        self
58    }
59
60    /// Filter by event tag IDs
61    pub fn events_tag(mut self, tag_ids: impl IntoIterator<Item = impl ToString>) -> Self {
62        self.request = self.request.query_many("events_tag", tag_ids);
63        self
64    }
65
66    /// Include closed markets in results
67    pub fn keep_closed_markets(mut self, keep: bool) -> Self {
68        self.request = self.request.query("keep_closed_markets", keep);
69        self
70    }
71
72    /// Set sort order
73    pub fn sort(mut self, sort: impl Into<String>) -> Self {
74        self.request = self.request.query("sort", sort.into());
75        self
76    }
77
78    /// Include tag search results
79    pub fn search_tags(mut self, include: bool) -> Self {
80        self.request = self.request.query("search_tags", include);
81        self
82    }
83
84    /// Filter by recurrence pattern
85    pub fn recurrence(mut self, recurrence: impl Into<String>) -> Self {
86        self.request = self.request.query("recurrence", recurrence.into());
87        self
88    }
89
90    /// Exclude events with specified tag IDs
91    pub fn exclude_tag_id(mut self, tag_ids: impl IntoIterator<Item = i64>) -> Self {
92        self.request = self.request.query_many("exclude_tag_id", tag_ids);
93        self
94    }
95
96    /// Enable optimized search
97    pub fn optimized(mut self, optimized: bool) -> Self {
98        self.request = self.request.query("optimized", optimized);
99        self
100    }
101
102    /// Execute the request
103    pub async fn send(self) -> Result<SearchResponse, GammaError> {
104        self.request.send().await
105    }
106}
107
108/// Response from public search
109#[cfg_attr(feature = "specta", derive(specta::Type))]
110#[derive(Debug, Clone, Serialize, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct SearchResponse {
113    /// Matching user profiles
114    #[serde(default)]
115    pub profiles: Vec<SearchProfile>,
116    /// Matching events
117    #[serde(default)]
118    pub events: Vec<Event>,
119    /// Matching tags
120    #[serde(default)]
121    pub tags: Vec<Tag>,
122}
123
124/// Profile result from search
125#[cfg_attr(feature = "specta", derive(specta::Type))]
126#[derive(Debug, Clone, Serialize, Deserialize)]
127#[serde(rename_all = "camelCase")]
128pub struct SearchProfile {
129    /// User address
130    pub address: Option<String>,
131    /// Display name
132    pub name: Option<String>,
133    /// Profile image URL
134    pub profile_image: Option<String>,
135    /// User pseudonym
136    pub pseudonym: Option<String>,
137    /// User biography
138    pub bio: Option<String>,
139    /// Proxy wallet address
140    pub proxy_wallet: Option<String>,
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use crate::Gamma;
147
148    fn gamma() -> Gamma {
149        Gamma::new().unwrap()
150    }
151
152    #[test]
153    fn test_public_search_full_chain() {
154        let _search = gamma()
155            .search()
156            .public_search("bitcoin")
157            .search_profiles(true)
158            .limit_per_type(10)
159            .page(1)
160            .cache(false)
161            .events_status("active")
162            .events_tag(vec![1i64, 2])
163            .keep_closed_markets(false)
164            .sort("volume")
165            .search_tags(true)
166            .recurrence("daily")
167            .exclude_tag_id(vec![99i64])
168            .optimized(true);
169    }
170
171    #[test]
172    fn test_search_response_deserialization() {
173        let json = r#"{
174            "profiles": [
175                {
176                    "address": "0xabc",
177                    "name": "trader1",
178                    "profileImage": null,
179                    "pseudonym": null,
180                    "bio": null,
181                    "proxyWallet": "0xproxy"
182                }
183            ],
184            "events": [],
185            "tags": []
186        }"#;
187        let resp: SearchResponse = serde_json::from_str(json).unwrap();
188        assert_eq!(resp.profiles.len(), 1);
189        assert_eq!(resp.profiles[0].address.as_deref(), Some("0xabc"));
190        assert!(resp.events.is_empty());
191        assert!(resp.tags.is_empty());
192    }
193
194    #[test]
195    fn test_search_response_empty() {
196        let json = r#"{"profiles": [], "events": [], "tags": []}"#;
197        let resp: SearchResponse = serde_json::from_str(json).unwrap();
198        assert!(resp.profiles.is_empty());
199    }
200
201    #[test]
202    fn test_search_response_missing_fields() {
203        let json = r#"{}"#;
204        let resp: SearchResponse = serde_json::from_str(json).unwrap();
205        assert!(resp.profiles.is_empty());
206        assert!(resp.events.is_empty());
207        assert!(resp.tags.is_empty());
208    }
209
210    #[test]
211    fn test_search_profile_deserialization() {
212        let json = r#"{
213            "address": "0x123",
214            "name": "Searcher",
215            "profileImage": "https://img.example.com/pic.png",
216            "pseudonym": "anon",
217            "bio": "A bio",
218            "proxyWallet": "0xproxy123"
219        }"#;
220        let profile: SearchProfile = serde_json::from_str(json).unwrap();
221        assert_eq!(profile.address.as_deref(), Some("0x123"));
222        assert_eq!(profile.name.as_deref(), Some("Searcher"));
223        assert_eq!(profile.bio.as_deref(), Some("A bio"));
224        assert_eq!(profile.proxy_wallet.as_deref(), Some("0xproxy123"));
225    }
226
227    #[test]
228    fn test_search_profile_all_null() {
229        let json = r#"{}"#;
230        let profile: SearchProfile = serde_json::from_str(json).unwrap();
231        assert!(profile.address.is_none());
232        assert!(profile.name.is_none());
233        assert!(profile.profile_image.is_none());
234        assert!(profile.pseudonym.is_none());
235        assert!(profile.bio.is_none());
236        assert!(profile.proxy_wallet.is_none());
237    }
238}