use crate::error::EveryMapResult;
use crate::types::{Address, BoundingBox, Coordinate};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GeocodeOptions {
pub limit: Option<u32>,
pub language: Option<String>,
pub country_codes: Vec<String>,
pub bounding_box: Option<BoundingBox>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_extra: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ReverseGeocodeOptions {
pub limit: Option<u32>,
pub language: Option<String>,
pub radius: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_extra: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum SearchResultType {
ExactMatch,
Approximate,
Interpolated,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResult {
pub id: Option<String>,
pub coordinate: Coordinate,
pub address: Address,
pub title: Option<String>,
pub result_type: SearchResultType,
pub distance: Option<f64>,
pub confidence: Option<f64>,
pub categories: Vec<String>,
pub bounding_box: Option<BoundingBox>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub raw: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResponse {
pub items: Vec<SearchResult>,
}
#[async_trait]
pub trait Geocoder: Send + Sync {
async fn geocode(
&self,
query: &str,
options: &GeocodeOptions,
) -> EveryMapResult<SearchResponse>;
async fn reverse_geocode(
&self,
coordinate: &Coordinate,
options: &ReverseGeocodeOptions,
) -> EveryMapResult<SearchResponse>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_geocode_options_default() {
let options = GeocodeOptions::default();
assert!(options.limit.is_none());
assert!(options.language.is_none());
assert!(options.country_codes.is_empty());
assert!(options.bounding_box.is_none());
assert!(options.provider_extra.is_none());
}
#[test]
fn test_geocode_options_with_fields() {
let options = GeocodeOptions {
limit: Some(10),
language: Some("en-US".to_string()),
country_codes: vec!["DEU".to_string(), "FRA".to_string()],
provider_extra: Some(serde_json::json!({"political_view": "ARG"})),
..Default::default()
};
assert_eq!(options.limit, Some(10));
assert_eq!(options.language.as_deref(), Some("en-US"));
assert_eq!(options.country_codes.len(), 2);
assert!(options.provider_extra.is_some());
}
#[test]
fn test_geocode_options_provider_extra_json() {
let options = GeocodeOptions {
provider_extra: Some(serde_json::json!({
"show": ["streetInfo", "mapReference"],
"qq": "city=Berlin"
})),
..Default::default()
};
let extra = options.provider_extra.unwrap();
assert!(extra.get("show").is_some());
assert_eq!(extra["qq"], "city=Berlin");
}
#[test]
fn test_reverse_geocode_options_default() {
let options = ReverseGeocodeOptions::default();
assert!(options.limit.is_none());
assert!(options.language.is_none());
assert!(options.radius.is_none());
assert!(options.provider_extra.is_none());
}
#[test]
fn test_search_result_type_serialization() {
assert_eq!(
serde_json::to_string(&SearchResultType::ExactMatch).unwrap(),
"\"ExactMatch\""
);
assert_eq!(
serde_json::to_string(&SearchResultType::Unknown).unwrap(),
"\"Unknown\""
);
let rt: SearchResultType = serde_json::from_str("\"Approximate\"").unwrap();
assert_eq!(rt, SearchResultType::Approximate);
}
#[test]
fn test_search_result_construction() {
let result = SearchResult {
id: Some("test-id".to_string()),
coordinate: Coordinate::new(52.5, 13.4).unwrap(),
address: Address::empty(),
title: Some("Test Place".to_string()),
result_type: SearchResultType::ExactMatch,
distance: Some(150.0),
confidence: Some(0.95),
categories: vec!["restaurant".to_string()],
bounding_box: None,
raw: Some(serde_json::json!({"provider_specific": true})),
};
assert_eq!(result.id.as_deref(), Some("test-id"));
assert_eq!(result.coordinate.lat, 52.5);
assert_eq!(result.result_type, SearchResultType::ExactMatch);
assert!(result.raw.is_some());
}
#[test]
fn test_search_response_serialization_roundtrip() {
let response = SearchResponse {
items: vec![SearchResult {
id: Some("abc".to_string()),
coordinate: Coordinate::new(1.0, 2.0).unwrap(),
address: Address::empty(),
title: None,
result_type: SearchResultType::Unknown,
distance: None,
confidence: None,
categories: vec![],
bounding_box: None,
raw: None,
}],
};
let json = serde_json::to_string(&response).unwrap();
let deserialized: SearchResponse = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.items.len(), 1);
assert_eq!(deserialized.items[0].id.as_deref(), Some("abc"));
}
#[test]
fn test_search_result_type_all_variants_serde() {
let variants = [
SearchResultType::ExactMatch,
SearchResultType::Approximate,
SearchResultType::Interpolated,
SearchResultType::Unknown,
];
for v in &variants {
let json = serde_json::to_string(v).unwrap();
let back: SearchResultType = serde_json::from_str(&json).unwrap();
assert_eq!(*v, back, "Failed roundtrip for {:?}", v);
}
}
#[test]
fn test_search_result_type_all_variants_distinct() {
let variants = [
SearchResultType::ExactMatch,
SearchResultType::Approximate,
SearchResultType::Interpolated,
SearchResultType::Unknown,
];
for i in 0..variants.len() {
for j in 0..variants.len() {
if i != j {
assert_ne!(variants[i], variants[j]);
}
}
}
}
#[test]
fn test_search_response_empty_serde_roundtrip() {
let response = SearchResponse { items: vec![] };
let json = serde_json::to_string(&response).unwrap();
let back: SearchResponse = serde_json::from_str(&json).unwrap();
assert!(back.items.is_empty());
}
#[test]
fn test_search_response_full_serde_roundtrip() {
let ne = crate::types::BoundingBox::new(
Coordinate::new(52.6, 13.5).unwrap(),
Coordinate::new(52.4, 13.3).unwrap(),
);
let response = SearchResponse {
items: vec![SearchResult {
id: Some("id-123".to_string()),
coordinate: Coordinate::new(52.52, 13.40).unwrap(),
address: Address::from_label("Brandenburg Gate".to_string()),
title: Some("Brandenburg Gate".to_string()),
result_type: SearchResultType::ExactMatch,
distance: Some(250.0),
confidence: Some(0.98),
categories: vec!["monument".to_string(), "landmark".to_string()],
bounding_box: Some(ne),
raw: Some(serde_json::json!({"source": "here"})),
}],
};
let json = serde_json::to_string(&response).unwrap();
let back: SearchResponse = serde_json::from_str(&json).unwrap();
assert_eq!(back.items.len(), 1);
assert_eq!(back.items[0].id.as_deref(), Some("id-123"));
assert_eq!(back.items[0].title.as_deref(), Some("Brandenburg Gate"));
assert_eq!(back.items[0].result_type, SearchResultType::ExactMatch);
assert_eq!(back.items[0].distance, Some(250.0));
assert_eq!(back.items[0].confidence, Some(0.98));
assert_eq!(back.items[0].categories.len(), 2);
assert!(back.items[0].bounding_box.is_some());
assert!(back.items[0].raw.is_some());
}
#[test]
fn test_geocode_options_serde_roundtrip() {
let options = GeocodeOptions {
limit: Some(5),
language: Some("de-DE".to_string()),
country_codes: vec!["DEU".to_string()],
bounding_box: Some(crate::types::BoundingBox::new(
Coordinate::new(52.6, 13.5).unwrap(),
Coordinate::new(52.4, 13.3).unwrap(),
)),
provider_extra: Some(serde_json::json!({"political_view": "ARG"})),
};
let json = serde_json::to_string(&options).unwrap();
let back: GeocodeOptions = serde_json::from_str(&json).unwrap();
assert_eq!(back.limit, Some(5));
assert_eq!(back.language.as_deref(), Some("de-DE"));
assert_eq!(back.country_codes.len(), 1);
assert!(back.bounding_box.is_some());
assert!(back.provider_extra.is_some());
}
#[test]
fn test_reverse_geocode_options_serde_roundtrip() {
let options = ReverseGeocodeOptions {
limit: Some(1),
language: Some("fr".to_string()),
radius: Some(500.0),
provider_extra: Some(serde_json::json!({"include_shapes": true})),
};
let json = serde_json::to_string(&options).unwrap();
let back: ReverseGeocodeOptions = serde_json::from_str(&json).unwrap();
assert_eq!(back.limit, Some(1));
assert_eq!(back.radius, Some(500.0));
assert!(back.provider_extra.is_some());
}
#[test]
fn test_search_result_zero_distance() {
let result = SearchResult {
id: None,
coordinate: Coordinate::ORIGIN,
address: Address::empty(),
title: None,
result_type: SearchResultType::Unknown,
distance: Some(0.0),
confidence: Some(0.0),
categories: vec![],
bounding_box: None,
raw: None,
};
assert_eq!(result.distance, Some(0.0));
assert_eq!(result.confidence, Some(0.0));
}
#[test]
fn test_search_result_large_coordinates() {
let result = SearchResult {
id: None,
coordinate: Coordinate::new(89.9999, 179.9999).unwrap(),
address: Address::empty(),
title: None,
result_type: SearchResultType::Unknown,
distance: None,
confidence: None,
categories: vec![],
bounding_box: None,
raw: None,
};
assert!((result.coordinate.lat - 89.9999).abs() < f64::EPSILON);
}
#[test]
fn test_search_result_with_empty_categories() {
let result = SearchResult {
id: Some("x".to_string()),
coordinate: Coordinate::ORIGIN,
address: Address::empty(),
title: None,
result_type: SearchResultType::Approximate,
distance: None,
confidence: None,
categories: vec![],
bounding_box: None,
raw: None,
};
assert!(result.categories.is_empty());
}
}