1use crate::error::EveryMapResult;
2use crate::types::{Address, BoundingBox, Coordinate};
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Default, Serialize, Deserialize)]
8pub struct GeocodeOptions {
9 pub limit: Option<u32>,
11 pub language: Option<String>,
13 pub country_codes: Vec<String>,
15 pub bounding_box: Option<BoundingBox>,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub provider_extra: Option<serde_json::Value>,
20}
21
22#[derive(Debug, Clone, Default, Serialize, Deserialize)]
24pub struct ReverseGeocodeOptions {
25 pub limit: Option<u32>,
27 pub language: Option<String>,
29 pub radius: Option<f64>,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub provider_extra: Option<serde_json::Value>,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
38pub enum SearchResultType {
39 ExactMatch,
41 Approximate,
43 Interpolated,
45 Unknown,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct SearchResult {
55 pub id: Option<String>,
57 pub coordinate: Coordinate,
59 pub address: Address,
61 pub title: Option<String>,
63 pub result_type: SearchResultType,
65 pub distance: Option<f64>,
67 pub confidence: Option<f64>,
69 pub categories: Vec<String>,
71 pub bounding_box: Option<BoundingBox>,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub raw: Option<serde_json::Value>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct SearchResponse {
80 pub items: Vec<SearchResult>,
81}
82
83#[async_trait]
92pub trait Geocoder: Send + Sync {
93 async fn geocode(
94 &self,
95 query: &str,
96 options: &GeocodeOptions,
97 ) -> EveryMapResult<SearchResponse>;
98 async fn reverse_geocode(
99 &self,
100 coordinate: &Coordinate,
101 options: &ReverseGeocodeOptions,
102 ) -> EveryMapResult<SearchResponse>;
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn test_geocode_options_default() {
111 let options = GeocodeOptions::default();
112 assert!(options.limit.is_none());
113 assert!(options.language.is_none());
114 assert!(options.country_codes.is_empty());
115 assert!(options.bounding_box.is_none());
116 assert!(options.provider_extra.is_none());
117 }
118
119 #[test]
120 fn test_geocode_options_with_fields() {
121 let options = GeocodeOptions {
122 limit: Some(10),
123 language: Some("en-US".to_string()),
124 country_codes: vec!["DEU".to_string(), "FRA".to_string()],
125 provider_extra: Some(serde_json::json!({"political_view": "ARG"})),
126 ..Default::default()
127 };
128 assert_eq!(options.limit, Some(10));
129 assert_eq!(options.language.as_deref(), Some("en-US"));
130 assert_eq!(options.country_codes.len(), 2);
131 assert!(options.provider_extra.is_some());
132 }
133
134 #[test]
135 fn test_geocode_options_provider_extra_json() {
136 let options = GeocodeOptions {
137 provider_extra: Some(serde_json::json!({
138 "show": ["streetInfo", "mapReference"],
139 "qq": "city=Berlin"
140 })),
141 ..Default::default()
142 };
143 let extra = options.provider_extra.unwrap();
144 assert!(extra.get("show").is_some());
145 assert_eq!(extra["qq"], "city=Berlin");
146 }
147
148 #[test]
149 fn test_reverse_geocode_options_default() {
150 let options = ReverseGeocodeOptions::default();
151 assert!(options.limit.is_none());
152 assert!(options.language.is_none());
153 assert!(options.radius.is_none());
154 assert!(options.provider_extra.is_none());
155 }
156
157 #[test]
158 fn test_search_result_type_serialization() {
159 assert_eq!(
160 serde_json::to_string(&SearchResultType::ExactMatch).unwrap(),
161 "\"ExactMatch\""
162 );
163 assert_eq!(
164 serde_json::to_string(&SearchResultType::Unknown).unwrap(),
165 "\"Unknown\""
166 );
167 let rt: SearchResultType = serde_json::from_str("\"Approximate\"").unwrap();
168 assert_eq!(rt, SearchResultType::Approximate);
169 }
170
171 #[test]
172 fn test_search_result_construction() {
173 let result = SearchResult {
174 id: Some("test-id".to_string()),
175 coordinate: Coordinate::new(52.5, 13.4).unwrap(),
176 address: Address::empty(),
177 title: Some("Test Place".to_string()),
178 result_type: SearchResultType::ExactMatch,
179 distance: Some(150.0),
180 confidence: Some(0.95),
181 categories: vec!["restaurant".to_string()],
182 bounding_box: None,
183 raw: Some(serde_json::json!({"provider_specific": true})),
184 };
185 assert_eq!(result.id.as_deref(), Some("test-id"));
186 assert_eq!(result.coordinate.lat, 52.5);
187 assert_eq!(result.result_type, SearchResultType::ExactMatch);
188 assert!(result.raw.is_some());
189 }
190
191 #[test]
192 fn test_search_response_serialization_roundtrip() {
193 let response = SearchResponse {
194 items: vec![SearchResult {
195 id: Some("abc".to_string()),
196 coordinate: Coordinate::new(1.0, 2.0).unwrap(),
197 address: Address::empty(),
198 title: None,
199 result_type: SearchResultType::Unknown,
200 distance: None,
201 confidence: None,
202 categories: vec![],
203 bounding_box: None,
204 raw: None,
205 }],
206 };
207 let json = serde_json::to_string(&response).unwrap();
208 let deserialized: SearchResponse = serde_json::from_str(&json).unwrap();
209 assert_eq!(deserialized.items.len(), 1);
210 assert_eq!(deserialized.items[0].id.as_deref(), Some("abc"));
211 }
212
213 #[test]
216 fn test_search_result_type_all_variants_serde() {
217 let variants = [
218 SearchResultType::ExactMatch,
219 SearchResultType::Approximate,
220 SearchResultType::Interpolated,
221 SearchResultType::Unknown,
222 ];
223 for v in &variants {
224 let json = serde_json::to_string(v).unwrap();
225 let back: SearchResultType = serde_json::from_str(&json).unwrap();
226 assert_eq!(*v, back, "Failed roundtrip for {:?}", v);
227 }
228 }
229
230 #[test]
231 fn test_search_result_type_all_variants_distinct() {
232 let variants = [
233 SearchResultType::ExactMatch,
234 SearchResultType::Approximate,
235 SearchResultType::Interpolated,
236 SearchResultType::Unknown,
237 ];
238 for i in 0..variants.len() {
239 for j in 0..variants.len() {
240 if i != j {
241 assert_ne!(variants[i], variants[j]);
242 }
243 }
244 }
245 }
246
247 #[test]
250 fn test_search_response_empty_serde_roundtrip() {
251 let response = SearchResponse { items: vec![] };
252 let json = serde_json::to_string(&response).unwrap();
253 let back: SearchResponse = serde_json::from_str(&json).unwrap();
254 assert!(back.items.is_empty());
255 }
256
257 #[test]
260 fn test_search_response_full_serde_roundtrip() {
261 let ne = crate::types::BoundingBox::new(
262 Coordinate::new(52.6, 13.5).unwrap(),
263 Coordinate::new(52.4, 13.3).unwrap(),
264 );
265 let response = SearchResponse {
266 items: vec![SearchResult {
267 id: Some("id-123".to_string()),
268 coordinate: Coordinate::new(52.52, 13.40).unwrap(),
269 address: Address::from_label("Brandenburg Gate".to_string()),
270 title: Some("Brandenburg Gate".to_string()),
271 result_type: SearchResultType::ExactMatch,
272 distance: Some(250.0),
273 confidence: Some(0.98),
274 categories: vec!["monument".to_string(), "landmark".to_string()],
275 bounding_box: Some(ne),
276 raw: Some(serde_json::json!({"source": "here"})),
277 }],
278 };
279 let json = serde_json::to_string(&response).unwrap();
280 let back: SearchResponse = serde_json::from_str(&json).unwrap();
281 assert_eq!(back.items.len(), 1);
282 assert_eq!(back.items[0].id.as_deref(), Some("id-123"));
283 assert_eq!(back.items[0].title.as_deref(), Some("Brandenburg Gate"));
284 assert_eq!(back.items[0].result_type, SearchResultType::ExactMatch);
285 assert_eq!(back.items[0].distance, Some(250.0));
286 assert_eq!(back.items[0].confidence, Some(0.98));
287 assert_eq!(back.items[0].categories.len(), 2);
288 assert!(back.items[0].bounding_box.is_some());
289 assert!(back.items[0].raw.is_some());
290 }
291
292 #[test]
295 fn test_geocode_options_serde_roundtrip() {
296 let options = GeocodeOptions {
297 limit: Some(5),
298 language: Some("de-DE".to_string()),
299 country_codes: vec!["DEU".to_string()],
300 bounding_box: Some(crate::types::BoundingBox::new(
301 Coordinate::new(52.6, 13.5).unwrap(),
302 Coordinate::new(52.4, 13.3).unwrap(),
303 )),
304 provider_extra: Some(serde_json::json!({"political_view": "ARG"})),
305 };
306 let json = serde_json::to_string(&options).unwrap();
307 let back: GeocodeOptions = serde_json::from_str(&json).unwrap();
308 assert_eq!(back.limit, Some(5));
309 assert_eq!(back.language.as_deref(), Some("de-DE"));
310 assert_eq!(back.country_codes.len(), 1);
311 assert!(back.bounding_box.is_some());
312 assert!(back.provider_extra.is_some());
313 }
314
315 #[test]
318 fn test_reverse_geocode_options_serde_roundtrip() {
319 let options = ReverseGeocodeOptions {
320 limit: Some(1),
321 language: Some("fr".to_string()),
322 radius: Some(500.0),
323 provider_extra: Some(serde_json::json!({"include_shapes": true})),
324 };
325 let json = serde_json::to_string(&options).unwrap();
326 let back: ReverseGeocodeOptions = serde_json::from_str(&json).unwrap();
327 assert_eq!(back.limit, Some(1));
328 assert_eq!(back.radius, Some(500.0));
329 assert!(back.provider_extra.is_some());
330 }
331
332 #[test]
335 fn test_search_result_zero_distance() {
336 let result = SearchResult {
337 id: None,
338 coordinate: Coordinate::ORIGIN,
339 address: Address::empty(),
340 title: None,
341 result_type: SearchResultType::Unknown,
342 distance: Some(0.0),
343 confidence: Some(0.0),
344 categories: vec![],
345 bounding_box: None,
346 raw: None,
347 };
348 assert_eq!(result.distance, Some(0.0));
349 assert_eq!(result.confidence, Some(0.0));
350 }
351
352 #[test]
353 fn test_search_result_large_coordinates() {
354 let result = SearchResult {
355 id: None,
356 coordinate: Coordinate::new(89.9999, 179.9999).unwrap(),
357 address: Address::empty(),
358 title: None,
359 result_type: SearchResultType::Unknown,
360 distance: None,
361 confidence: None,
362 categories: vec![],
363 bounding_box: None,
364 raw: None,
365 };
366 assert!((result.coordinate.lat - 89.9999).abs() < f64::EPSILON);
367 }
368
369 #[test]
370 fn test_search_result_with_empty_categories() {
371 let result = SearchResult {
372 id: Some("x".to_string()),
373 coordinate: Coordinate::ORIGIN,
374 address: Address::empty(),
375 title: None,
376 result_type: SearchResultType::Approximate,
377 distance: None,
378 confidence: None,
379 categories: vec![],
380 bounding_box: None,
381 raw: None,
382 };
383 assert!(result.categories.is_empty());
384 }
385}