waitup 1.1.1

Wait for TCP ports and HTTP endpoints to be available. Essential for Docker, K8s, and CI/CD pipelines to ensure services are ready before proceeding.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
//! Target implementation and builders.
//!
//! This module provides implementations for creating and working with targets,
//! which represent services to wait for. Targets can be either TCP connections
//! or HTTP endpoints.
//!
//! # Features
//!
//! - **TCP Targets**: Direct socket connections to host:port
//! - **HTTP Targets**: HTTP/HTTPS requests with status code validation
//! - **Builder Pattern**: Fluent APIs for complex target configuration
//! - **Input Validation**: Comprehensive validation for all inputs
//! - **Parsing**: String-to-target parsing for CLI and config files
//!
//! # Examples
//!
//! ## Basic target creation
//!
//! ```rust
//! use waitup::Target;
//! use url::Url;
//!
//! // Create TCP targets
//! let db = Target::tcp("database.example.com", 5432)?;
//! let localhost_api = Target::localhost(8080)?;
//!
//! // Create HTTP targets
//! let health_check = Target::http_url("https://api.example.com/health", 200)?;
//! let status_page = Target::http(
//!     Url::parse("https://status.example.com")?,
//!     200
//! )?;
//! # Ok::<(), waitup::WaitForError>(())
//! ```
//!
//! ## Using builders for complex configurations
//!
//! ```rust
//! use waitup::Target;
//! use url::Url;
//!
//! // HTTP target with custom headers
//! let api_target = Target::http_builder(Url::parse("https://api.example.com/protected")?)
//!     .status(201)
//!     .auth_bearer("your-api-token")
//!     .content_type("application/json")
//!     .header("X-Custom-Header", "custom-value")
//!     .build()?;
//!
//! // TCP target with specific port type validation
//! let service = Target::tcp_builder("service.example.com")?
//!     .registered_port(8080)
//!     .build()?;
//! # Ok::<(), waitup::WaitForError>(())
//! ```
//!
//! ## Parsing targets from strings
//!
//! ```rust
//! use waitup::Target;
//!
//! // Parse various target formats
//! let tcp_target = Target::parse("localhost:8080", 200)?;
//! let http_target = Target::parse("https://example.com/health", 200)?;
//! let custom_port = Target::parse("api.example.com:3000", 200)?;
//! # Ok::<(), waitup::WaitForError>(())
//! ```

use std::borrow::Cow;
use url::Url;

use crate::types::{Hostname, Port, Target};
use crate::{Result, ResultExt, WaitForError};

impl Target {
    /// Create multiple TCP targets from a list of host:port pairs
    ///
    /// # Errors
    ///
    /// Returns an error if any hostname or port is invalid
    pub fn tcp_batch<I, S>(targets: I) -> crate::types::TargetVecResult
    where
        I: IntoIterator<Item = (S, u16)>,
        S: AsRef<str>,
    {
        targets
            .into_iter()
            .map(|(host, port)| Self::tcp(host.as_ref(), port))
            .collect()
    }

    /// Create multiple HTTP targets from a list of URLs
    ///
    /// # Errors
    ///
    /// Returns an error if any URL is invalid or cannot be parsed
    pub fn http_batch<I, S>(urls: I, default_status: u16) -> crate::types::TargetVecResult
    where
        I: IntoIterator<Item = S>,
        S: AsRef<str>,
    {
        urls.into_iter()
            .map(|url| Self::http_url(url.as_ref(), default_status))
            .collect()
    }
    /// Create a new TCP target.
    ///
    /// # Errors
    ///
    /// Returns an error if the hostname is invalid or the port is out of range (1-65535)
    ///
    /// # Examples
    ///
    /// ```rust
    /// use waitup::Target;
    ///
    /// let target = Target::tcp("localhost", 8080)?;
    /// # Ok::<(), waitup::WaitForError>(())
    /// ```
    pub fn tcp(host: impl AsRef<str>, port: u16) -> Result<Self> {
        let hostname = Hostname::new(host.as_ref())
            .with_context(|| format!("Invalid hostname '{host}'", host = host.as_ref()))?;
        let port = Port::try_from(port).with_context(|| format!("Invalid port {port}"))?;
        Ok(Self::Tcp {
            host: hostname,
            port,
        })
    }

    /// Create a TCP target for localhost.
    ///
    /// # Errors
    ///
    /// Returns an error if the port is invalid (0 or > 65535)
    ///
    /// # Examples
    ///
    /// ```rust
    /// use waitup::Target;
    ///
    /// let target = Target::localhost(8080)?;
    /// # Ok::<(), waitup::WaitForError>(())
    /// ```
    pub fn localhost(port: u16) -> Result<Self> {
        let port = Port::try_from(port).with_context(|| format!("Invalid port {port}"))?;
        Ok(Self::Tcp {
            host: Hostname::localhost(),
            port,
        })
    }

    /// Create a TCP target for IPv4 loopback (127.0.0.1).
    ///
    /// # Errors
    ///
    /// Returns an error if the port is invalid (0 or > 65535)
    ///
    /// # Examples
    ///
    /// ```rust
    /// use waitup::Target;
    ///
    /// let target = Target::loopback(8080)?;
    /// # Ok::<(), waitup::WaitForError>(())
    /// ```
    pub fn loopback(port: u16) -> Result<Self> {
        let port = Port::try_from(port).with_context(|| format!("Invalid port {port}"))?;
        Ok(Self::Tcp {
            host: Hostname::loopback(),
            port,
        })
    }

    /// Create a TCP target for IPv6 loopback (`::1`).
    ///
    /// # Errors
    ///
    /// Returns an error if the port is invalid (0 or > 65535)
    ///
    /// # Examples
    ///
    /// ```rust
    /// use waitup::Target;
    ///
    /// let target = Target::loopback_v6(8080)?;
    /// # Ok::<(), waitup::WaitForError>(())
    /// ```
    pub fn loopback_v6(port: u16) -> Result<Self> {
        let port = Port::try_from(port).with_context(|| format!("Invalid port {port}"))?;
        Ok(Self::Tcp {
            host: Hostname::loopback_v6(),
            port,
        })
    }

    /// Create a TCP target with validated types (no additional validation).
    ///
    /// # Examples
    ///
    /// ```rust
    /// use waitup::{Target, Hostname, Port};
    ///
    /// let hostname = Hostname::localhost();
    /// let port = Port::new(8080).unwrap();
    /// let target = Target::from_parts(hostname, port);
    /// ```
    #[must_use]
    pub const fn from_parts(host: Hostname, port: Port) -> Self {
        Self::Tcp { host, port }
    }

    /// Create a new HTTP target with expected status code 200.
    ///
    /// # Errors
    ///
    /// Returns an error if the URL scheme is not HTTP/HTTPS or if the status code is invalid
    ///
    /// # Examples
    ///
    /// ```rust
    /// use waitup::Target;
    /// use url::Url;
    ///
    /// let url = Url::parse("https://api.example.com/health")?;
    /// let target = Target::http(url, 200)?;
    /// # Ok::<(), waitup::WaitForError>(())
    /// ```
    pub fn http(url: Url, expected_status: u16) -> Result<Self> {
        Self::validate_http_config(&url, expected_status, None)?;
        Ok(Self::Http {
            url,
            expected_status,
            headers: None,
        })
    }

    /// Create a new HTTP target from a URL string.
    ///
    /// # Errors
    ///
    /// Returns an error if the URL cannot be parsed or if validation fails
    ///
    /// # Examples
    ///
    /// ```rust
    /// use waitup::Target;
    ///
    /// let target = Target::http_url("https://api.example.com/health", 200)?;
    /// # Ok::<(), waitup::WaitForError>(())
    /// ```
    pub fn http_url(url: impl AsRef<str>, expected_status: u16) -> Result<Self> {
        let url = Url::parse(url.as_ref())
            .with_context(|| format!("Invalid URL: {url}", url = url.as_ref()))?;
        Self::http(url, expected_status)
    }

    /// Validate HTTP target configuration
    pub(crate) fn validate_http_config(
        url: &Url,
        expected_status: u16,
        headers: Option<&crate::types::HttpHeaders>,
    ) -> Result<()> {
        // Validate URL scheme
        if !matches!(url.scheme(), "http" | "https") {
            return Err(WaitForError::InvalidTarget(Cow::Owned(format!(
                "Unsupported URL scheme: {scheme}",
                scheme = url.scheme()
            ))));
        }

        // Validate status code
        if !(100..=599).contains(&expected_status) {
            return Err(WaitForError::InvalidTarget(Cow::Owned(format!(
                "Invalid HTTP status code: {expected_status}"
            ))));
        }

        // Validate headers if present
        if let Some(headers) = headers {
            for (key, value) in headers {
                if key.is_empty() {
                    return Err(WaitForError::InvalidTarget(Cow::Borrowed(
                        "HTTP header key cannot be empty",
                    )));
                }
                if value.is_empty() {
                    return Err(WaitForError::InvalidTarget(Cow::Borrowed(
                        "HTTP header value cannot be empty",
                    )));
                }
                // Basic header name validation (simplified)
                if !key
                    .chars()
                    .all(|c| c.is_ascii_alphanumeric() || "-_".contains(c))
                {
                    return Err(WaitForError::InvalidTarget(Cow::Owned(format!(
                        "Invalid HTTP header name: {key}"
                    ))));
                }
            }
        }

        Ok(())
    }

    /// Create a new HTTP target with custom headers.
    ///
    /// # Errors
    ///
    /// Returns an error if URL validation fails or if headers are invalid
    ///
    /// # Examples
    ///
    /// ```rust
    /// use waitup::Target;
    /// use url::Url;
    ///
    /// let url = Url::parse("https://api.example.com/health")?;
    /// let headers = vec![("Authorization".to_string(), "Bearer token".to_string())];
    /// let target = Target::http_with_headers(url, 200, headers)?;
    /// # Ok::<(), waitup::WaitForError>(())
    /// ```
    pub fn http_with_headers(
        url: Url,
        expected_status: u16,
        headers: crate::types::HttpHeaders,
    ) -> Result<Self> {
        Self::validate_http_config(&url, expected_status, Some(&headers))?;
        Ok(Self::Http {
            url,
            expected_status,
            headers: Some(headers),
        })
    }

    /// Parse a target from a string.
    ///
    /// Supports formats:
    /// - `host:port` for TCP targets
    /// - `http://host/path` or `https://host/path` for HTTP targets
    ///
    /// # Errors
    ///
    /// Returns an error if the string format is invalid or if parsing fails
    ///
    /// # Examples
    ///
    /// ```rust
    /// use waitup::Target;
    ///
    /// let tcp_target = Target::parse("localhost:8080", 200)?;
    /// let http_target = Target::parse("https://api.example.com/health", 200)?;
    /// # Ok::<(), waitup::WaitForError>(())
    /// ```
    pub fn parse(target_str: &str, default_http_status: u16) -> Result<Self> {
        if target_str.starts_with("http://") || target_str.starts_with("https://") {
            let url = Url::parse(target_str)?;
            Ok(Self::Http {
                url,
                expected_status: default_http_status,
                headers: None,
            })
        } else {
            let parts: Vec<&str> = target_str.split(':').collect();
            if parts.len() != 2 {
                return Err(WaitForError::InvalidTarget(Cow::Owned(
                    target_str.to_string(),
                )));
            }
            let hostname = Hostname::try_from(parts[0]).with_context(|| {
                format!(
                    "Invalid hostname '{hostname}' in target '{target_str}'",
                    hostname = parts[0]
                )
            })?;
            let port = parts[1]
                .parse::<u16>()
                .map_err(|_| WaitForError::InvalidTarget(Cow::Owned(target_str.to_string())))
                .with_context(|| {
                    format!(
                        "Invalid port '{port}' in target '{target_str}'",
                        port = parts[1]
                    )
                })?;
            let port = Port::try_from(port)
                .with_context(|| format!("Port {port} out of valid range (1-65535)"))?;
            Ok(Self::Tcp {
                host: hostname,
                port,
            })
        }
    }

    /// Get a string representation of this target for display purposes.
    #[must_use]
    pub fn display(&self) -> String {
        crate::zero_cost::TargetDisplay::new(self).to_string()
    }

    /// Get the hostname for this target (useful for logging and grouping)
    #[must_use]
    pub fn hostname(&self) -> &str {
        match self {
            Self::Tcp { host, .. } => host.as_str(),
            Self::Http { url, .. } => url.host_str().unwrap_or("unknown"),
        }
    }

    /// Get the port for this target
    #[must_use]
    pub fn port(&self) -> Option<u16> {
        match self {
            Self::Tcp { port, .. } => Some(port.get()),
            Self::Http { url, .. } => url.port(),
        }
    }

    /// Create a builder for HTTP targets
    #[must_use]
    pub const fn http_builder(url: Url) -> HttpTargetBuilder {
        HttpTargetBuilder::new(url)
    }

    /// Create a builder for TCP targets
    ///
    /// # Errors
    ///
    /// Returns an error if the hostname is invalid
    pub fn tcp_builder(host: impl AsRef<str>) -> Result<TcpTargetBuilder> {
        let hostname = Hostname::new(host.as_ref())
            .with_context(|| format!("Invalid hostname '{host}'", host = host.as_ref()))?;
        Ok(TcpTargetBuilder::new(hostname))
    }
}

/// Builder for HTTP targets
#[derive(Debug)]
pub struct HttpTargetBuilder {
    url: Url,
    expected_status: u16,
    headers: crate::types::HttpHeaders,
}

impl HttpTargetBuilder {
    pub(crate) const fn new(url: Url) -> Self {
        Self {
            url,
            expected_status: 200,
            headers: Vec::new(),
        }
    }

    /// Set the expected HTTP status code
    #[must_use]
    pub const fn status(mut self, status: u16) -> Self {
        self.expected_status = status;
        self
    }

    /// Add a header
    #[must_use]
    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.headers.push((key.into(), value.into()));
        self
    }

    /// Add multiple headers
    #[must_use]
    pub fn headers(mut self, headers: impl IntoIterator<Item = (String, String)>) -> Self {
        self.headers.extend(headers);
        self
    }

    /// Set authorization header with Bearer token
    #[must_use]
    pub fn auth_bearer(self, token: impl AsRef<str>) -> Self {
        self.header(
            "Authorization",
            crate::lazy_format!("Bearer {}", token.as_ref()).to_string(),
        )
    }

    /// Set content type header
    #[must_use]
    pub fn content_type(self, content_type: impl Into<String>) -> Self {
        self.header("Content-Type", content_type)
    }

    /// Set user agent header
    #[must_use]
    pub fn user_agent(self, user_agent: impl Into<String>) -> Self {
        self.header("User-Agent", user_agent)
    }

    /// Build the HTTP target
    ///
    /// # Errors
    ///
    /// Returns an error if validation fails
    pub fn build(self) -> Result<Target> {
        let headers = if self.headers.is_empty() {
            None
        } else {
            Some(self.headers)
        };
        Target::validate_http_config(&self.url, self.expected_status, headers.as_ref())?;
        Ok(Target::Http {
            url: self.url,
            expected_status: self.expected_status,
            headers,
        })
    }
}

/// Builder for TCP targets
#[derive(Debug)]
pub struct TcpTargetBuilder {
    host: Hostname,
    port: Option<Port>,
    port_validation_error: Option<WaitForError>,
}

impl TcpTargetBuilder {
    pub(crate) const fn new(host: Hostname) -> Self {
        Self {
            host,
            port: None,
            port_validation_error: None,
        }
    }

    /// Set the port
    #[must_use]
    pub fn port(mut self, port: u16) -> Self {
        match Port::try_from(port) {
            Ok(p) => {
                self.port = Some(p);
                self.port_validation_error = None;
            }
            Err(e) => {
                self.port_validation_error = Some(e);
            }
        }
        self
    }

    /// Set a well-known port (0-1023)
    #[must_use]
    pub fn well_known_port(mut self, port: u16) -> Self {
        match Port::system_port(port) {
            Some(p) => {
                self.port = Some(p);
                self.port_validation_error = None;
            }
            None => {
                self.port_validation_error = Some(WaitForError::InvalidPort(port));
            }
        }
        self
    }

    /// Set a registered port (1024-49151)
    #[must_use]
    pub fn registered_port(mut self, port: u16) -> Self {
        match Port::user_port(port) {
            Some(p) => {
                self.port = Some(p);
                self.port_validation_error = None;
            }
            None => {
                self.port_validation_error = Some(WaitForError::InvalidPort(port));
            }
        }
        self
    }

    /// Set a dynamic port (49152-65535)
    #[must_use]
    pub fn dynamic_port(mut self, port: u16) -> Self {
        match Port::dynamic_port(port) {
            Some(p) => {
                self.port = Some(p);
                self.port_validation_error = None;
            }
            None => {
                self.port_validation_error = Some(WaitForError::InvalidPort(port));
            }
        }
        self
    }

    /// Build the TCP target
    ///
    /// # Errors
    ///
    /// Returns an error if no port was specified or if validation fails
    pub fn build(self) -> Result<Target> {
        // Check for validation errors first
        if let Some(error) = self.port_validation_error {
            return Err(error);
        }

        let port = self
            .port
            .ok_or_else(|| WaitForError::InvalidTarget(Cow::Borrowed("Port must be specified")))?;
        Ok(Target::Tcp {
            host: self.host,
            port,
        })
    }
}