use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Coordinates {
pub lat: f64,
pub lon: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GeocodingResult {
pub lat: f64,
pub lon: f64,
pub name: Option<String>,
pub country: Option<String>,
pub state: Option<String>,
pub city: Option<String>,
pub postcode: Option<String>,
pub street: Option<String>,
pub housenumber: Option<String>,
pub osm_key: Option<String>,
pub osm_value: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct GeocodingResponse {
pub results: Vec<GeocodingResult>,
}
impl GeocodingResponse {
pub fn first(&self) -> Option<&GeocodingResult> {
self.results.first()
}
pub fn is_empty(&self) -> bool {
self.results.is_empty()
}
pub fn len(&self) -> usize {
self.results.len()
}
pub fn iter(&self) -> impl Iterator<Item = &GeocodingResult> {
self.results.iter()
}
}
impl IntoIterator for GeocodingResponse {
type Item = GeocodingResult;
type IntoIter = std::vec::IntoIter<GeocodingResult>;
fn into_iter(self) -> Self::IntoIter {
self.results.into_iter()
}
}
impl<'a> IntoIterator for &'a GeocodingResponse {
type Item = &'a GeocodingResult;
type IntoIter = std::slice::Iter<'a, GeocodingResult>;
fn into_iter(self) -> Self::IntoIter {
self.results.iter()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Place {
#[serde(default)]
pub id: String,
#[serde(default)]
pub name: String,
#[serde(default)]
pub lat: f64,
#[serde(default)]
pub lon: f64,
pub category: Option<String>,
pub confidence: Option<f64>,
pub country: Option<String>,
pub region: Option<String>,
pub locality: Option<String>,
pub city: Option<String>,
pub brand: Option<String>,
pub distance_m: Option<f64>,
pub score: Option<f64>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct NearbyResponse {
#[serde(default)]
pub places: Vec<Place>,
pub center: Option<Coordinates>,
pub radius_m: Option<u32>,
#[serde(default)]
pub count: u32,
}
impl NearbyResponse {
pub fn is_empty(&self) -> bool {
self.places.is_empty()
}
pub fn len(&self) -> usize {
self.places.len()
}
pub fn iter(&self) -> impl Iterator<Item = &Place> {
self.places.iter()
}
}
impl IntoIterator for NearbyResponse {
type Item = Place;
type IntoIter = std::vec::IntoIter<Place>;
fn into_iter(self) -> Self::IntoIter {
self.places.into_iter()
}
}
impl<'a> IntoIterator for &'a NearbyResponse {
type Item = &'a Place;
type IntoIter = std::slice::Iter<'a, Place>;
fn into_iter(self) -> Self::IntoIter {
self.places.iter()
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct SearchResponse {
#[serde(default)]
pub places: Vec<Place>,
pub query: Option<String>,
#[serde(default)]
pub count: u32,
}
impl SearchResponse {
pub fn is_empty(&self) -> bool {
self.places.is_empty()
}
pub fn len(&self) -> usize {
self.places.len()
}
pub fn iter(&self) -> impl Iterator<Item = &Place> {
self.places.iter()
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct AutosuggestResponse {
#[serde(default, alias = "results")]
pub suggestions: Vec<Place>,
pub query: Option<String>,
#[serde(default)]
pub count: u32,
}
impl AutosuggestResponse {
pub fn is_empty(&self) -> bool {
self.suggestions.is_empty()
}
pub fn len(&self) -> usize {
self.suggestions.len()
}
pub fn iter(&self) -> impl Iterator<Item = &Place> {
self.suggestions.iter()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Category {
pub category: String,
#[serde(default)]
pub count: u64,
pub osm_tag: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CategoriesResponse {
#[serde(default)]
pub categories: Vec<Category>,
#[serde(default)]
pub count: u32,
}
impl CategoriesResponse {
pub fn is_empty(&self) -> bool {
self.categories.is_empty()
}
pub fn len(&self) -> usize {
self.categories.len()
}
pub fn iter(&self) -> impl Iterator<Item = &Category> {
self.categories.iter()
}
}
#[derive(Debug, Deserialize)]
pub(crate) struct FeatureCollection {
#[serde(default)]
features: Vec<Feature>,
}
#[derive(Debug, Deserialize)]
struct Feature {
geometry: Option<Geometry>,
#[serde(default)]
properties: GeocodingProperties,
}
#[derive(Debug, Deserialize)]
struct Geometry {
coordinates: Option<[f64; 2]>,
}
#[derive(Debug, Default, Deserialize)]
struct GeocodingProperties {
name: Option<String>,
country: Option<String>,
state: Option<String>,
city: Option<String>,
postcode: Option<String>,
street: Option<String>,
housenumber: Option<String>,
osm_key: Option<String>,
osm_value: Option<String>,
}
impl From<FeatureCollection> for GeocodingResponse {
fn from(collection: FeatureCollection) -> Self {
let results = collection
.features
.into_iter()
.filter_map(|feature| {
let coordinates = feature.geometry?.coordinates?;
Some(GeocodingResult {
lat: coordinates[1],
lon: coordinates[0],
name: feature.properties.name,
country: feature.properties.country,
state: feature.properties.state,
city: feature.properties.city,
postcode: feature.properties.postcode,
street: feature.properties.street,
housenumber: feature.properties.housenumber,
osm_key: feature.properties.osm_key,
osm_value: feature.properties.osm_value,
})
})
.collect();
Self { results }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn converts_geojson_features_to_geocoding_results() {
let collection: FeatureCollection = serde_json::from_value(serde_json::json!({
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [13.3888599, 52.5170365]
},
"properties": {
"name": "Berlin",
"country": "Germany",
"state": "Berlin",
"osm_key": "place",
"osm_value": "city"
}
}
]
}))
.expect("valid feature collection");
let response = GeocodingResponse::from(collection);
assert_eq!(response.len(), 1);
let first = response.first().expect("first result");
assert_eq!(first.name.as_deref(), Some("Berlin"));
assert_eq!(first.lat, 52.5170365);
assert_eq!(first.lon, 13.3888599);
assert_eq!(first.osm_key.as_deref(), Some("place"));
}
#[test]
fn deserializes_nearby_response() {
let response: NearbyResponse = serde_json::from_value(serde_json::json!({
"type": "nearby",
"center": { "lat": 40.748, "lon": -73.985 },
"radius_m": 500,
"count": 1,
"places": [
{
"id": "abc123",
"name": "Empire State Building",
"category": "attraction",
"lat": 40.7484,
"lon": -73.9857,
"confidence": 0.95,
"country": "US",
"region": "New York",
"locality": "Manhattan",
"distance_m": 42
}
]
}))
.expect("valid nearby response");
assert_eq!(response.count, 1);
assert_eq!(response.radius_m, Some(500));
assert_eq!(
response.center.as_ref().map(|center| center.lat),
Some(40.748)
);
let place = response.places.first().expect("first place");
assert_eq!(place.name, "Empire State Building");
assert_eq!(place.category.as_deref(), Some("attraction"));
assert_eq!(place.distance_m, Some(42.0));
}
#[test]
fn deserializes_categories_response_without_counts() {
let response: CategoriesResponse = serde_json::from_value(serde_json::json!({
"count": 1,
"source": "osm_tags",
"categories": [
{ "category": "restaurant", "osm_tag": "amenity:restaurant" }
]
}))
.expect("valid categories response");
assert_eq!(response.count, 1);
let category = response.categories.first().expect("first category");
assert_eq!(category.category, "restaurant");
assert_eq!(category.count, 0);
assert_eq!(category.osm_tag.as_deref(), Some("amenity:restaurant"));
}
#[test]
fn deserializes_autosuggest_results_alias() {
let response: AutosuggestResponse = serde_json::from_value(serde_json::json!({
"query": "eiff",
"count": 1,
"results": [
{
"id": "eiffel",
"name": "Tour Eiffel",
"category": "attraction",
"lat": 48.8582599,
"lon": 2.2945006,
"city": "Paris",
"country": "fr"
}
]
}))
.expect("valid autosuggest response");
assert_eq!(response.len(), 1);
let first = response.suggestions.first().expect("first suggestion");
assert_eq!(first.name, "Tour Eiffel");
assert_eq!(first.city.as_deref(), Some("Paris"));
}
}