1use super::common::{deserialize_string_or_number, deserialize_string_or_number_required};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Serialize, Deserialize)]
10pub struct SearchResponse {
11 pub total: u64,
12 pub hits: Vec<CrashHit>,
13 #[serde(default)]
14 pub facets: HashMap<String, Vec<FacetBucket>>,
15}
16
17#[derive(Debug, Serialize, Deserialize)]
18pub struct CrashHit {
19 pub uuid: String,
20 pub date: String,
21 pub signature: String,
22 pub product: String,
23 pub version: String,
24 #[serde(default)]
25 pub platform: Option<String>,
26 #[serde(default, deserialize_with = "deserialize_string_or_number")]
27 pub build_id: Option<String>,
28 #[serde(default)]
29 pub release_channel: Option<String>,
30 #[serde(default)]
31 pub platform_version: Option<String>,
32}
33
34#[derive(Debug, Serialize, Deserialize)]
35pub struct FacetBucket {
36 #[serde(deserialize_with = "deserialize_string_or_number_required")]
37 pub term: String,
38 pub count: u64,
39}
40
41pub struct SearchParams {
42 pub signature: Option<String>,
43 pub proto_signature: Option<String>,
44 pub product: String,
45 pub version: Option<String>,
46 pub platform: Option<String>,
47 pub cpu_arch: Option<String>,
48 pub release_channel: Option<String>,
49 pub platform_version: Option<String>,
50 pub process_type: Option<String>,
51 pub date_from: String,
52 pub date_to: Option<String>,
53 pub limit: usize,
54 pub facets: Vec<String>,
55 pub facets_size: Option<usize>,
56 pub sort: String,
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 #[test]
64 fn test_deserialize_search_response() {
65 let json = r#"{
66 "total": 42,
67 "hits": [
68 {
69 "uuid": "247653e8-7a18-4836-97d1-42a720260120",
70 "date": "2024-01-15T10:30:00",
71 "signature": "mozilla::SomeFunction",
72 "product": "Firefox",
73 "version": "120.0",
74 "platform": "Windows"
75 }
76 ],
77 "facets": {}
78 }"#;
79
80 let response: SearchResponse = serde_json::from_str(json).unwrap();
81 assert_eq!(response.total, 42);
82 assert_eq!(response.hits.len(), 1);
83 assert_eq!(
84 response.hits[0].uuid,
85 "247653e8-7a18-4836-97d1-42a720260120"
86 );
87 assert_eq!(response.hits[0].signature, "mozilla::SomeFunction");
88 }
89
90 #[test]
91 fn test_deserialize_search_response_with_facets() {
92 let json = r#"{
93 "total": 100,
94 "hits": [],
95 "facets": {
96 "version": [
97 {"term": "120.0", "count": 50},
98 {"term": "119.0", "count": 30},
99 {"term": "118.0", "count": 20}
100 ],
101 "platform": [
102 {"term": "Windows", "count": 60},
103 {"term": "Linux", "count": 40}
104 ]
105 }
106 }"#;
107
108 let response: SearchResponse = serde_json::from_str(json).unwrap();
109 assert_eq!(response.total, 100);
110 assert_eq!(response.facets.len(), 2);
111
112 let version_facets = response.facets.get("version").unwrap();
113 assert_eq!(version_facets.len(), 3);
114 assert_eq!(version_facets[0].term, "120.0");
115 assert_eq!(version_facets[0].count, 50);
116 }
117
118 #[test]
119 fn test_deserialize_facet_with_integer_term() {
120 let json = r#"{
121 "total": 100,
122 "hits": [],
123 "facets": {
124 "build_id": [
125 {"term": 20251116092356, "count": 47},
126 {"term": 20251116210936, "count": 40},
127 {"term": "20251115204042", "count": 22}
128 ]
129 }
130 }"#;
131
132 let response: SearchResponse = serde_json::from_str(json).unwrap();
133 let build_id_facets = response.facets.get("build_id").unwrap();
134 assert_eq!(build_id_facets.len(), 3);
135 assert_eq!(build_id_facets[0].term, "20251116092356");
136 assert_eq!(build_id_facets[0].count, 47);
137 assert_eq!(build_id_facets[1].term, "20251116210936");
138 assert_eq!(build_id_facets[2].term, "20251115204042");
139 }
140
141 #[test]
142 fn test_deserialize_crash_hit_missing_platform() {
143 let json = r#"{
144 "uuid": "test-id",
145 "date": "2024-01-15",
146 "signature": "crash_sig",
147 "product": "Firefox",
148 "version": "120.0"
149 }"#;
150
151 let hit: CrashHit = serde_json::from_str(json).unwrap();
152 assert_eq!(hit.platform, None);
153 }
154
155 #[test]
156 fn test_deserialize_empty_response() {
157 let json = r#"{
158 "total": 0,
159 "hits": [],
160 "facets": {}
161 }"#;
162
163 let response: SearchResponse = serde_json::from_str(json).unwrap();
164 assert_eq!(response.total, 0);
165 assert!(response.hits.is_empty());
166 assert!(response.facets.is_empty());
167 }
168}