ngrok_api/
lib.rs

1#![allow(clippy::too_long_first_doc_paragraph)]
2
3use url::Url;
4
5mod clients;
6mod errors;
7pub mod types;
8
9pub use clients::*;
10pub use errors::*;
11
12/// `ClientBuilder` is a builder for [Client]
13pub struct ClientBuilder {
14    // required options
15    api_key: String,
16
17    // optional
18    api_url: Option<Url>,
19    reqwest_client: Option<reqwest::Client>,
20}
21
22/// `ClientBuilder` allows customizing various options for the [Client]
23impl ClientBuilder {
24    /// Construct a builder to customize and then build the client.
25    pub fn new(api_key: String) -> ClientBuilder {
26        ClientBuilder {
27            api_key,
28            api_url: None,
29            reqwest_client: None,
30        }
31    }
32
33    /// Set an API url to connect to. By default, the public ngrok API will be used.
34    pub fn api_url(&mut self, api_url: Url) -> &mut Self {
35        self.api_url = Some(api_url);
36        self
37    }
38
39    /// Set a custom reqwest client to use for http requests to the ngrok API.
40    pub fn reqwest_client(&mut self, client: reqwest::Client) -> &mut Self {
41        self.reqwest_client = Some(client);
42        self
43    }
44
45    /// Build the client, applying any options set.
46    pub fn build(self) -> Client {
47        Client {
48            api_key: self.api_key,
49            api_url: self
50                .api_url
51                .unwrap_or_else(|| Url::parse("https://api.ngrok.com").unwrap()),
52            c: self.reqwest_client.unwrap_or_default(),
53        }
54    }
55}
56
57/// An ngrok API client.
58#[derive(Clone, Debug)]
59pub struct Client {
60    /// The ngrok API Key to authenticate with. See the [API documentat](https://ngrok.com/docs/api) for more information on creating an ngrok API key.
61    api_key: String,
62    /// The API URL base, such as `"https://api.ngrok.com"`.
63    api_url: Url,
64    c: reqwest::Client,
65}
66
67impl Client {
68    /// Create a new API client
69    ///
70    /// # Examples
71    ///
72    /// ```
73    /// use ngrok_api::Client;
74    ///
75    /// let c = Client::new("EXAMPLETOKEN123456789012345_EXAMPLESECRETIIIIIIIV".to_string());
76    /// // use methods like 'c.reserved_domains().list(...).await' to make API calls.
77    /// ```
78    pub fn new(api_key: String) -> Self {
79        ClientBuilder::new(api_key).build()
80    }
81
82    pub(crate) async fn make_request<T, R>(
83        &self,
84        path: &str,
85        method: reqwest::Method,
86        req: Option<T>,
87    ) -> Result<R, Error>
88    where
89        T: serde::Serialize,
90        R: serde::de::DeserializeOwned + Default,
91    {
92        let api_url = &self.api_url;
93
94        let mut builder = self
95            .c
96            .request(method.clone(), api_url.join(path).unwrap())
97            .bearer_auth(&self.api_key)
98            .header("Ngrok-Version", "2");
99        if let Some(r) = req {
100            // get requests use query strings instead of bodies
101            builder = match method {
102                reqwest::Method::GET => builder.query(&r),
103                _ => builder.json(&r),
104            };
105        }
106
107        let resp = builder.send().await?;
108
109        match resp.status() {
110            reqwest::StatusCode::NO_CONTENT => return Ok(Default::default()),
111            s if s.is_success() => {
112                return resp.json().await.map_err(|e| e.into());
113            }
114            _ => {}
115        }
116
117        // if we got an error status, see if it fits into an ngrok error, and then if not return it
118        // Unfortunately, that means we have to buffer it so we can try both
119        let resp_bytes = resp.bytes().await?;
120        if let Ok(e) = serde_json::from_slice(&resp_bytes) {
121            // recognized ngrok error
122            return Err(Error::Ngrok(e));
123        }
124        // ¯\_(ツ)_/¯
125        Err(Error::UnknownError(
126            String::from_utf8_lossy(&resp_bytes).into(),
127        ))
128    }
129
130    pub(crate) async fn get_by_uri<R>(&self, uri: &str) -> Result<R, Error>
131    where
132        R: serde::de::DeserializeOwned,
133    {
134        let builder = self
135            .c
136            .request(reqwest::Method::GET, uri)
137            .bearer_auth(&self.api_key)
138            .header("Ngrok-Version", "2");
139
140        let resp = builder.send().await?;
141        if resp.status().is_success() {
142            return resp.json().await.map_err(|e| e.into());
143        }
144        let resp_bytes = resp.bytes().await?;
145        if let Ok(e) = serde_json::from_slice(&resp_bytes) {
146            return Err(Error::Ngrok(e));
147        }
148        Err(Error::UnknownError(
149            String::from_utf8_lossy(&resp_bytes).into(),
150        ))
151    }
152}