1use std::io;
2
3use derivative::Derivative;
4use serde::de::DeserializeOwned;
5use ureq::OrAnyStatus;
6
7pub mod api;
8
9use crate::api::{ApiResponse, GeoCoords};
10pub use crate::api::{AvailableLanguages, Coords};
11
12#[derive(Debug, thiserror::Error)]
13#[allow(clippy::large_enum_variant)]
14pub enum Error {
15 #[error("API returned an error: {0:?}")]
16 Api(crate::api::ErrorResponse),
17 #[error("HTTP transport error")]
18 HttpTransport(#[from] ureq::Transport),
19 #[error("JSON error")]
20 Json(#[from] io::Error),
21 #[error("URL error")]
22 Url(#[from] url::ParseError),
23}
24
25#[derive(Derivative)]
26#[derivative(Debug)]
27pub struct Client {
28 #[derivative(Debug = "ignore")]
29 key: String,
30 pub base_url: url::Url,
31}
32
33impl Client {
34 pub fn new(key: &str) -> Self {
35 let client = Self {
36 key: key.to_string(),
37 base_url: url::Url::parse("https://api.what3words.com/v3/").unwrap(),
38 };
39 tracing::debug!(%client.base_url, "creating new client");
40 client
41 }
42
43 #[tracing::instrument(skip(self))]
44 fn prepare_request(&self, path: &str) -> Result<ureq::Request, Error> {
45 let url = self.base_url.join(path)?;
46 let request = ureq::get(url.as_str()).query("format", "json");
47 tracing::trace!(url = ?request.url());
48 Ok(request.query("key", &self.key))
49 }
50
51 #[tracing::instrument(skip(self, request), err)]
52 fn send<T: DeserializeOwned>(&self, request: ureq::Request) -> Result<ApiResponse<T>, Error> {
53 let response = request
54 .call()
55 .or_any_status()
56 .map_err(Error::HttpTransport)?;
57 let (status, status_text) = (response.status(), response.status_text());
58 tracing::trace!(response.status = status, response.status_text = status_text);
59 response.into_json().map_err(Error::Json)
60 }
61
62 #[tracing::instrument(skip(self), err)]
63 pub fn convert_to_coordinates(&self, words: &str) -> Result<Coords, Error> {
64 let request = self
65 .prepare_request("convert-to-coordinates")?
66 .query("words", words);
67 self.send(request)?.into()
68 }
69
70 #[tracing::instrument(skip(self), err)]
71 pub fn convert_to_3wa(&self, coordinates: &GeoCoords) -> Result<Coords, Error> {
72 let request = self
73 .prepare_request("convert-to-3wa")?
74 .query("coordinates", &coordinates.to_string());
75 self.send(request)?.into()
76 }
77
78 #[tracing::instrument(skip(self), err)]
79 pub fn available_languages(&self) -> Result<AvailableLanguages, Error> {
80 let request = self.prepare_request("available-languages")?;
81 self.send(request)?.into()
82 }
83}