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                "'{}' doesn't match <hostname>:<port> pattern",
71                domain_and_port
72            ));
73        }
74
75        // check port
76        let port: u16 = parts[1]
77            .parse()
78            .map_err(|err| format!("'{}', port error: {}", domain_and_port, err))?;
79
80        if port == 0 {
81            return Err("dynamic port number (0) can't be used here".into());
82        }
83
84        // check hostname
85        let hostname = parts[0].clone();
86        let regex = regex::Regex::new(DOMAIN_REGEX).unwrap();
87        let ip: Result<std::net::IpAddr, _> = hostname.parse();
88
89        if !regex.is_match(&hostname) && ip.is_err() {
90            return Err(format!("'{}' is not a valid hostname", hostname));
91        }
92        Ok(Self::HostnameAndPort(hostname, port))
93    }
94
95    #[cfg(feature = "http")]
96    fn from_http_url(http_url: &str) -> Result<Self, String> {
97        Ok(Self::HttpOrHttpsUrl(
98            http_url.parse::<hyper::Uri>().map_err(|e| e.to_string())?,
99        ))
100    }
101
102    #[cfg(not(feature = "http"))]
103    fn from_http_url(_uri: &str) -> Result<Self, String> {
104        panic!("Not compiled with 'http' feature")
105    }
106}
107
108impl std::str::FromStr for ToCheck {
109    type Err = String;
110
111    #[cfg(feature = "http")]
112    fn from_str(s: &str) -> Result<Self, Self::Err> {
113        if s.starts_with("http://") || s.starts_with("https://") {
114            Self::from_http_url(s)
115        } else {
116            Self::from_host_and_port(s)
117        }
118    }
119
120    #[cfg(not(feature = "http"))]
121    fn from_str(s: &str) -> Result<Self, Self::Err> {
122        Self::from_host_and_port(s)
123    }
124}
125
126/// Waits till all hostname and port combinations are opened
127/// or until `200` status code is returned from http(s) URLs.
128///
129/// # Arguments
130///
131/// * `hosts_ports_or_http_urls` - items to be check
132/// * `timeout` - `None` means that it it may wait forever or `Some(..)` set timeout in milis
133/// * `start_time` - Optional time_tracker
134/// * `silent` - suppresses output to console if true
135///
136/// # Returns
137/// `Vec` with `Option` - `Some(..)` with elapsed time in milis on success `None` otherwise.
138///
139pub async fn wait_for_them(
140    hosts_ports_or_http_urls: &[ToCheck],
141    timeout: Option<u64>,
142    start_time: Option<std::time::Instant>,
143    silent: bool,
144) -> Vec<Option<u64>> {
145    let start_time = start_time.unwrap_or_else(std::time::Instant::now);
146    let futures = if silent {
147        scanner::wait_silent(hosts_ports_or_http_urls, timeout, start_time)
148    } else {
149        scanner::wait(hosts_ports_or_http_urls, timeout, start_time)
150    };
151
152    futures::future::join_all(futures).await
153}