1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
use std::io;

use derivative::Derivative;
use serde::de::DeserializeOwned;
use ureq::OrAnyStatus;

pub mod api;

use crate::api::{ApiResponse, GeoCoords};
pub use crate::api::{AvailableLanguages, Coords};

#[derive(Debug, thiserror::Error)]
#[allow(clippy::large_enum_variant)]
pub enum Error {
    #[error("API returned an error: {0:?}")]
    Api(crate::api::ErrorResponse),
    #[error("HTTP transport error")]
    HttpTransport(#[from] ureq::Transport),
    #[error("JSON error")]
    Json(#[from] io::Error),
    #[error("URL error")]
    Url(#[from] url::ParseError),
}

#[derive(Derivative)]
#[derivative(Debug)]
pub struct Client {
    #[derivative(Debug = "ignore")]
    key: String,
    pub base_url: url::Url,
}

impl Client {
    pub fn new(key: &str) -> Self {
        let client = Self {
            key: key.to_string(),
            base_url: url::Url::parse("https://api.what3words.com/v3/").unwrap(),
        };
        tracing::debug!(%client.base_url, "creating new client");
        client
    }

    #[tracing::instrument(skip(self))]
    fn prepare_request(&self, path: &str) -> Result<ureq::Request, Error> {
        let url = self.base_url.join(path)?;
        let request = ureq::get(url.as_str()).query("format", "json");
        tracing::trace!(url = ?request.url());
        Ok(request.query("key", &self.key))
    }

    #[tracing::instrument(skip(self, request), err)]
    fn send<T: DeserializeOwned>(&self, request: ureq::Request) -> Result<ApiResponse<T>, Error> {
        let response = request
            .call()
            .or_any_status()
            .map_err(Error::HttpTransport)?;
        let (status, status_text) = (response.status(), response.status_text());
        tracing::trace!(response.status = status, response.status_text = status_text);
        response.into_json().map_err(Error::Json)
    }

    #[tracing::instrument(skip(self), err)]
    pub fn convert_to_coordinates(&self, words: &str) -> Result<Coords, Error> {
        let request = self
            .prepare_request("convert-to-coordinates")?
            .query("words", words);
        self.send(request)?.into()
    }

    #[tracing::instrument(skip(self), err)]
    pub fn convert_to_3wa(&self, coordinates: &GeoCoords) -> Result<Coords, Error> {
        let request = self
            .prepare_request("convert-to-3wa")?
            .query("coordinates", &coordinates.to_string());
        self.send(request)?.into()
    }

    #[tracing::instrument(skip(self), err)]
    pub fn available_languages(&self) -> Result<AvailableLanguages, Error> {
        let request = self.prepare_request("available-languages")?;
        self.send(request)?.into()
    }
}