photon_geocoding/
api.rs

1use std::error::Error;
2
3use serde::Deserialize;
4use ureq::{Agent, AgentBuilder, Request};
5
6use crate::data::filter::{ForwardFilter, ReverseFilter};
7use crate::data::json::PhotonFeatureCollection;
8use crate::data::{LatLon, PhotonFeature};
9use crate::error::PhotonError;
10
11type PhotonResult = Result<Vec<PhotonFeature>, Box<dyn Error>>;
12
13pub struct Client {
14    forward_url: String,
15    reverse_url: String,
16    client: Agent,
17}
18
19impl Default for Client {
20    /// Default Photon client, using https://photon.komoot.io for requests.
21    fn default() -> Self {
22        Client::new(&"https://photon.komoot.io")
23    }
24}
25
26impl Client {
27    /// Creates a new API client with the specified `base_url`.
28    /// 
29    /// `base_url` must begin with `http://` or `https://`.
30    pub fn new(base_url: &str) -> Self {
31        let mut base_url = base_url;
32        if base_url.ends_with("/") {
33            base_url = &base_url[..base_url.len() - 1]
34        }
35        Client {
36            forward_url: String::from(base_url) + "/api",
37            reverse_url: String::from(base_url) + "/reverse",
38            client: AgentBuilder::new().build(),
39        }
40    }
41
42    /// Performs a forward search for the provided `query`.
43    /// 
44    /// Results can be filtered by the optional `filter`. Pass `None` for no filter.
45    /// 
46    /// This function is blocking, so no async features are involved here. It is, however, safe to
47    /// call this function in parallel, since the entire API client is thread-safe.
48    pub fn forward_search(&self, query: &str, filter: Option<ForwardFilter>) -> PhotonResult {
49        let mut request = self.client.get(&self.forward_url).query("q", query);
50
51        if let Some(filter) = filter {
52            request = filter.append_to(request);
53        }
54
55        let response = request.call()?.into_json()?;
56
57        self.parse_response(response)
58    }
59
60    /// Performs a reverse search for objects at the specified `coords`.
61    /// 
62    /// Results can be filtered by the optional `filter`. Pass `None` for no filter.
63    /// 
64    /// This function is blocking, so no async features are involved here. It is, however, safe to
65    /// call this function in parallel, since the entire API client is thread-safe.
66    pub fn reverse_search(
67        &self,
68        coords: LatLon,
69        filter: Option<ReverseFilter>,
70    ) -> PhotonResult {
71        let mut request = self
72            .client
73            .get(&self.reverse_url)
74            .query("lon", &coords.lon.to_string())
75            .query("lat", &coords.lat.to_string());
76
77        if let Some(filter) = filter {
78            request = filter.append_to(request)
79        }
80
81        let response = request.call()?.into_json()?;
82
83        self.parse_response(response)
84    }
85
86    fn parse_response(&self, response: serde_json::Value) -> PhotonResult {
87        let deserialize_result = PhotonFeatureCollection::deserialize(&response);
88        match deserialize_result {
89            Ok(features) => {
90                return Ok(features
91                    .features()
92                    .into_iter()
93                    .map(PhotonFeature::from)
94                    .collect());
95            }
96            Err(error) => {
97                let message = self.try_parse_error(response);
98                match message {
99                    Some(error) => Err(Box::new(error)),
100                    None => Err(Box::new(error)),
101                }
102            }
103        }
104    }
105
106    fn try_parse_error(&self, response: serde_json::Value) -> Option<PhotonError> {
107        let message = response.get("message")?.to_string();
108        Some(PhotonError::new(&message))
109    }
110}
111
112#[test]
113fn test_base_url_trailing_slash() {
114    let base_url_with_trailing_slash = "https://example.com/";
115    let base_url_without_trailing_slash = "https://example.com";
116
117    let client_with = Client::new(base_url_with_trailing_slash);
118    let client_without = Client::new(base_url_without_trailing_slash);
119
120    assert_eq!(client_with.forward_url, client_without.forward_url);
121    assert_eq!(client_with.reverse_url, client_without.reverse_url);
122}
123
124pub trait RequestAppend {
125    fn append_to(self, request: Request) -> Request;
126}
127
128impl RequestAppend for ForwardFilter {
129    fn append_to(self, request: Request) -> Request {
130        let mut request = request;
131        if let Some(bias) = self.location_bias {
132            request = request
133                .query("lat", &bias.lat.to_string())
134                .query("lon", &bias.lon.to_string());
135
136            if let Some(zoom) = self.location_bias_zoom {
137                request = request.query("zoom", &zoom.to_string());
138            }
139            if let Some(scale) = self.location_bias_scale {
140                request = request.query("location_bias_scale", &scale.to_string());
141            }
142        }
143        if let Some(bbox) = self.bounding_box {
144            let format = format!(
145                "{},{},{},{}",
146                bbox.south_west.lon, bbox.south_west.lat, bbox.north_east.lon, bbox.north_east.lat
147            );
148            request = request.query("bbox", &format);
149        }
150        if let Some(limit) = self.limit {
151            request = request.query("limit", &limit.to_string());
152        }
153        if let Some(lang) = self.lang {
154            request = request.query("lang", &lang);
155        }
156        if let Some(layers) = self.layer {
157            for layer in layers {
158                request = request.query("layer", &layer.to_string());
159            }
160        }
161        if let Some(query) = self.additional_query {
162            for (param, value) in query {
163                request = request.query(&param, &value);
164            }
165        }
166        request
167    }
168}
169
170impl RequestAppend for ReverseFilter {
171    fn append_to(self, request: Request) -> Request {
172        let mut request = request;
173        if let Some(radius) = self.radius {
174            request = request.query("radius", &radius.to_string());
175        }
176        if let Some(limit) = self.limit {
177            request = request.query("limit", &limit.to_string());
178        }
179        if let Some(lang) = self.lang {
180            request = request.query("lang", &lang);
181        }
182        if let Some(layers) = self.layer {
183            for layer in layers {
184                request = request.query("layer", &layer.to_string());
185            }
186        }
187        if let Some(query) = self.additional_query {
188            for (param, value) in query {
189                request = request.query(&param, &value);
190            }
191        }
192        request
193    }
194}