wait_for_them/
lib.rs

1//! Wait For Them library
2//!
3//! this library is used to asynchronously wait when
4//! sockets or http(s) services become ready.
5//!
6//! # Example
7//! ```no_run
8//! use wait_for_them::{ToCheck, wait_for_them};
9//!
10//! #[tokio::main]
11//! async fn main() {
12//!     let res = wait_for_them(
13//!         &[
14//!             ToCheck::HostnameAndPort("localhost".into(), 8080),
15//!             ToCheck::HttpOrHttpsUrl("https://example.com/".parse().unwrap()),
16//!         ],
17//!         Some(8000),  // 8 seconds
18//!         None,  // time tracker
19//!         true,  // silent
20//!     ).await;
21//! }
22//! ```
23
24mod scanner;
25
26static DOMAIN_REGEX: &str =
27    r"^(([a-zA-Z_\-]{1,63}\.)*?)*?([a-zA-Z_\-]{1,63})(\.[a-zA-Z_\-]{1,63})?$";
28
29/// Wrapper around items which are going to be checked
30///
31/// it may be parsed from string
32/// ```
33/// let checks: Result<Vec<wait_for_them::ToCheck>, _> = [
34///     "localhost:8000", "localhost:8080"
35/// ].iter().map(|e| e.parse()).collect();
36/// ```
37#[derive(Debug, PartialEq, Clone)]
38pub enum ToCheck {
39    /// Hostname or IP address e.g. `127.0.0.1:8080` or `localhost:80`
40    HostnameAndPort(String, u16),
41
42    #[cfg(feature = "http")]
43    #[allow(rustdoc::bare_urls)]
44    /// Url with https or http `https://www.example.com:8080/some/?x=0&y=1#frag`
45    HttpOrHttpsUrl(hyper::Uri),
46}
47
48impl std::fmt::Display for ToCheck {
49    #[cfg(feature = "http")]
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        match self {
52            Self::HostnameAndPort(domain, port) => format!("{domain}:{port}").fmt(f),
53            Self::HttpOrHttpsUrl(uri) => uri.fmt(f),
54        }
55    }
56
57    #[cfg(not(feature = "http"))]
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        match self {
60            Self::HostnameAndPort(domain, port) => format!("{}:{}", domain, port).fmt(f),
61        }
62    }
63}
64
65impl ToCheck {
66    fn from_host_and_port(domain_and_port: &str) -> Result<Self, String> {
67        let parts: Vec<String> = domain_and_port.split(':').map(String::from).collect();
68        if parts.len() != 2 {
69            return Err(format!(
70                "'{domain_and_port}' doesn't match <hostname>:<port> pattern"
71            ));
72        }
73
74        // check port
75        let port: u16 = parts[1]
76            .parse()
77            .map_err(|err| format!("'{domain_and_port}', port error: {err}"))?;
78
79        if port == 0 {
80            return Err("dynamic port number (0) can't be used here".into());
81        }
82
83        // check hostname
84        let hostname = parts[0].clone();
85        let regex = regex::Regex::new(DOMAIN_REGEX).unwrap();
86        let ip: Result<std::net::IpAddr, _> = hostname.parse();
87
88        if !regex.is_match(&hostname) && ip.is_err() {
89            return Err(format!("'{hostname}' is not a valid hostname"));
90        }
91        Ok(Self::HostnameAndPort(hostname, port))
92    }
93
94    #[cfg(feature = "http")]
95    fn from_http_url(http_url: &str) -> Result<Self, String> {
96        Ok(Self::HttpOrHttpsUrl(
97            http_url.parse::<hyper::Uri>().map_err(|e| e.to_string())?,
98        ))
99    }
100
101    #[cfg(not(feature = "http"))]
102    fn from_http_url(_uri: &str) -> Result<Self, String> {
103        panic!("Not compiled with 'http' feature")
104    }
105}
106
107impl std::str::FromStr for ToCheck {
108    type Err = String;
109
110    #[cfg(feature = "http")]
111    fn from_str(s: &str) -> Result<Self, Self::Err> {
112        if s.starts_with("http://") || s.starts_with("https://") {
113            Self::from_http_url(s)
114        } else {
115            Self::from_host_and_port(s)
116        }
117    }
118
119    #[cfg(not(feature = "http"))]
120    fn from_str(s: &str) -> Result<Self, Self::Err> {
121        Self::from_host_and_port(s)
122    }
123}
124
125/// Waits till all hostname and port combinations are opened
126/// or until `200` status code is returned from http(s) URLs.
127///
128/// # Arguments
129///
130/// * `hosts_ports_or_http_urls` - items to be check
131/// * `timeout` - `None` means that it it may wait forever or `Some(..)` set timeout in milis
132/// * `start_time` - Optional time_tracker
133/// * `silent` - suppresses output to console if true
134///
135/// # Returns
136/// `Vec` with `Option` - `Some(..)` with elapsed time in milis on success `None` otherwise.
137///
138pub async fn wait_for_them(
139    hosts_ports_or_http_urls: &[ToCheck],
140    timeout: Option<u64>,
141    start_time: Option<std::time::Instant>,
142    silent: bool,
143) -> Vec<Option<u64>> {
144    let start_time = start_time.unwrap_or_else(std::time::Instant::now);
145    let futures = if silent {
146        scanner::wait_silent(hosts_ports_or_http_urls, timeout, start_time)
147    } else {
148        scanner::wait(hosts_ports_or_http_urls, timeout, start_time)
149    };
150
151    futures::future::join_all(futures).await
152}