use std::fmt;
use std::num::NonZeroU16;
use schemars::JsonSchema;
use serde::de::{self, MapAccess, Visitor};
use serde::{Deserialize, Deserializer, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum PortProtocol {
#[default]
Tcp,
Udp,
Http,
Https,
Tls,
}
impl PortProtocol {
#[must_use]
pub fn is_tcp(self) -> bool {
self != PortProtocol::Udp
}
#[must_use]
pub fn is_http(self) -> bool {
matches!(self, PortProtocol::Http | PortProtocol::Https)
}
#[must_use]
pub fn is_tls(self) -> bool {
matches!(self, PortProtocol::Https | PortProtocol::Tls)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Publish {
Free,
Preferred(NonZeroU16),
Fixed(NonZeroU16),
}
#[derive(JsonSchema)]
#[serde(untagged)]
enum PublishInput {
Free(bool),
Preferred(NonZeroU16),
Host {
host: NonZeroU16,
#[serde(default)]
fixed: bool,
},
}
impl PublishInput {
fn publish(self) -> Option<Publish> {
match self {
PublishInput::Free(free) => free.then_some(Publish::Free),
PublishInput::Preferred(host) | PublishInput::Host { host, fixed: false } => Some(Publish::Preferred(host)),
PublishInput::Host { host, fixed: true } => Some(Publish::Fixed(host)),
}
}
}
impl<'de> Deserialize<'de> for PublishInput {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<PublishInput, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = PublishInput;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("`true`, a host port, or `{ host, fixed: true }`")
}
fn visit_bool<E: de::Error>(self, v: bool) -> Result<PublishInput, E> {
Ok(PublishInput::Free(v))
}
fn visit_u64<E: de::Error>(self, v: u64) -> Result<PublishInput, E> {
host_port(v).map(PublishInput::Preferred)
}
fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<PublishInput, A::Error> {
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct HostFields {
host: NonZeroU16,
#[serde(default)]
fixed: bool,
}
let h = HostFields::deserialize(de::value::MapAccessDeserializer::new(map))?;
Ok(PublishInput::Host { host: h.host, fixed: h.fixed })
}
}
d.deserialize_any(V)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(from = "PortInput")]
pub struct Port {
pub port: Option<NonZeroU16>,
pub protocol: PortProtocol,
pub publish: Option<Publish>,
pub dormant: bool,
}
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
struct PortFull {
#[serde(default)]
port: Option<NonZeroU16>,
#[serde(default)]
protocol: PortProtocol,
#[serde(default, deserialize_with = "publish")]
#[schemars(with = "Option<PublishInput>")]
publish: Option<Publish>,
#[serde(default)]
dormant: bool,
}
#[derive(JsonSchema)]
#[serde(untagged)]
enum PortInput {
Number(NonZeroU16),
Full(PortFull),
}
impl<'de> Deserialize<'de> for PortInput {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<PortInput, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = PortInput;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a port number, or `{ port, protocol, publish, dormant }`")
}
fn visit_u64<E: de::Error>(self, v: u64) -> Result<PortInput, E> {
let port = u16::try_from(v)
.ok()
.and_then(NonZeroU16::new)
.ok_or_else(|| E::custom("a port is 1 to 65535"))?;
Ok(PortInput::Number(port))
}
fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<PortInput, A::Error> {
PortFull::deserialize(de::value::MapAccessDeserializer::new(map)).map(PortInput::Full)
}
}
d.deserialize_any(V)
}
}
impl From<PortInput> for Port {
fn from(written: PortInput) -> Port {
match written {
PortInput::Number(port) => Port {
port: Some(port),
protocol: PortProtocol::Tcp,
publish: None,
dormant: false,
},
PortInput::Full(f) => Port {
port: f.port,
protocol: f.protocol,
publish: f.publish,
dormant: f.dormant,
},
}
}
}
fn publish<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Publish>, D::Error> {
PublishInput::deserialize(d).map(PublishInput::publish)
}
fn host_port<E: de::Error>(v: u64) -> Result<NonZeroU16, E> {
u16::try_from(v)
.ok()
.and_then(NonZeroU16::new)
.ok_or_else(|| E::custom("a host port is 1 to 65535"))
}
impl JsonSchema for Port {
fn schema_name() -> std::borrow::Cow<'static, str> {
"Port".into()
}
fn json_schema(g: &mut schemars::SchemaGenerator) -> schemars::Schema {
PortInput::json_schema(g)
}
}
#[cfg(test)]
mod tests {
use indexmap::IndexMap;
use super::*;
use crate::syntax::names::PortName;
fn ports(yaml: &str) -> Result<IndexMap<PortName, Port>, String> {
serde_yaml_ng::from_str(yaml).map_err(|e| e.to_string())
}
#[test]
fn a_protocol_says_what_it_runs_over_and_speaks() {
let all = [PortProtocol::Tcp, PortProtocol::Udp, PortProtocol::Http, PortProtocol::Https, PortProtocol::Tls];
let tcp: Vec<bool> = all.iter().map(|p| p.is_tcp()).collect();
let http: Vec<bool> = all.iter().map(|p| p.is_http()).collect();
let tls: Vec<bool> = all.iter().map(|p| p.is_tls()).collect();
assert_eq!(tcp, [true, false, true, true, true]);
assert_eq!(http, [false, false, true, true, false]);
assert_eq!(tls, [false, false, false, true, true]);
}
#[test]
fn a_port_or_a_publish_of_the_wrong_shape_says_what_is_expected() {
let port = ports("{ a: [1] }").unwrap_err();
assert!(port.contains("a port number, or `{ port, protocol, publish, dormant }`"), "{port}");
let publish = ports("{ a: { port: 1, publish: [1] } }").unwrap_err();
assert!(publish.contains("`true`, a host port, or `{ host, fixed: true }`"), "{publish}");
assert!(ports("{ a: { port: 1, publish: 0 } }").unwrap_err().contains("a host port is 1 to 65535"));
}
#[test]
fn a_port_is_a_number_or_written_in_full() {
let p = ports("{ ssh: 22, site: { port: 8000, protocol: http, publish: 8080 }, shell: { port: 4444, dormant: true } }").unwrap();
let get = |n: &str| &p[&n.parse::<PortName>().unwrap()];
assert_eq!((get("ssh").port.map(NonZeroU16::get), get("ssh").protocol), (Some(22), PortProtocol::Tcp));
assert_eq!(get("site").publish, Some(Publish::Preferred(NonZeroU16::new(8080).unwrap())));
assert!(get("shell").dormant && get("site").protocol.is_http() && !get("site").protocol.is_tls());
}
#[test]
fn publish_is_true_a_number_or_fixed() {
let p = ports("{ a: { port: 1, publish: true }, b: { port: 2, publish: { host: 3206, fixed: true } }, c: { port: 3, publish: false } }").unwrap();
let publish: Vec<_> = p.values().map(|p| p.publish).collect();
assert_eq!(publish, [Some(Publish::Free), Some(Publish::Fixed(NonZeroU16::new(3206).unwrap())), None]);
}
#[test]
fn mistakes_are_named() {
assert!(ports("{ a: 0 }").unwrap_err().contains("1 to 65535"));
assert!(ports("{ a: { port: 1, protocol: quic } }").unwrap_err().contains("unknown variant"));
assert!(ports("{ a: { port: 1, http: true } }").unwrap_err().contains("unknown field `http`"));
assert!(ports("{ 80: 80 }").unwrap_err().contains("port's name"));
}
}