1use crate::models::{
2 autosuggest::{Autosuggest, AutosuggestResult, AutosuggestSelection},
3 error::ErrorResult,
4 gridsection::{BoundingBox, FormattedGridSection},
5 language::AvailableLanguages,
6 location::{ConvertTo3wa, ConvertToCoordinates, FormattedAddress},
7};
8use http::{HeaderMap, HeaderName, HeaderValue};
9use regex::Regex;
10#[cfg(feature = "sync")]
11use reqwest::blocking::Client;
12#[cfg(not(feature = "sync"))]
13use reqwest::Client;
14use serde::de::DeserializeOwned;
15use std::{collections::HashMap, env, fmt};
16
17pub(crate) trait Validator {
18 fn validate(&self) -> std::result::Result<(), Error>;
19}
20
21pub(crate) trait ToHashMap {
22 fn to_hash_map<'a>(&self) -> std::result::Result<HashMap<&'a str, String>, Error>;
23}
24
25#[derive(Debug)]
26pub enum Error {
27 Network(String),
28 Http(String),
29 Api(String, String),
30 Decode(String),
31 InvalidParameter(&'static str),
32 Unknown(String),
33}
34
35impl fmt::Display for Error {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match self {
38 Error::Network(msg) => write!(f, "Network error: {}", msg),
39 Error::Http(msg) => write!(f, "HTTP error: {}", msg),
40 Error::Api(code, message) => {
41 write!(f, "W3W error: {} {}", code, message)
42 }
43 Error::Decode(msg) => write!(f, "Decode error: {}", msg),
44 Error::InvalidParameter(msg) => write!(f, "Invalid input: {}", msg),
45 Error::Unknown(msg) => write!(f, "Unknown error: {}", msg),
46 }
47 }
48}
49
50impl std::error::Error for Error {}
51
52impl From<reqwest::Error> for Error {
53 fn from(error: reqwest::Error) -> Self {
54 if error.is_request() {
55 Error::Http(error.to_string())
56 } else if error.is_connect() {
57 Error::Network(error.to_string())
58 } else if error.is_decode() {
59 Error::Decode(error.to_string())
60 } else {
61 Error::Unknown(error.to_string())
62 }
63 }
64}
65
66pub(crate) type Result<T> = std::result::Result<T, Error>;
67
68const DEFAULT_W3W_API_BASE_URL: &str = "https://api.what3words.com/v3";
69const HEADER_WHAT3WORDS_API_KEY: &str = "X-Api-Key";
70const W3W_WRAPPER: &str = "X-W3W-Wrapper";
71
72pub struct What3words {
73 api_key: String,
74 host: String,
75 headers: HeaderMap,
76 user_agent: String,
77}
78
79impl What3words {
80 pub fn new(api_key: impl Into<String>) -> Self {
81 Self {
82 api_key: api_key.into(),
83 headers: HeaderMap::new(),
84 host: DEFAULT_W3W_API_BASE_URL.into(),
85 user_agent: format!(
86 "what3words-rust/{} ({})",
87 env!("CARGO_PKG_VERSION"),
88 env::consts::OS
89 ),
90 }
91 }
92
93 pub fn header<K, V>(mut self, key: K, value: V) -> Self
94 where
95 HeaderName: TryFrom<K>,
96 <HeaderName as TryFrom<K>>::Error: Into<http::Error>,
97 HeaderValue: TryFrom<V>,
98 <HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
99 {
100 if let (Ok(header_name), Ok(header_value)) =
101 (HeaderName::try_from(key), HeaderValue::try_from(value))
102 {
103 self.headers.insert(header_name, header_value);
104 }
105 self
106 }
107
108 pub fn hostname(mut self, host: impl Into<String>) -> Self {
109 self.host = host.into();
110 self
111 }
112
113 #[cfg(feature = "sync")]
114 pub fn convert_to_3wa<T: FormattedAddress + DeserializeOwned>(
115 &self,
116 options: &ConvertTo3wa,
117 ) -> Result<T> {
118 let url = format!("{}/convert-to-3wa", self.host);
119 let mut params = options.to_hash_map()?;
120 params.insert("format", T::format().to_string());
121 self.request(url, Some(params))
122 }
123
124 #[cfg(not(feature = "sync"))]
125 pub async fn convert_to_3wa<T: FormattedAddress + DeserializeOwned>(
126 &self,
127 options: &ConvertTo3wa,
128 ) -> Result<T> {
129 let url = format!("{}/convert-to-3wa", self.host);
130 let mut params = options.to_hash_map()?;
131 params.insert("format", T::format().to_string());
132 self.request(url, Some(params)).await
133 }
134
135 #[cfg(feature = "sync")]
136 pub fn convert_to_coordinates<T: FormattedAddress + DeserializeOwned>(
137 &self,
138 options: &ConvertToCoordinates,
139 ) -> Result<T> {
140 let url = format!("{}/convert-to-coordinates", self.host);
141 let mut params = options.to_hash_map()?;
142 params.insert("format", T::format().to_string());
143 self.request(url, Some(params))
144 }
145
146 #[cfg(not(feature = "sync"))]
147 pub async fn convert_to_coordinates<T: FormattedAddress + DeserializeOwned>(
148 &self,
149 options: &ConvertToCoordinates,
150 ) -> Result<T> {
151 let url = format!("{}/convert-to-coordinates", self.host);
152 let mut params = options.to_hash_map()?;
153 params.insert("format", T::format().to_string());
154 self.request(url, Some(params)).await
155 }
156
157 #[cfg(feature = "sync")]
158 pub fn available_languages(&self) -> Result<AvailableLanguages> {
159 let url = format!("{}/available-languages", self.host);
160 self.request(url, None)
161 }
162
163 #[cfg(not(feature = "sync"))]
164 pub async fn available_languages(&self) -> Result<AvailableLanguages> {
165 let url = format!("{}/available-languages", self.host);
166 self.request(url, None).await
167 }
168
169 #[cfg(feature = "sync")]
170 pub fn grid_section<T: DeserializeOwned + FormattedGridSection>(
171 &self,
172 bounding_box: &BoundingBox,
173 ) -> Result<T> {
174 let mut params = HashMap::new();
175 params.insert("bounding-box", bounding_box.to_string());
176 let url = format!("{}/grid-section", self.host);
177 params.insert("format", T::format().to_string());
178 self.request(url, Some(params))
179 }
180
181 #[cfg(not(feature = "sync"))]
182 pub async fn grid_section<T: DeserializeOwned + FormattedGridSection>(
183 &self,
184 bounding_box: &BoundingBox,
185 ) -> Result<T> {
186 let mut params = HashMap::new();
187 params.insert("bounding-box", bounding_box.to_string());
188 let url = format!("{}/grid-section", self.host);
189 params.insert("format", T::format().to_string());
190 self.request(url, Some(params)).await
191 }
192
193 #[cfg(feature = "sync")]
194 pub fn autosuggest(&self, autosuggest: &Autosuggest) -> Result<AutosuggestResult> {
195 let params = autosuggest.clone().to_hash_map()?;
196 let url = format!("{}/autosuggest", self.host);
197 self.request(url, Some(params))
198 }
199
200 #[cfg(not(feature = "sync"))]
201 pub async fn autosuggest(&self, autosuggest: &Autosuggest) -> Result<AutosuggestResult> {
202 let params = autosuggest.clone().to_hash_map()?;
203 let url = format!("{}/autosuggest", self.host);
204 self.request(url, Some(params)).await
205 }
206
207 #[cfg(feature = "sync")]
208 pub fn autosuggest_with_coordinates(
209 &self,
210 autosuggest: &Autosuggest,
211 ) -> Result<AutosuggestResult> {
212 let params = autosuggest.clone().to_hash_map()?;
213 let url = format!("{}/autosuggest-with-coordinates", self.host);
214 self.request(url, Some(params))
215 }
216
217 #[cfg(not(feature = "sync"))]
218 pub async fn autosuggest_with_coordinates(
219 &self,
220 autosuggest: &Autosuggest,
221 ) -> Result<AutosuggestResult> {
222 let params = autosuggest.clone().to_hash_map()?;
223 let url = format!("{}/autosuggest-with-coordinates", self.host);
224 self.request(url, Some(params)).await
225 }
226
227 #[cfg(feature = "sync")]
228 pub fn autosuggest_selection(&self, selection: &AutosuggestSelection) -> Result<()> {
229 let params = selection.to_hash_map()?;
230 let url = format!("{}/autosuggest-selection", self.host);
231 self.request(url, Some(params))
232 }
233
234 #[cfg(not(feature = "sync"))]
235 pub async fn autosuggest_selection(&self, selection: &AutosuggestSelection) -> Result<()> {
236 let params = selection.to_hash_map()?;
237 let url = format!("{}/autosuggest-selection", self.host);
238 self.request(url, Some(params)).await
239 }
240
241 #[cfg(feature = "sync")]
242 pub fn is_valid_3wa(&self, input: impl Into<String>) -> bool {
243 let input_str = input.into();
244 if self.is_possible_3wa(&input_str) {
245 if let Ok(suggestion) = self.autosuggest(&Autosuggest::new(&input_str).n_results("1")) {
246 return suggestion
247 .suggestions
248 .first()
249 .map_or(false, |suggestion| suggestion.words == input_str);
250 }
251 }
252 false
253 }
254
255 #[cfg(not(feature = "sync"))]
256 pub async fn is_valid_3wa(&self, input: impl Into<String>) -> bool {
257 let input_str = input.into();
258 if self.is_possible_3wa(&input_str) {
259 if let Ok(suggestion) = self
260 .autosuggest(&Autosuggest::new(&input_str).n_results("1"))
261 .await
262 {
263 return suggestion
264 .suggestions
265 .first()
266 .map_or(false, |suggestion| suggestion.words == input_str);
267 }
268 }
269 false
270 }
271
272 pub fn did_you_mean(&self, input: impl Into<String>) -> bool {
273 let pattern = Regex::new(
274 r#"^/?[^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?/;:£§º©®\s]{1,}[.\uFF61\u3002\uFF65\u30FB\uFE12\u17D4\u0964\u1362\u3002:။^_۔։ ,\\/+'&\\:;|\u3000-]{1,2}[^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?/;:£§º©®\s]{1,}[.\uFF61\u3002\uFF65\u30FB\uFE12\u17D4\u0964\u1362\u3002:။^_۔։ ,\\/+'&\\:;|\u3000-]{1,2}[^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?/;:£§º©®\s]{1,}$"#,
275 ).unwrap();
276 pattern.is_match(&input.into())
277 }
278
279 pub fn is_possible_3wa(&self, input: impl Into<String>) -> bool {
280 let pattern = Regex::new(
281 r#"^/*(?:[^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?/;:£§º©®\s]{1,}[.。。・・︒។։။۔።।][^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?/;:£§º©®\s]{1,}[.。。・・︒។։။۔።।][^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?/;:£§º©®\s]{1,}|[^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?/;:£§º©®\s]{1,}([\u0020\u00A0][^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?/;:£§º©®\s]+){1,3}[.。。・・︒។։။۔።।][^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?/;:£§º©®\s]{1,}([\u0020\u00A0][^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?/;:£§º©®\s]+){1,3}[.。。・・︒។։။۔።।][^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?/;:£§º©®\s]{1,}([\u0020\u00A0][^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?/;:£§º©®\s]+){1,3})$"#,
282 ).unwrap();
283 pattern.is_match(&input.into())
284 }
285
286 pub fn find_possible_3wa(&self, input: impl Into<String>) -> Vec<String> {
287 let pattern = Regex::new(
288 r#"[^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?/;:£§º©®\s]{1,}[.。。・・︒។։။۔።।][^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?/;:£§º©®\s]{1,}[.。。・・︒។։။۔።।][^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?/;:£§º©®\s]{1,}"#,
289 ).unwrap();
290 pattern
291 .find_iter(&input.into())
292 .map(|matched| matched.as_str().to_string())
293 .collect()
294 }
295
296 #[cfg(feature = "sync")]
297 fn request<T: DeserializeOwned>(
298 &self,
299 url: String,
300 params: Option<HashMap<&str, String>>,
301 ) -> Result<T> {
302 let response = Client::new()
303 .get(&url)
304 .query(¶ms)
305 .headers(self.headers.clone())
306 .header(W3W_WRAPPER, &self.user_agent)
307 .header(HEADER_WHAT3WORDS_API_KEY, &self.api_key)
308 .send()
309 .map_err(Error::from)?;
310
311 if !response.status().is_success() {
312 let error_response = response.json::<ErrorResult>().map_err(Error::from)?;
313 return Err(Error::Api(
314 error_response.error.code,
315 error_response.error.message,
316 ));
317 }
318 match response.content_length() {
319 Some(0) => Ok(serde_json::from_str("null").unwrap()),
321 _ => response.json::<T>().map_err(Error::from),
322 }
323 }
324
325 #[cfg(not(feature = "sync"))]
326 async fn request<T: DeserializeOwned>(
327 &self,
328 url: String,
329 params: Option<HashMap<&str, String>>,
330 ) -> Result<T> {
331 let response = Client::new()
332 .get(&url)
333 .query(¶ms)
334 .headers(self.headers.clone())
335 .header(W3W_WRAPPER, &self.user_agent)
336 .header(HEADER_WHAT3WORDS_API_KEY, &self.api_key)
337 .send()
338 .await
339 .map_err(Error::from)?;
340
341 if !response.status().is_success() {
342 let error_response = response.json::<ErrorResult>().await.map_err(Error::from)?;
343 return Err(Error::Api(
344 error_response.error.code,
345 error_response.error.message,
346 ));
347 }
348 match response.content_length() {
349 Some(0) => Ok(serde_json::from_str("null").unwrap()),
351 _ => response.json::<T>().await.map_err(Error::from),
352 }
353 }
354}
355
356#[cfg(test)]
357#[cfg(feature = "sync")]
358mod sync_tests {
359 use super::*;
360 use crate::{
361 models::{
362 autosuggest::Autosuggest,
363 location::{ConvertTo3wa, ConvertToCoordinates},
364 },
365 Address, AddressGeoJson, GridSection, Suggestion,
366 };
367
368 use mockito::{Matcher, Server};
369 use serde_json::json;
370
371 #[test]
372 fn test_custom_headers() {
373 let w3w = What3words::new("TEST_API_KEY").header("Custom-Header", "CustomValue");
374
375 assert_eq!(
376 w3w.headers.get("Custom-Header"),
377 Some(&HeaderValue::from_static("CustomValue"))
378 );
379 }
380
381 #[test]
382 fn test_custom_hostname() {
383 let w3w = What3words::new("TEST_API_KEY").hostname("https://custom.api.url");
384 assert_eq!(w3w.host, "https://custom.api.url");
385 }
386
387 #[test]
388 fn test_error_display() {
389 let network_error = Error::Network(String::from("Connection lost"));
390 assert_eq!(
391 format!("{}", network_error),
392 "Network error: Connection lost"
393 );
394
395 let http_error = Error::Http(String::from("404 Not Found"));
396 assert_eq!(format!("{}", http_error), "HTTP error: 404 Not Found");
397
398 let error_result = ErrorResult {
399 error: crate::models::error::Error {
400 code: String::from("400"),
401 message: String::from("Bad Request"),
402 },
403 };
404 let api_error = Error::Api(error_result.error.code, error_result.error.message);
405 assert_eq!(format!("{}", api_error), "W3W error: 400 Bad Request");
406
407 let decode_error = Error::Decode(String::from("Invalid JSON"));
408 assert_eq!(format!("{}", decode_error), "Decode error: Invalid JSON");
409
410 let unknown_error = Error::Unknown(String::from("Something went wrong"));
411 assert_eq!(
412 format!("{}", unknown_error),
413 "Unknown error: Something went wrong"
414 );
415 }
416
417 #[test]
418 fn test_convert_to_3wa() {
419 let words = "filled.count.soap";
420 let mut mock_server = Server::new();
421 let url = mock_server.url();
422 let mock = mock_server
423 .mock("GET", "/convert-to-3wa")
424 .match_query(mockito::Matcher::AllOf(vec![
425 Matcher::UrlEncoded("coordinates".into(), "51.521251,-0.203586".into()),
426 Matcher::UrlEncoded("format".into(), "json".into()),
427 ]))
428 .with_status(200)
429 .with_body(
430 json!({
431 "country": "GB",
432 "square": {
433 "southwest": {
434 "lng": -0.203607,
435 "lat": 51.521241
436 },
437 "northeast": {
438 "lng": -0.203575,
439 "lat": 51.521261
440 }
441 },
442 "nearestPlace": "Bayswater, London",
443 "coordinates": {
444 "lng": -0.203586,
445 "lat": 51.521251
446 },
447 "words": words,
448 "language": "en",
449 "map": format!("https://w3w.co/{}", words)
450 })
451 .to_string(),
452 )
453 .create();
454
455 let w3w = What3words::new("TEST_API_KEY").hostname(&url);
456 let result: Address = w3w
457 .convert_to_3wa(&ConvertTo3wa::new(51.521251, -0.203586))
458 .unwrap();
459 mock.assert();
460 assert_eq!(result.words, words);
461 }
462
463 #[test]
464 fn test_convert_to_coordinates() {
465 let words = "filled.count.soap";
466 let mut mock_server = Server::new();
467 let url = mock_server.url();
468 let mock = mock_server
469 .mock("GET", "/convert-to-coordinates")
470 .match_query(Matcher::AllOf(vec![
471 Matcher::UrlEncoded("words".into(), words.into()),
472 Matcher::UrlEncoded("format".into(), "json".into()),
473 ]))
474 .with_status(200)
475 .with_body(
476 json!({
477 "country": "GB",
478 "square": {
479 "southwest": {
480 "lng": -0.203607,
481 "lat": 51.521241
482 },
483 "northeast": {
484 "lng": -0.203575,
485 "lat": 51.521261
486 }
487 },
488 "nearestPlace": "Bayswater, London",
489 "coordinates": {
490 "lng": -0.203586,
491 "lat": 51.521251
492 },
493 "words": words,
494 "language": "en",
495 "map": format!("https://w3w.co/{}", words)
496 })
497 .to_string(),
498 )
499 .create();
500
501 let w3w = What3words::new("TEST_API_KEY").hostname(&url);
502 let result: Address = w3w
503 .convert_to_coordinates(&ConvertToCoordinates::new(words))
504 .unwrap();
505 mock.assert();
506 assert_eq!(result.coordinates.lng, -0.203586);
507 assert_eq!(result.coordinates.lat, 51.521251);
508 }
509
510 #[test]
511 fn test_convert_to_coordinates_bad_words() {
512 let bad_words = "filled.count";
513 let mut mock_server = Server::new();
514 let url = mock_server.url();
515 let mock = mock_server
516 .mock("GET", "/convert-to-coordinates")
517 .match_query(Matcher::AllOf(vec![
518 Matcher::UrlEncoded("words".into(), bad_words.into()),
519 Matcher::UrlEncoded("format".into(), "json".into()),
520 ]))
521 .with_status(400)
522 .with_body(
523 json!({
524 "error": {
525 "code": "BadWords",
526 "message": "words must be a valid 3 word address, such as filled.count.soap or ///filled.count.soap"
527 }
528 })
529 .to_string(),
530 )
531 .create();
532
533 let w3w = What3words::new("TEST_API_KEY").hostname(&url);
534 let result: std::result::Result<Address, Error> =
535 w3w.convert_to_coordinates::<Address>(&ConvertToCoordinates::new(bad_words));
536 mock.assert();
537 assert!(result.is_err());
538 let error = result.err().unwrap();
539 assert_eq!(format!("{}", error), "W3W error: BadWords words must be a valid 3 word address, such as filled.count.soap or ///filled.count.soap");
540 }
541
542 #[test]
543 fn test_convert_to_coordinates_with_locale() {
544 let words = "seruuhen.zemseg.dagaldah";
545 let mut mock_server = Server::new();
546 let url = mock_server.url();
547 let mock = mock_server
548 .mock("GET", "/convert-to-coordinates")
549 .match_query(Matcher::AllOf(vec![
550 Matcher::UrlEncoded("words".into(), words.into()),
551 Matcher::UrlEncoded("format".into(), "json".into()),
552 Matcher::UrlEncoded("locale".into(), "mn_la".into()),
553 ]))
554 .with_status(200)
555 .with_body(
556 json!({
557 "country": "GB",
558 "square": {
559 "southwest": {
560 "lng": -0.195543,
561 "lat": 51.520833
562 },
563 "northeast": {
564 "lng": -0.195499,
565 "lat": 51.52086
566 }
567 },
568 "nearestPlace": "Лондон",
569 "coordinates": {
570 "lng": -0.195521,
571 "lat": 51.520847
572 },
573 "words": words,
574 "language": "mn",
575 "locale": "mn_la",
576 "map": format!("https://w3w.co/{}", words),
577 })
578 .to_string(),
579 )
580 .create();
581
582 let w3w = What3words::new("TEST_API_KEY").hostname(&url);
583 let result: Address = w3w
584 .convert_to_coordinates(&ConvertToCoordinates::new(words).locale("mn_la"))
585 .unwrap();
586 mock.assert();
587 assert_eq!(result.words, words);
588 assert_eq!(result.locale, Some("mn_la".to_string()));
589 }
590
591 #[test]
592 fn test_convert_to_coordinates_geojson() {
593 let words = "filled.count.soap";
594 let mut mock_server = Server::new();
595 let url = mock_server.url();
596 let mock = mock_server
597 .mock("GET", "/convert-to-coordinates")
598 .match_query(Matcher::AllOf(vec![
599 Matcher::UrlEncoded("words".into(), words.into()),
600 Matcher::UrlEncoded("format".into(), "geojson".into()),
601 ]))
602 .with_status(200)
603 .with_body(
604 json!({
605 "features": [
606 {
607 "bbox": [
608 -0.195543,
609 51.520833,
610 -0.195499,
611 51.52086
612 ],
613 "geometry": {
614 "coordinates": [
615 -0.195521,
616 51.520847
617 ],
618 "type": "Point"
619 },
620 "type": "Feature",
621 "properties": {
622 "country": "GB",
623 "nearestPlace": "Bayswater, London",
624 "words": words,
625 "language": "en",
626 "map": format!("https://w3w.co/{}", words)
627 }
628 }
629 ],
630 "type": "FeatureCollection"
631 })
632 .to_string(),
633 )
634 .create();
635
636 let w3w = What3words::new("TEST_API_KEY").hostname(&url);
637 let result: AddressGeoJson = w3w
638 .convert_to_coordinates(&ConvertToCoordinates::new(words))
639 .unwrap();
640 mock.assert();
641 let bbox = result.features[0].bbox.as_ref().unwrap();
642 assert_eq!(bbox[0], -0.195543);
643 assert_eq!(bbox[1], 51.520833);
644 assert_eq!(bbox[2], -0.195499);
645 assert_eq!(bbox[3], 51.52086);
646 }
647
648 #[test]
649 fn test_available_languages() {
650 let mut mock_server = Server::new();
651 let url = mock_server.url();
652
653 let mock = mock_server
654 .mock("GET", "/available-languages")
655 .with_status(200)
656 .with_body(
657 json!({
658 "languages": [
659 {
660 "nativeName": "English",
661 "code": "en",
662 "name": "English"
663 },
664 {
665 "nativeName": "Français",
666 "code": "fr",
667 "name": "French"
668 }
669 ]
670 })
671 .to_string(),
672 )
673 .create();
674
675 let w3w = What3words::new("TEST_API_KEY").hostname(&url);
676 let result = w3w.available_languages().unwrap();
677 mock.assert();
678 assert_eq!(result.languages.len(), 2);
679 assert_eq!(result.languages[0].code, "en");
680 assert_eq!(result.languages[1].code, "fr");
681 }
682
683 #[test]
684 fn test_grid_section() {
685 let mut mock_server = Server::new();
686 let url = mock_server.url();
687 let mock = mock_server
688 .mock("GET", "/grid-section")
689 .match_query(Matcher::AllOf(vec![
690 Matcher::UrlEncoded(
691 "bounding-box".into(),
692 "52.207988,0.116126,52.208867,0.11754".into(),
693 ),
694 Matcher::UrlEncoded("format".into(), "json".into()),
695 ]))
696 .with_status(200)
697 .with_body(
698 json!({
699 "lines": [
700 {
701 "start": {
702 "lng": 0.116126,
703 "lat": 52.207988
704 },
705 "end": {
706 "lng": 0.11754,
707 "lat": 52.208867
708 }
709 }
710 ]
711 })
712 .to_string(),
713 )
714 .create();
715
716 let w3w = What3words::new("TEST_API_KEY").hostname(&url);
717 let result: GridSection = w3w
718 .grid_section(&BoundingBox::new(52.207988, 0.116126, 52.208867, 0.11754))
719 .unwrap();
720 mock.assert();
721 assert_eq!(result.lines.len(), 1);
722 }
723
724 #[test]
725 fn test_autosuggest() {
726 let mut mock_server = Server::new();
727 let url = mock_server.url();
728 let mock = mock_server
729 .mock("GET", "/autosuggest")
730 .match_query(Matcher::AllOf(vec![Matcher::UrlEncoded(
731 "input".into(),
732 "filled.count.soap".into(),
733 )]))
734 .with_status(200)
735 .with_body(
736 json!({
737 "suggestions": [
738 {
739 "country": "GB",
740 "nearestPlace": "Bayswater, London",
741 "words": "filled.count.soap",
742 "rank": 1,
743 "language": "en"
744 }
745 ]
746 })
747 .to_string(),
748 )
749 .create();
750
751 let w3w = What3words::new("TEST_API_KEY").hostname(&url);
752 let result = w3w
753 .autosuggest(&Autosuggest::new("filled.count.soap"))
754 .unwrap();
755 mock.assert();
756 assert_eq!(result.suggestions.len(), 1);
757 assert_eq!(result.suggestions[0].words, "filled.count.soap");
758 }
759
760 #[test]
761 fn test_autosuggest_with_coordinates() {
762 let mut mock_server = Server::new();
763 let url = mock_server.url();
764 let mock = mock_server
765 .mock("GET", "/autosuggest-with-coordinates")
766 .match_query(Matcher::AllOf(vec![Matcher::UrlEncoded(
767 "input".into(),
768 "filled.count.soap".into(),
769 )]))
770 .with_status(200)
771 .with_body(
772 json!({
773 "suggestions": [
774 {
775 "country": "GB",
776 "nearestPlace": "Bayswater, London",
777 "words": "filled.count.soap",
778 "rank": 1,
779 "language": "en"
780 }
781 ]
782 })
783 .to_string(),
784 )
785 .create();
786
787 let w3w = What3words::new("TEST_API_KEY").hostname(&url);
788 let result = w3w
789 .autosuggest_with_coordinates(&Autosuggest::new("filled.count.soap"))
790 .unwrap();
791
792 mock.assert();
793 assert_eq!(result.suggestions.len(), 1);
794 assert_eq!(result.suggestions[0].words, "filled.count.soap");
795 }
796
797 #[test]
798 fn test_autosuggest_selection() {
799 let mut mock_server = Server::new();
800 let url = mock_server.url();
801 let mock = mock_server
802 .mock("GET", "/autosuggest-selection")
803 .match_query(Matcher::AllOf(vec![
804 Matcher::UrlEncoded("selection".into(), "filled.count.soap".into()),
805 Matcher::UrlEncoded("rank".into(), "1".into()),
806 Matcher::UrlEncoded("raw-input".into(), "i.h.r".into()),
807 ]))
808 .with_status(200)
809 .create();
810
811 let w3w = What3words::new("TEST_API_KEY").hostname(&url);
812 let suggestion = Suggestion {
813 words: "filled.count.soap".to_string(),
814 country: "GB".to_string(),
815 nearest_place: "Bayswater, London".to_string(),
816 distance_to_focus_km: None,
817 rank: 1,
818 square: None,
819 coordinates: None,
820 language: "en".to_string(),
821 map: None,
822 };
823 let result = w3w.autosuggest_selection(&AutosuggestSelection::new("i.h.r", &suggestion));
824 mock.assert();
825 assert!(result.is_ok());
826 }
827
828 #[test]
829 fn test_is_valid_3wa_true() {
830 let words = "filled.count.soap";
831 let mut mock_server = Server::new();
832 let url = mock_server.url();
833
834 let mock = mock_server
835 .mock("GET", "/autosuggest")
836 .match_query(Matcher::AllOf(vec![
837 Matcher::UrlEncoded("input".into(), words.into()),
838 Matcher::UrlEncoded("n-results".into(), "1".into()),
839 ]))
840 .with_status(200)
841 .with_body(
842 json!({
843 "suggestions": [
844 {
845 "country": "GB",
846 "nearestPlace": "Bayswater, London",
847 "words": "filled.count.soap",
848 "rank": 1,
849 "language": "en"
850 }
851 ]
852 })
853 .to_string(),
854 )
855 .create();
856
857 let w3w: What3words = What3words::new("TEST_API_KEY").hostname(&url);
858 assert!(w3w.is_valid_3wa(words));
859 mock.assert();
860 }
861
862 #[test]
863 fn test_is_valid_3wa_false() {
864 let words = "filled.count";
865 let w3w: What3words = What3words::new("TEST_API_KEY");
866 assert!(!w3w.is_valid_3wa(words));
867 }
868
869 #[test]
870 fn test_is_valid_3wa_false_doesnt_match() {
871 let words = "rust.is.cool";
872 let mut mock_server = Server::new();
873 let url = mock_server.url();
874
875 let mock = mock_server
876 .mock("GET", "/autosuggest")
877 .match_query(Matcher::AllOf(vec![
878 Matcher::UrlEncoded("input".into(), words.into()),
879 Matcher::UrlEncoded("n-results".into(), "1".into()),
880 ]))
881 .with_status(200)
882 .with_body(
883 json!({
884 "suggestions": [
885 {
886 "country": "US",
887 "nearestPlace": "Huntington Station, New York",
888 "words": "rust.this.cool",
889 "rank": 1,
890 "language": "en"
891 }
892 ]
893 })
894 .to_string(),
895 )
896 .create();
897
898 let w3w: What3words = What3words::new("TEST_API_KEY").hostname(&url);
899 assert!(!w3w.is_valid_3wa(words));
900 mock.assert();
901 }
902
903 #[test]
904 fn test_did_you_mean_true() {
905 let w3w = What3words::new("TEST_API_KEY");
906 assert!(w3w.did_you_mean("filled。count。soap"));
907 assert!(w3w.did_you_mean("filled count soap"));
908 }
909
910 #[test]
911 fn test_did_you_mean_false() {
912 let w3w = What3words::new("TEST_API_KEY");
913 assert!(!w3w.did_you_mean("filledcountsoap"));
914 }
915
916 #[test]
917 fn test_is_possible_3wa_true() {
918 let w3w = What3words::new("TEST_API_KEY");
919 assert!(w3w.is_possible_3wa("filled.count.soap"));
920 }
921
922 #[test]
923 fn test_is_possible_3wa_false() {
924 let w3w = What3words::new("TEST_API_KEY");
925 assert!(!w3w.is_possible_3wa("filled count soap"));
926 }
927
928 #[test]
929 fn test_find_possible_3wa_true() {
930 let w3w = What3words::new("TEST_API_KEY");
931 let result = w3w.find_possible_3wa("This is a test with filled.count.soap in it.");
932 assert_eq!(result.len(), 1);
933 assert_eq!(result[0], "filled.count.soap");
934 }
935
936 #[test]
937 fn test_find_possible_3wa_false() {
938 let w3w = What3words::new("TEST_API_KEY");
939 let result = w3w.find_possible_3wa("This is a test with filled count soap in it.");
940 assert_eq!(result.len(), 0);
941 }
942}
943
944#[cfg(test)]
945#[cfg(not(feature = "sync"))]
946mod async_tests {
947 use super::*;
948 use crate::{
949 models::{
950 autosuggest::Autosuggest,
951 location::{ConvertTo3wa, ConvertToCoordinates},
952 },
953 Address, AddressGeoJson, GridSection, Suggestion,
954 };
955 use mockito::{Matcher, Server};
956 use serde_json::json;
957
958 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
959 async fn test_convert_to_3wa() {
960 let words = "filled.count.soap";
961 let mut mock_server = Server::new_async().await;
962 let url = mock_server.url();
963 let mock = mock_server
964 .mock("GET", "/convert-to-3wa")
965 .match_query(mockito::Matcher::AllOf(vec![
966 Matcher::UrlEncoded("coordinates".into(), "51.521251,-0.203586".into()),
967 Matcher::UrlEncoded("format".into(), "json".into()),
968 ]))
969 .with_status(200)
970 .with_body(
971 json!({
972 "country": "GB",
973 "square": {
974 "southwest": {
975 "lng": -0.203607,
976 "lat": 51.521241
977 },
978 "northeast": {
979 "lng": -0.203575,
980 "lat": 51.521261
981 }
982 },
983 "nearestPlace": "Bayswater, London",
984 "coordinates": {
985 "lng": -0.203586,
986 "lat": 51.521251
987 },
988 "words": words,
989 "language": "en",
990 "map": format!("https://w3w.co/{}", words)
991 })
992 .to_string(),
993 )
994 .create();
995
996 let w3w = What3words::new("TEST_API_KEY").hostname(&url);
997 let result: Address = w3w
998 .convert_to_3wa(&ConvertTo3wa::new(51.521251, -0.203586))
999 .await
1000 .unwrap();
1001 mock.assert_async().await;
1002 assert_eq!(result.words, "filled.count.soap");
1003 }
1004
1005 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1006 async fn test_convert_to_coordinates() {
1007 let words = "filled.count.soap";
1008 let mut mock_server = Server::new_async().await;
1009 let url = mock_server.url();
1010 let mock = mock_server
1011 .mock("GET", "/convert-to-coordinates")
1012 .match_query(Matcher::AllOf(vec![
1013 Matcher::UrlEncoded("words".into(), words.into()),
1014 Matcher::UrlEncoded("format".into(), "json".into()),
1015 ]))
1016 .with_status(200)
1017 .with_body(
1018 json!({
1019 "country": "GB",
1020 "square": {
1021 "southwest": {
1022 "lng": -0.203607,
1023 "lat": 51.521241
1024 },
1025 "northeast": {
1026 "lng": -0.203575,
1027 "lat": 51.521261
1028 }
1029 },
1030 "nearestPlace": "Bayswater, London",
1031 "coordinates": {
1032 "lng": -0.203586,
1033 "lat": 51.521251
1034 },
1035 "words": words,
1036 "language": "en",
1037 "map": format!("https://w3w.co/{}", words)
1038 })
1039 .to_string(),
1040 )
1041 .create();
1042
1043 let w3w = What3words::new("TEST_API_KEY").hostname(&url);
1044 let result: Address = w3w
1045 .convert_to_coordinates(&ConvertToCoordinates::new(words))
1046 .await
1047 .unwrap();
1048 mock.assert_async().await;
1049 assert_eq!(result.coordinates.lng, -0.203586);
1050 assert_eq!(result.coordinates.lat, 51.521251);
1051 }
1052
1053 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1054 async fn test_convert_to_coordinates_bad_words() {
1055 let bad_words = "filled.count";
1056 let mut mock_server = Server::new_async().await;
1057 let url = mock_server.url();
1058 let mock = mock_server
1059 .mock("GET", "/convert-to-coordinates")
1060 .match_query(Matcher::AllOf(vec![
1061 Matcher::UrlEncoded("words".into(), bad_words.into()),
1062 Matcher::UrlEncoded("format".into(), "json".into()),
1063 ]))
1064 .with_status(400)
1065 .with_body(
1066 json!({
1067 "error": {
1068 "code": "BadWords",
1069 "message": "words must be a valid 3 word address, such as filled.count.soap or ///filled.count.soap"
1070 }
1071 })
1072 .to_string(),
1073 )
1074 .create();
1075
1076 let w3w = What3words::new("TEST_API_KEY").hostname(&url);
1077 let result: std::result::Result<Address, Error> = w3w
1078 .convert_to_coordinates::<Address>(&ConvertToCoordinates::new(bad_words))
1079 .await;
1080 mock.assert_async().await;
1081 assert!(result.is_err());
1082 let error = result.err().unwrap();
1083 assert_eq!(format!("{}", error), "W3W error: BadWords words must be a valid 3 word address, such as filled.count.soap or ///filled.count.soap");
1084 }
1085
1086 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1087 async fn test_convert_to_coordinates_geojson() {
1088 let mut mock_server = Server::new_async().await;
1089 let url = mock_server.url();
1090 let mock = mock_server
1091 .mock("GET", "/convert-to-coordinates")
1092 .match_query(Matcher::AllOf(vec![
1093 Matcher::UrlEncoded("words".into(), "filled.count.soap".into()),
1094 Matcher::UrlEncoded("format".into(), "geojson".into()),
1095 ]))
1096 .with_status(200)
1097 .with_body(
1098 json!({
1099 "features": [
1100 {
1101 "bbox": [
1102 -0.195543,
1103 51.520833,
1104 -0.195499,
1105 51.52086
1106 ],
1107 "geometry": {
1108 "coordinates": [
1109 -0.195521,
1110 51.520847
1111 ],
1112 "type": "Point"
1113 },
1114 "type": "Feature",
1115 "properties": {
1116 "country": "GB",
1117 "nearestPlace": "Bayswater, London",
1118 "words": "filled.count.soap",
1119 "language": "en",
1120 "map": "https://w3w.co/filled.count.soap"
1121 }
1122 }
1123 ],
1124 "type": "FeatureCollection"
1125 })
1126 .to_string(),
1127 )
1128 .create();
1129
1130 let w3w = What3words::new("TEST_API_KEY").hostname(&url);
1131 let result: AddressGeoJson = w3w
1132 .convert_to_coordinates(&ConvertToCoordinates::new("filled.count.soap"))
1133 .await
1134 .unwrap();
1135 mock.assert_async().await;
1136 let bbox = result.features[0].bbox.as_ref().unwrap();
1137 assert_eq!(bbox[0], -0.195543);
1138 assert_eq!(bbox[1], 51.520833);
1139 assert_eq!(bbox[2], -0.195499);
1140 assert_eq!(bbox[3], 51.52086);
1141 }
1142
1143 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1144 async fn test_available_languages() {
1145 let mut mock_server = Server::new_async().await;
1146 let url = mock_server.url();
1147
1148 let mock = mock_server
1149 .mock("GET", "/available-languages")
1150 .with_status(200)
1151 .with_body(
1152 json!({
1153 "languages": [
1154 {
1155 "nativeName": "English",
1156 "code": "en",
1157 "name": "English"
1158 },
1159 {
1160 "nativeName": "Français",
1161 "code": "fr",
1162 "name": "French"
1163 }
1164 ]
1165 })
1166 .to_string(),
1167 )
1168 .create();
1169
1170 let w3w = What3words::new("TEST_API_KEY").hostname(&url);
1171 let result = w3w.available_languages().await.unwrap();
1172 mock.assert_async().await;
1173 assert_eq!(result.languages.len(), 2);
1174 assert_eq!(result.languages[0].code, "en");
1175 assert_eq!(result.languages[1].code, "fr");
1176 }
1177
1178 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1179 async fn test_grid_section() {
1180 let mut mock_server = Server::new_async().await;
1181 let url = mock_server.url();
1182 let mock = mock_server
1183 .mock("GET", "/grid-section")
1184 .match_query(Matcher::AllOf(vec![
1185 Matcher::UrlEncoded(
1186 "bounding-box".into(),
1187 "52.207988,0.116126,52.208867,0.11754".into(),
1188 ),
1189 Matcher::UrlEncoded("format".into(), "json".into()),
1190 ]))
1191 .with_status(200)
1192 .with_body(
1193 json!({
1194 "lines": [
1195 {
1196 "start": {
1197 "lng": 0.116126,
1198 "lat": 52.207988
1199 },
1200 "end": {
1201 "lng": 0.11754,
1202 "lat": 52.208867
1203 }
1204 }
1205 ]
1206 })
1207 .to_string(),
1208 )
1209 .create();
1210
1211 let w3w = What3words::new("TEST_API_KEY").hostname(&url);
1212 let result: GridSection = w3w
1213 .grid_section(&BoundingBox::new(52.207988, 0.116126, 52.208867, 0.11754))
1214 .await
1215 .unwrap();
1216 mock.assert_async().await;
1217 assert_eq!(result.lines.len(), 1);
1218 }
1219
1220 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1221 async fn test_autosuggest() {
1222 let mut mock_server = Server::new_async().await;
1223 let url = mock_server.url();
1224 let mock = mock_server
1225 .mock("GET", "/autosuggest")
1226 .match_query(Matcher::AllOf(vec![Matcher::UrlEncoded(
1227 "input".into(),
1228 "filled.count.soap".into(),
1229 )]))
1230 .with_status(200)
1231 .with_body(
1232 json!({
1233 "suggestions": [
1234 {
1235 "country": "GB",
1236 "nearestPlace": "Bayswater, London",
1237 "words": "filled.count.soap",
1238 "rank": 1,
1239 "language": "en"
1240 }
1241 ]
1242 })
1243 .to_string(),
1244 )
1245 .create();
1246
1247 let w3w = What3words::new("TEST_API_KEY").hostname(&url);
1248 let result = w3w
1249 .autosuggest(&Autosuggest::new("filled.count.soap"))
1250 .await
1251 .unwrap();
1252 mock.assert_async().await;
1253 assert_eq!(result.suggestions.len(), 1);
1254 assert_eq!(result.suggestions[0].words, "filled.count.soap");
1255 }
1256
1257 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1258 async fn test_autosuggest_with_coordinates() {
1259 let mut mock_server = Server::new_async().await;
1260 let url = mock_server.url();
1261 let mock = mock_server
1262 .mock("GET", "/autosuggest-with-coordinates")
1263 .match_query(Matcher::AllOf(vec![Matcher::UrlEncoded(
1264 "input".into(),
1265 "filled.count.soap".into(),
1266 )]))
1267 .with_status(200)
1268 .with_body(
1269 json!({
1270 "suggestions": [
1271 {
1272 "country": "GB",
1273 "nearestPlace": "Bayswater, London",
1274 "words": "filled.count.soap",
1275 "rank": 1,
1276 "language": "en"
1277 }
1278 ]
1279 })
1280 .to_string(),
1281 )
1282 .create();
1283
1284 let w3w = What3words::new("TEST_API_KEY").hostname(&url);
1285 let result = w3w
1286 .autosuggest_with_coordinates(&Autosuggest::new("filled.count.soap"))
1287 .await
1288 .unwrap();
1289
1290 mock.assert_async().await;
1291 assert_eq!(result.suggestions.len(), 1);
1292 assert_eq!(result.suggestions[0].words, "filled.count.soap");
1293 }
1294
1295 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1296 async fn test_autosuggest_selection() {
1297 let mut mock_server = Server::new_async().await;
1298 let url = mock_server.url();
1299 let mock = mock_server
1300 .mock("GET", "/autosuggest-selection")
1301 .match_query(Matcher::AllOf(vec![
1302 Matcher::UrlEncoded("selection".into(), "filled.count.soap".into()),
1303 Matcher::UrlEncoded("rank".into(), "1".into()),
1304 Matcher::UrlEncoded("raw-input".into(), "i.h.r".into()),
1305 ]))
1306 .with_status(200)
1307 .create();
1308
1309 let w3w = What3words::new("TEST_API_KEY").hostname(&url);
1310 let suggestion = Suggestion {
1311 words: "filled.count.soap".to_string(),
1312 country: "GB".to_string(),
1313 nearest_place: "Bayswater, London".to_string(),
1314 distance_to_focus_km: None,
1315 rank: 1,
1316 square: None,
1317 coordinates: None,
1318 language: "en".to_string(),
1319 map: None,
1320 };
1321 let result = w3w
1322 .autosuggest_selection(&AutosuggestSelection::new("i.h.r", &suggestion))
1323 .await;
1324 mock.assert_async().await;
1325 assert!(result.is_ok());
1326 }
1327
1328 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1329 async fn test_is_valid_3wa_true() {
1330 let words = "filled.count.soap";
1331 let mut mock_server = Server::new_async().await;
1332 let url = mock_server.url();
1333
1334 let mock = mock_server
1335 .mock("GET", "/autosuggest")
1336 .match_query(Matcher::AllOf(vec![
1337 Matcher::UrlEncoded("input".into(), words.into()),
1338 Matcher::UrlEncoded("n-results".into(), "1".into()),
1339 ]))
1340 .with_status(200)
1341 .with_body(
1342 json!({
1343 "suggestions": [
1344 {
1345 "country": "GB",
1346 "nearestPlace": "Bayswater, London",
1347 "words": "filled.count.soap",
1348 "rank": 1,
1349 "language": "en"
1350 }
1351 ]
1352 })
1353 .to_string(),
1354 )
1355 .create();
1356
1357 let w3w: What3words = What3words::new("TEST_API_KEY").hostname(&url);
1358 assert!(w3w.is_valid_3wa(words).await);
1359 mock.assert_async().await;
1360 }
1361
1362 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1363 async fn test_is_valid_3wa_false() {
1364 let words = "filled.count";
1365 let w3w: What3words = What3words::new("TEST_API_KEY");
1366 assert!(!w3w.is_valid_3wa(words).await);
1367 }
1368
1369 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1370 async fn test_is_valid_3wa_false_doesnt_match() {
1371 let words = "rust.is.cool";
1372 let mut mock_server = Server::new_async().await;
1373 let url = mock_server.url();
1374
1375 let mock = mock_server
1376 .mock("GET", "/autosuggest")
1377 .match_query(Matcher::AllOf(vec![
1378 Matcher::UrlEncoded("input".into(), words.into()),
1379 Matcher::UrlEncoded("n-results".into(), "1".into()),
1380 ]))
1381 .with_status(200)
1382 .with_body(
1383 json!({
1384 "suggestions": [
1385 {
1386 "country": "US",
1387 "nearestPlace": "Huntington Station, New York",
1388 "words": "rust.this.cool",
1389 "rank": 1,
1390 "language": "en"
1391 }
1392 ]
1393 })
1394 .to_string(),
1395 )
1396 .create();
1397
1398 let w3w: What3words = What3words::new("TEST_API_KEY").hostname(&url);
1399 assert!(!w3w.is_valid_3wa(words).await);
1400 mock.assert();
1401 }
1402}