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 fn default() -> Self {
22 Client::new(&"https://photon.komoot.io")
23 }
24}
25
26impl Client {
27 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 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 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(¶m, &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(¶m, &value);
190 }
191 }
192 request
193 }
194}