use-docker-network 0.0.1

Primitive Docker network mode helpers for RustUse
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

use core::{fmt, str::FromStr};
use std::error::Error;

/// Error returned when a Docker network mode is invalid.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DockerNetworkError {
    /// The network value was empty after trimming.
    Empty,
    /// A service, container, or named network label was invalid.
    InvalidName,
}

impl fmt::Display for DockerNetworkError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Empty => formatter.write_str("Docker network value cannot be empty"),
            Self::InvalidName => formatter.write_str("invalid Docker network name"),
        }
    }
}

impl Error for DockerNetworkError {}

/// Docker network mode or named network value.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum DockerNetworkMode {
    /// Docker bridge networking.
    Bridge,
    /// Host networking.
    Host,
    /// No networking.
    None,
    /// Share a service network namespace.
    Service(String),
    /// Share a container network namespace.
    Container(String),
    /// A named network.
    Named(String),
}

impl DockerNetworkMode {
    /// Creates a named network value.
    pub fn named(value: impl AsRef<str>) -> Result<Self, DockerNetworkError> {
        let value = value.as_ref().trim();
        validate_name(value)?;
        Ok(Self::Named(value.to_string()))
    }

    /// Returns true for `none`.
    #[must_use]
    pub const fn is_isolated(&self) -> bool {
        matches!(self, Self::None)
    }

    /// Returns true for `service:<name>`.
    #[must_use]
    pub const fn is_service_reference(&self) -> bool {
        matches!(self, Self::Service(_))
    }

    /// Returns true for named networks.
    #[must_use]
    pub const fn is_named(&self) -> bool {
        matches!(self, Self::Named(_))
    }
}

impl fmt::Display for DockerNetworkMode {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Bridge => formatter.write_str("bridge"),
            Self::Host => formatter.write_str("host"),
            Self::None => formatter.write_str("none"),
            Self::Service(name) => write!(formatter, "service:{name}"),
            Self::Container(name) => write!(formatter, "container:{name}"),
            Self::Named(name) => formatter.write_str(name),
        }
    }
}

impl FromStr for DockerNetworkMode {
    type Err = DockerNetworkError;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        let trimmed = value.trim();
        if trimmed.is_empty() {
            return Err(DockerNetworkError::Empty);
        }
        match trimmed {
            "bridge" => Ok(Self::Bridge),
            "host" => Ok(Self::Host),
            "none" => Ok(Self::None),
            _ => {
                if let Some(service) = trimmed.strip_prefix("service:") {
                    validate_name(service)?;
                    Ok(Self::Service(service.to_string()))
                } else if let Some(container) = trimmed.strip_prefix("container:") {
                    validate_name(container)?;
                    Ok(Self::Container(container.to_string()))
                } else {
                    Self::named(trimmed)
                }
            },
        }
    }
}

impl TryFrom<&str> for DockerNetworkMode {
    type Error = DockerNetworkError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        value.parse()
    }
}

fn validate_name(value: &str) -> Result<(), DockerNetworkError> {
    if value.is_empty() || value.chars().any(char::is_whitespace) {
        Err(DockerNetworkError::InvalidName)
    } else {
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::DockerNetworkMode;

    #[test]
    fn parses_network_modes() -> Result<(), Box<dyn std::error::Error>> {
        let service: DockerNetworkMode = "service:web".parse()?;
        let named: DockerNetworkMode = "frontend".parse()?;

        assert!(service.is_service_reference());
        assert!(named.is_named());
        assert!(DockerNetworkMode::None.is_isolated());
        assert_eq!(service.to_string(), "service:web");
        Ok(())
    }
}