use std::{
borrow::Cow,
fmt::{self, Display, Formatter},
net::IpAddr,
ops::Not,
};
use compose_spec_macros::{DeserializeTryFromString, SerializeDisplay};
use indexmap::IndexMap;
use ipnet::IpNet;
use serde::{Deserialize, Serialize};
use crate::{
impl_from_str, service::Hostname, Extensions, ListOrMap, MapKey, Resource, StringOrNumber,
};
impl Resource<Network> {
#[must_use]
pub const fn name(&self) -> Option<&String> {
match self {
Self::External { name } => name.as_ref(),
Self::Compose(network) => network.name.as_ref(),
}
}
}
impl From<Network> for Resource<Network> {
fn from(value: Network) -> Self {
Self::Compose(value)
}
}
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
pub struct Network {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub driver: Option<Driver>,
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub driver_opts: IndexMap<MapKey, StringOrNumber>,
#[serde(default, skip_serializing_if = "Not::not")]
pub attachable: bool,
#[serde(default, skip_serializing_if = "Not::not")]
pub enable_ipv6: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ipam: Option<Ipam>,
#[serde(default, skip_serializing_if = "Not::not")]
pub internal: bool,
#[serde(default, skip_serializing_if = "ListOrMap::is_empty")]
pub labels: ListOrMap,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(flatten)]
pub extensions: Extensions,
}
impl Network {
#[must_use]
pub fn is_empty(&self) -> bool {
let Self {
driver,
driver_opts,
attachable,
enable_ipv6,
ipam,
internal,
labels,
name,
extensions,
} = self;
driver.is_none()
&& driver_opts.is_empty()
&& !attachable
&& !enable_ipv6
&& !ipam.as_ref().is_some_and(|ipam| !ipam.is_empty())
&& !internal
&& labels.is_empty()
&& name.is_none()
&& extensions.is_empty()
}
}
#[derive(SerializeDisplay, DeserializeTryFromString, Debug, Clone, PartialEq, Eq)]
pub enum Driver {
Host,
None,
Other(String),
}
impl Driver {
const HOST: &'static str = "host";
const NONE: &'static str = "none";
pub fn parse<T>(driver: T) -> Self
where
T: AsRef<str> + Into<String>,
{
match driver.as_ref() {
Self::HOST => Self::Host,
Self::NONE => Self::None,
_ => Self::Other(driver.into()),
}
}
#[must_use]
pub fn as_str(&self) -> &str {
match self {
Self::Host => Self::HOST,
Self::None => Self::NONE,
Self::Other(other) => other,
}
}
}
impl_from_str!(Driver);
impl AsRef<str> for Driver {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl Display for Driver {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl From<Driver> for Cow<'static, str> {
fn from(value: Driver) -> Self {
match value {
Driver::Host => Driver::HOST.into(),
Driver::None => Driver::NONE.into(),
Driver::Other(other) => other.into(),
}
}
}
impl From<Driver> for String {
fn from(value: Driver) -> Self {
Cow::from(value).into_owned()
}
}
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
pub struct Ipam {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub driver: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub config: Vec<IpamConfig>,
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub options: IndexMap<MapKey, String>,
#[serde(flatten)]
pub extensions: Extensions,
}
impl Ipam {
#[must_use]
pub fn is_empty(&self) -> bool {
let Self {
driver,
config,
options,
extensions,
} = self;
driver.is_none()
&& config.iter().all(IpamConfig::is_empty)
&& options.is_empty()
&& extensions.is_empty()
}
}
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
pub struct IpamConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subnet: Option<IpNet>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ip_range: Option<IpNet>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gateway: Option<IpAddr>,
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub aux_addresses: IndexMap<Hostname, IpAddr>,
#[serde(flatten)]
pub extensions: Extensions,
}
impl IpamConfig {
#[must_use]
pub fn is_empty(&self) -> bool {
let Self {
subnet,
ip_range,
gateway,
aux_addresses,
extensions,
} = self;
subnet.is_none()
&& ip_range.is_none()
&& gateway.is_none()
&& aux_addresses.is_empty()
&& extensions.is_empty()
}
}