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}