use std::borrow::Cow;
use url::Url;
use crate::types::{Hostname, Port, Target};
use crate::{Result, ResultExt, WaitForError};
impl Target {
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()
}
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()
}
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,
})
}
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,
})
}
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,
})
}
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,
})
}
#[must_use]
pub const fn from_parts(host: Hostname, port: Port) -> Self {
Self::Tcp { host, port }
}
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,
})
}
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)
}
pub(crate) fn validate_http_config(
url: &Url,
expected_status: u16,
headers: Option<&crate::types::HttpHeaders>,
) -> Result<()> {
if !matches!(url.scheme(), "http" | "https") {
return Err(WaitForError::InvalidTarget(Cow::Owned(format!(
"Unsupported URL scheme: {scheme}",
scheme = url.scheme()
))));
}
if !(100..=599).contains(&expected_status) {
return Err(WaitForError::InvalidTarget(Cow::Owned(format!(
"Invalid HTTP status code: {expected_status}"
))));
}
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",
)));
}
if !key
.chars()
.all(|c| c.is_ascii_alphanumeric() || "-_".contains(c))
{
return Err(WaitForError::InvalidTarget(Cow::Owned(format!(
"Invalid HTTP header name: {key}"
))));
}
}
}
Ok(())
}
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),
})
}
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,
})
}
}
#[must_use]
pub fn display(&self) -> String {
crate::zero_cost::TargetDisplay::new(self).to_string()
}
#[must_use]
pub fn hostname(&self) -> &str {
match self {
Self::Tcp { host, .. } => host.as_str(),
Self::Http { url, .. } => url.host_str().unwrap_or("unknown"),
}
}
#[must_use]
pub fn port(&self) -> Option<u16> {
match self {
Self::Tcp { port, .. } => Some(port.get()),
Self::Http { url, .. } => url.port(),
}
}
#[must_use]
pub const fn http_builder(url: Url) -> HttpTargetBuilder {
HttpTargetBuilder::new(url)
}
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))
}
}
#[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(),
}
}
#[must_use]
pub const fn status(mut self, status: u16) -> Self {
self.expected_status = status;
self
}
#[must_use]
pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.push((key.into(), value.into()));
self
}
#[must_use]
pub fn headers(mut self, headers: impl IntoIterator<Item = (String, String)>) -> Self {
self.headers.extend(headers);
self
}
#[must_use]
pub fn auth_bearer(self, token: impl AsRef<str>) -> Self {
self.header(
"Authorization",
crate::lazy_format!("Bearer {}", token.as_ref()).to_string(),
)
}
#[must_use]
pub fn content_type(self, content_type: impl Into<String>) -> Self {
self.header("Content-Type", content_type)
}
#[must_use]
pub fn user_agent(self, user_agent: impl Into<String>) -> Self {
self.header("User-Agent", user_agent)
}
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,
})
}
}
#[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,
}
}
#[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
}
#[must_use]
pub fn well_known_port(mut self, port: u16) -> Self {
match Port::well_known(port) {
Some(p) => {
self.port = Some(p);
self.port_validation_error = None;
}
None => {
self.port_validation_error = Some(WaitForError::InvalidPort(port));
}
}
self
}
#[must_use]
pub fn registered_port(mut self, port: u16) -> Self {
match Port::registered(port) {
Some(p) => {
self.port = Some(p);
self.port_validation_error = None;
}
None => {
self.port_validation_error = Some(WaitForError::InvalidPort(port));
}
}
self
}
#[must_use]
pub fn dynamic_port(mut self, port: u16) -> Self {
match Port::dynamic(port) {
Some(p) => {
self.port = Some(p);
self.port_validation_error = None;
}
None => {
self.port_validation_error = Some(WaitForError::InvalidPort(port));
}
}
self
}
pub fn build(self) -> Result<Target> {
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,
})
}
}