use std::{
fmt::{self, Display, Formatter},
str::FromStr,
};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::serde::FromStrOrU16Visitor;
use super::ports::{ParseRangeError, Protocol, Range};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Expose {
pub range: Range,
pub protocol: Option<Protocol>,
}
impl From<u16> for Expose {
fn from(start: u16) -> Self {
Self {
range: start.into(),
protocol: None,
}
}
}
impl From<Range> for Expose {
fn from(range: Range) -> Self {
Self {
range,
protocol: None,
}
}
}
impl FromStr for Expose {
type Err = ParseRangeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (range, protocol) = s.split_once('/').map_or((s, None), |(range, protocol)| {
(range, Some(protocol.into()))
});
Ok(Self {
range: range.parse()?,
protocol,
})
}
}
impl TryFrom<&str> for Expose {
type Error = ParseRangeError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
impl Display for Expose {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let Self { range, protocol } = self;
Display::fmt(range, f)?;
if let Some(protocol) = protocol {
write!(f, "/{protocol}")?;
}
Ok(())
}
}
impl Serialize for Expose {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
if self.range.end().is_none() && self.protocol.is_none() {
self.range.start().serialize(serializer)
} else {
serializer.collect_str(self)
}
}
}
impl<'de> Deserialize<'de> for Expose {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
FromStrOrU16Visitor::new("an integer or string representing a port or port range")
.deserialize(deserializer)
}
}
#[cfg(test)]
mod tests {
use proptest::{option, prop_assert_eq, prop_compose, proptest};
use crate::service::ports::tests::{protocol, range};
use super::*;
proptest! {
#[test]
fn parse_no_panic(string: String) {
let _ = string.parse::<Expose>();
}
#[test]
fn to_string_no_panic(expose in expose()) {
expose.to_string();
}
#[test]
fn round_trip(expose in expose()) {
prop_assert_eq!(&expose, &expose.to_string().parse()?);
}
}
prop_compose! {
fn expose()(range in range(), protocol in option::of(protocol())) -> Expose {
Expose {
range,
protocol,
}
}
}
}