ipify_rs/
lib.rs

1//! Ipify
2//!
3//! My implementation of the ipify-cli.org API to get your own public IP address
4//!
5//! The fastest way to use it is to use the `myip()` wrapper:
6//!
7//! # Example:
8//!
9//! ```rust
10//! use ipify_rs::myip;
11//!
12//! println!("My IP is: {}", myip());
13//! ```
14//!
15//! The full API is described below.
16
17use clap::{crate_name, crate_version};
18use eyre::Result;
19
20/// IPv4 endpoint, plain text
21const ENDPOINT4: &str = "https://api.ipify.org";
22/// IPv6 endpoint, plain text
23const ENDPOINT6: &str = "https://api64.ipify.org";
24/// IPv4 endpoint, JSON
25const ENDPOINT4J: &str = "https://api.ipify.org?format=json";
26/// IPv6 endpoint, JSON
27const ENDPOINT6J: &str = "https://api64.ipify.org?format=json";
28
29/// Minimalistic API
30///
31/// Example:
32/// ```
33/// use ipify_rs::myip;
34///
35/// println!("{}", myip())
36/// ```
37///
38#[inline]
39pub fn myip() -> String {
40    Ipify::new().set(Op::IPv6).call().unwrap()
41}
42
43/// Enumeration for different types of operations provided by the Ipify API.
44///
45/// Each variant corresponds to a specific operation or request type.
46///
47/// # Variants
48///
49/// * `IPv4` - Retrieves the IPv4 address in plain text format.
50/// * `IPv6` - Retrieves the IPv6 address in plain text format (default).
51/// * `IPv4J` - Retrieves the IPv4 address in JSON format.
52/// * `IPv6J` - Retrieves the IPv6 address in JSON format.
53///
54/// # Examples
55///
56/// ```
57/// use ipify_rs::{Ipify, Op};
58///
59/// let mut client = Ipify::new();
60/// client = client.set(Op::IPv4);
61///
62/// println!("Public IPv4 address: {}", client.call().unwrap());
63/// ```
64///
65#[derive(Clone, Copy, Debug, PartialEq)]
66pub enum Op {
67    /// Plain text
68    IPv4,
69    /// Plain text (default)
70    IPv6,
71    /// Json output
72    IPv4J,
73    /// Json output
74    IPv6J,
75}
76
77/// The main API struct
78///
79/// This struct represents a client for interacting with the Ipify API.
80/// It allows users to configure and perform operations for retrieving
81/// their public IP addresses, either in plain text or JSON format.
82///
83/// # Fields
84///
85/// * `t` - The current operation to perform (e.g., IPv4, IPv6, JSON outputs).
86/// * `endp` - The API endpoint used for the operation.
87///
88/// # Examples
89///
90/// Basic usage:
91///
92/// ```rust
93/// use ipify_rs::{Ipify, Op};
94///
95/// let mut client = Ipify::new();
96/// client = client.set(Op::IPv4);
97///
98/// let ip = client.call().unwrap();
99/// println!("Public IPv4 address: {}", ip);
100/// ```
101///
102/// Using the default settings (IPv6):
103///
104/// ```rust
105/// use ipify_rs::Ipify;
106///
107/// let ip = Ipify::new().call().unwrap();
108/// println!("Public IPv6 address: {}", ip);
109/// ```
110///
111#[derive(Clone, Debug)]
112pub struct Ipify {
113    /// Current type of operation
114    pub t: Op,
115    /// Endpoint, different for every operation
116    pub endp: String,
117}
118
119/// Impl. default values.
120impl Default for Ipify {
121    fn default() -> Self {
122        Self::new()
123    }
124}
125
126/// API Implementation
127impl Ipify {
128    /// Create a new API instance client with the defaults
129    ///
130    /// # Example:
131    /// ```
132    /// use ipify_rs::*;
133    ///
134    /// let a = Ipify::new();
135    ///
136    /// println!("{}", a.call().unwrap());
137    /// ```
138    ///
139    pub fn new() -> Self {
140        Self {
141            t: Op::IPv6,
142            endp: ENDPOINT6.to_owned(),
143        }
144    }
145
146    /// Specify the subsequent operation to perform on `call()`
147    ///
148    /// # Example:
149    /// ```rust
150    /// # fn main() -> eyre::Result<()> {
151    ///     use ipify_rs::{Ipify, Op};
152    ///
153    ///     let mut a = Ipify::new();
154    ///     a.set(Op::IPv6J);
155    ///
156    ///     println!("{}", a.call()?);
157    /// #     Ok(())
158    /// # }
159    /// ```
160    ///
161    pub fn set(&self, op: Op) -> Self {
162        Self {
163            t: op,
164            endp: match op {
165                Op::IPv4 => ENDPOINT4.to_owned(),
166                Op::IPv6 => ENDPOINT6.to_owned(),
167                Op::IPv4J => ENDPOINT4J.to_owned(),
168                Op::IPv6J => ENDPOINT6J.to_owned(),
169            },
170        }
171    }
172
173    /// Actually perform the API call
174    ///
175    /// # Example:
176    /// ```rust
177    /// # fn main() -> eyre::Result<()> {
178    ///     use ipify_rs::Ipify;
179    ///
180    ///     let r = Ipify::new().call()?;
181    ///
182    ///     println!("my ip = {}", r);
183    /// # Ok(())
184    /// # }
185    /// ```
186    ///
187    pub fn call(self) -> Result<String> {
188        let c = reqwest::blocking::ClientBuilder::new()
189            .user_agent(format!("{}/{}", crate_name!(), crate_version!()))
190            .build()?;
191        Ok(c.get(self.endp).send()?.text()?)
192    }
193
194    ///
195    /// Perform the API call asynchronously to retrieve the IP address.
196    ///
197    /// This function communicates with the configured `Ipify` endpoint, sending an
198    /// HTTP GET request and retrieving the response body as a string. The result of
199    /// the call is typically a public IP address of the client in either plain text
200    /// or JSON format, based on the selected operation (`Op`).
201    ///
202    /// # Example
203    ///
204    /// ```rust
205    /// # use std::io::ErrorKind;
206    /// use ipify_rs::Ipify;
207    ///
208    /// # #[tokio::main]
209    /// # async fn main() -> eyre::Result<()> {
210    ///     let ip = Ipify::new().call_async().await?;
211    ///     println!("My public IP address: {}", ip);
212    /// #   Ok(())
213    /// # }
214    /// ```
215    ///
216    /// # Errors
217    ///
218    /// This function will panic if:
219    /// - The HTTP client fails to build properly (e.g., invalid user-agent).
220    /// - The GET request to the endpoint fails (e.g., network error or invalid endpoint).
221    /// - The response cannot be transformed into a plain string (e.g., invalid encoding).
222    ///
223    /// To avoid panics, consider handling errors explicitly by using a custom implementation that propagates errors instead of unwrapping results.
224    ///
225    pub async fn call_async(self) -> Result<String> {
226        let c = reqwest::ClientBuilder::new()
227            .user_agent(format!("{}/{}", crate_name!(), crate_version!()))
228            .build()?;
229        Ok(c.get(self.endp).send().await?.text().await?)
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use httpmock::prelude::*;
237    use std::net::IpAddr;
238
239    #[test]
240    fn test_set_1() {
241        let c = Ipify::new();
242
243        assert_eq!(Op::IPv6, c.t);
244        let c = c.set(Op::IPv4J);
245        assert_eq!(Op::IPv4J, c.t);
246        let c = c.set(Op::IPv6);
247        assert_eq!(Op::IPv6, c.t);
248    }
249
250    #[test]
251    fn test_set_2() {
252        let c = Ipify::new();
253
254        let c = c.set(Op::IPv4J).set(Op::IPv6J);
255        assert_eq!(Op::IPv6J, c.t);
256    }
257
258    #[test]
259    fn test_with_1() {
260        let c = Ipify::new();
261
262        assert_eq!(Op::IPv6, c.t);
263    }
264
265    #[test]
266    fn test_with_set() {
267        let c = Ipify::new();
268
269        assert_eq!(Op::IPv6, c.t);
270        let c = c.set(Op::IPv4);
271        assert_eq!(Op::IPv4, c.t);
272
273        let c = c.set(Op::IPv4J);
274        assert_eq!(Op::IPv4J, c.t);
275    }
276
277    #[test]
278    fn test_myip() {
279        let server = MockServer::start();
280
281        let m = server.mock(|when, then| {
282            when.method(GET).header(
283                "user-agent",
284                format!("{}/{}", crate_name!(), crate_version!()),
285            );
286            then.status(200).body("192.0.2.1");
287        });
288
289        let mut c = Ipify::new();
290        let b = server.base_url().clone();
291        c.endp = b.to_owned();
292        let str = c.call();
293        assert!(str.is_ok());
294        let str = str.unwrap();
295
296        let ip = str.parse::<IpAddr>();
297        m.assert();
298        assert!(ip.is_ok());
299        assert_eq!("192.0.2.1", str);
300    }
301
302    #[tokio::test]
303    async fn test_async_call() {
304        let server = MockServer::start_async().await;
305
306        let m = server
307            .mock_async(|when, then| {
308                when.method(GET).header(
309                    "user-agent",
310                    format!("{}/{}", crate_name!(), crate_version!()),
311                );
312                then.status(200).body("192.0.2.1");
313            })
314            .await;
315
316        let mut c = Ipify::new();
317        let b = server.base_url().clone();
318        c.endp = b.to_owned();
319        let str = c.call_async().await;
320        assert!(str.is_ok());
321        let str = str.unwrap();
322
323        let ip = str.parse::<IpAddr>();
324        m.assert_async().await;
325        assert!(ip.is_ok());
326        assert_eq!("192.0.2.1", str);
327    }
328
329    #[test]
330    fn test_set_to_ipv4j() {
331        let c = Ipify::new().set(Op::IPv4J);
332        assert_eq!(Op::IPv4J, c.t);
333        assert_eq!(ENDPOINT4J, c.endp);
334    }
335
336    #[test]
337    fn test_set_to_ipv6() {
338        let c = Ipify::new().set(Op::IPv6);
339        assert_eq!(Op::IPv6, c.t);
340        assert_eq!(ENDPOINT6, c.endp);
341    }
342
343    #[test]
344    fn test_call_ipv4() {
345        let server = MockServer::start();
346
347        let m = server.mock(|when, then| {
348            when.method(GET);
349            then.status(200).body("203.0.113.1");
350        });
351
352        let mut c = Ipify::new().set(Op::IPv4);
353        c.endp = server.base_url();
354        let str = c.call();
355        assert!(str.is_ok());
356        let str = str.unwrap();
357
358        m.assert();
359        assert_eq!("203.0.113.1", str);
360    }
361
362    #[tokio::test]
363    async fn test_async_call_with_ipv6j() {
364        let server = MockServer::start_async().await;
365
366        let m = server
367            .mock_async(|when, then| {
368                when.method(GET);
369                then.status(200).body("{\"ip\":\"2001:db8::2\"}");
370            })
371            .await;
372
373        let mut c = Ipify::new().set(Op::IPv6J);
374        c.endp = server.base_url();
375        let str = c.call_async().await;
376        assert!(str.is_ok());
377        let str = str.unwrap();
378
379        m.assert_async().await;
380        assert_eq!("{\"ip\":\"2001:db8::2\"}", str);
381    }
382
383    #[test]
384    fn test_default_values() {
385        let c = Ipify::new();
386        assert_eq!(Op::IPv6, c.t);
387        assert_eq!(ENDPOINT6, c.endp);
388    }
389
390    #[test]
391    fn test_chaining_set_calls() {
392        let c = Ipify::new().set(Op::IPv4).set(Op::IPv4J).set(Op::IPv6);
393        assert_eq!(Op::IPv6, c.t);
394        assert_eq!(ENDPOINT6, c.endp);
395    }
396}