diff --git a/Cargo.lock b/Cargo.lock index 495f92d..850697a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,6 +47,7 @@ version = "0.1.0" dependencies = [ "compose_spec_macros", "indexmap", + "ipnet", "itoa", "pomsky-macro", "proptest", @@ -152,6 +153,15 @@ dependencies = [ "serde", ] +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +dependencies = [ + "serde", +] + [[package]] name = "itoa" version = "1.0.10" diff --git a/Cargo.toml b/Cargo.toml index c6e3f8c..8b87ed6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ workspace = true [dependencies] compose_spec_macros.workspace = true indexmap = { version = "2", features = ["serde"] } +ipnet = { version = "2", features = ["serde"] } itoa = "1" serde = { workspace = true, features = ["derive"] } serde-untagged = "0.1" diff --git a/src/common.rs b/src/common.rs index 8fae55a..1cb42a4 100644 --- a/src/common.rs +++ b/src/common.rs @@ -5,9 +5,10 @@ mod short_or_long; use std::{ borrow::Cow, - fmt::{self, Display, Formatter}, + fmt::{self, Display, Formatter, LowerExp, UpperExp}, hash::Hash, num::{ParseFloatError, ParseIntError, TryFromIntError}, + str::FromStr, }; use indexmap::{indexset, IndexMap, IndexSet}; @@ -255,27 +256,15 @@ impl Default for ListOrMap { } } -/// A single string, integer, float, or boolean value. -/// -/// The maximum range of integer values deserialized is `i64::MIN..=u64::MAX`. +/// A single string, number, or boolean value. #[derive(Serialize, Debug, Clone, PartialEq)] #[serde(untagged)] pub enum Value { /// A [`String`] value. String(String), - /// A [`u64`] (unsigned integer) value. - /// - /// Positive integers will parse/deserialize into this. - UnsignedInt(u64), - - /// A [`i64`] (signed integer) value. - /// - /// Negative integers will parse/deserialize into this. - SignedInt(i64), - - /// A [`f64`] (floating point) value. - Float(f64), + /// A [`Number`] value. + Number(Number), /// A [`bool`]ean value. Bool(bool), @@ -296,13 +285,17 @@ impl Value { let s = string.as_ref(); s.parse() .map(Self::Bool) - .or_else(|_| s.parse().map(Self::UnsignedInt)) - .or_else(|_| s.parse().map(Self::SignedInt)) - .or_else(|_| s.parse().map(Self::Float)) + .or_else(|_| s.parse().map(Self::Number)) .unwrap_or_else(|_| Self::String(string.into())) } - /// Returns [`Some`] if a [`Value::String`]. + /// Returns `true` if the value is a [`String`]. + #[must_use] + pub fn is_string(&self) -> bool { + matches!(self, Self::String(..)) + } + + /// Returns [`Some`] if the value is a [`String`]. #[must_use] pub fn as_string(&self) -> Option<&String> { if let Self::String(v) = self { @@ -312,43 +305,29 @@ impl Value { } } - /// Returns [`Some`] if a [`Value::UnsignedInt`]. - /// - /// Use `u64::try_from()` if conversion from other value types is wanted. + /// Returns `true` if the value is a [`Number`]. #[must_use] - pub fn as_unsigned_int(&self) -> Option { - if let Self::UnsignedInt(v) = self { - Some(*v) - } else { - None - } + pub fn is_number(&self) -> bool { + matches!(self, Self::Number(..)) } - /// Returns [`Some`] if a [`Value::SignedInt`]. - /// - /// Use `i64::try_from()` if conversion from other value types is wanted. + /// Returns [`Some`] if the value is a [`Number`]. #[must_use] - pub fn as_signed_int(&self) -> Option { - if let Self::SignedInt(v) = self { - Some(*v) + pub fn as_number(&self) -> Option<&Number> { + if let Self::Number(v) = self { + Some(v) } else { None } } - /// Returns [`Some`] if a [`Value::Float`]. - /// - /// Use `f64::try_from()` if conversion from other value types is wanted. + /// Returns `true` if the value is a [`bool`]. #[must_use] - pub fn as_float(&self) -> Option { - if let Self::Float(v) = self { - Some(*v) - } else { - None - } + pub fn is_bool(&self) -> bool { + matches!(self, Self::Bool(..)) } - /// Returns [`Some`] if a [`Value::Bool`]. + /// Returns [`Some`] if the value is a [`bool`]. #[must_use] pub fn as_bool(&self) -> Option { if let Self::Bool(v) = self { @@ -360,15 +339,12 @@ impl Value { } impl<'de> Deserialize<'de> for Value { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { + fn deserialize>(deserializer: D) -> Result { ValueEnumVisitor::new("a string, integer, float, or boolean") .string(Self::String) - .u64(Self::UnsignedInt) - .i64(Self::SignedInt) - .f64(Self::Float) + .u64(Into::into) + .i64(Into::into) + .f64(Into::into) .bool(Self::Bool) .deserialize(deserializer) } @@ -378,9 +354,7 @@ impl Display for Value { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { Self::String(value) => Display::fmt(value, f), - Self::UnsignedInt(value) => Display::fmt(value, f), - Self::SignedInt(value) => Display::fmt(value, f), - Self::Float(value) => Display::fmt(value, f), + Self::Number(value) => Display::fmt(value, f), Self::Bool(value) => Display::fmt(value, f), } } @@ -410,21 +384,27 @@ impl From> for Value { } } +impl From for Value { + fn from(value: Number) -> Self { + Self::Number(value) + } +} + impl From for Value { fn from(value: u64) -> Self { - Self::UnsignedInt(value) + Number::from(value).into() } } impl From for Value { fn from(value: i64) -> Self { - Self::SignedInt(value) + Number::from(value).into() } } impl From for Value { fn from(value: f64) -> Self { - Self::Float(value) + Number::from(value).into() } } @@ -444,31 +424,31 @@ impl From for String { } } -impl TryFrom for u64 { - type Error = TryFromValueError; +impl TryFrom for Number { + type Error = ParseNumberError; fn try_from(value: Value) -> Result { match value { - Value::String(value) => Ok(value.parse()?), - Value::UnsignedInt(value) => Ok(value), - Value::SignedInt(value) => Ok(value.try_into()?), - Value::Bool(value) => Ok(value.into()), - Value::Float(_) => Err(TryFromValueError::WrongType), + Value::String(value) => value.parse(), + Value::Number(value) => Ok(value), + Value::Bool(value) => Ok(u64::from(value).into()), } } } +impl TryFrom for u64 { + type Error = TryFromValueError; + + fn try_from(value: Value) -> Result { + Number::try_from(value)?.try_into().map_err(Into::into) + } +} + impl TryFrom for i64 { type Error = TryFromValueError; fn try_from(value: Value) -> Result { - match value { - Value::String(value) => Ok(value.parse()?), - Value::UnsignedInt(value) => Ok(value.try_into()?), - Value::SignedInt(value) => Ok(value), - Value::Bool(value) => Ok(value.into()), - Value::Float(_) => Err(TryFromValueError::WrongType), - } + Number::try_from(value)?.try_into().map_err(Into::into) } } @@ -476,12 +456,7 @@ impl TryFrom for f64 { type Error = TryFromValueError; fn try_from(value: Value) -> Result { - match value { - Value::String(value) => Ok(value.parse()?), - Value::Float(value) => Ok(value), - Value::Bool(value) => Ok(value.into()), - Value::UnsignedInt(_) | Value::SignedInt(_) => Err(TryFromValueError::WrongType), - } + Number::try_from(value)?.try_into().map_err(Into::into) } } @@ -491,31 +466,402 @@ impl TryFrom for bool { fn try_from(value: Value) -> Result { match value { Value::Bool(value) => Ok(value), - _ => Err(TryFromValueError::WrongType), + _ => Err(TryFromValueError::IntoBool), } } } -/// Error returned when failing to convert [`Value`] into another type. +/// Error returned when attempting to convert [`Value`] into another type. #[derive(Error, Debug, Clone, PartialEq, Eq)] pub enum TryFromValueError { - /// [`Value`] is not the correct type for conversion. - #[error("value is not the correct type for conversion")] - WrongType, + /// Error parsing [`Value::String`] into a [`Number`]. + #[error("error parsing string value into a number")] + ParseNumber(#[from] ParseNumberError), + + /// Error converting [`Number`] into the type. + #[error("error converting number")] + TryFromNumber(#[from] TryFromNumberError), + + /// Error converting integer type. + #[error("error converting integer type")] + TryFromInt(#[from] TryFromIntError), + + /// Can only convert [`Value::Bool`] into a [`bool`]. + #[error("cannot convert a non-bool value into a bool")] + IntoBool, +} + +/// A numerical [`Value`]. +#[derive(Serialize, Debug, Clone, Copy, PartialEq)] +#[serde(untagged)] +pub enum Number { + /// A [`u64`] (unsigned integer) value. + /// + /// Positive integers will parse/deserialize into this. + UnsignedInt(u64), + + /// A [`i64`] (signed integer) value. + /// + /// Negative integers will parse/deserialize into this. + SignedInt(i64), + + /// A [`f64`] (floating point) value. + Float(f64), +} + +impl Number { + /// Returns `true` if the number is an [`UnsignedInt`]. + /// + /// [`UnsignedInt`]: Number::UnsignedInt + #[must_use] + pub fn is_unsigned_int(&self) -> bool { + matches!(self, Self::UnsignedInt(..)) + } + + /// Returns [`Some`] if the number is an [`UnsignedInt`]. + /// + /// [`UnsignedInt`]: Number::UnsignedInt + #[must_use] + pub fn as_unsigned_int(&self) -> Option { + if let Self::UnsignedInt(v) = *self { + Some(v) + } else { + None + } + } + + /// Returns `true` if the number is a [`SignedInt`]. + /// + /// [`SignedInt`]: Number::SignedInt + #[must_use] + pub fn is_signed_int(&self) -> bool { + matches!(self, Self::SignedInt(..)) + } + + /// Returns [`Some`] if the number is a [`SignedInt`]. + /// + /// [`SignedInt`]: Number::SignedInt + #[must_use] + pub fn as_signed_int(&self) -> Option { + if let Self::SignedInt(v) = *self { + Some(v) + } else { + None + } + } + + /// Returns `true` if the number is a [`Float`]. + /// + /// [`Float`]: Number::Float + #[must_use] + pub fn is_float(&self) -> bool { + matches!(self, Self::Float(..)) + } + + /// Returns [`Some`] if the number is a [`Float`]. + /// + /// [`Float`]: Number::Float + #[must_use] + pub fn as_float(&self) -> Option { + if let Self::Float(v) = *self { + Some(v) + } else { + None + } + } +} + +impl<'de> Deserialize<'de> for Number { + fn deserialize>(deserializer: D) -> Result { + ValueEnumVisitor::new("an integer or float") + .u64(Self::UnsignedInt) + .i64(Self::SignedInt) + .f64(Self::Float) + .deserialize(deserializer) + } +} + +impl From for Number { + fn from(value: u64) -> Self { + Self::UnsignedInt(value) + } +} + +impl From for Number { + fn from(value: i64) -> Self { + Self::SignedInt(value) + } +} + +impl From for Number { + fn from(value: f64) -> Self { + Self::Float(value) + } +} + +impl FromStr for Number { + type Err = ParseNumberError; + + fn from_str(s: &str) -> Result { + if s.strip_prefix(['+', '-']) + .unwrap_or(s) + .contains(|char: char| !char.is_ascii_digit()) + { + // Parse as float if `s` contains non-digits, e.g. "5." or "inf". + Ok(Self::Float(s.parse()?)) + } else if s.starts_with('-') { + Ok(Self::SignedInt(s.parse()?)) + } else { + Ok(Self::UnsignedInt(s.parse()?)) + } + } +} + +impl TryFrom<&str> for Number { + type Error = ParseNumberError; + + fn try_from(value: &str) -> Result { + value.parse() + } +} + +/// Error returned when parsing a [`Number`] from a string. +#[derive(Error, Debug, Clone, PartialEq, Eq)] +pub enum ParseNumberError { + /// Error parsing [`u64`] or [`i64`]. + #[error("error parsing number as an integer")] + Int(#[from] ParseIntError), + + /// Error parsing [`f64`]. + #[error("error parsing number as a float")] + Float(#[from] ParseFloatError), +} + +impl Display for Number { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::UnsignedInt(number) => Display::fmt(number, f), + Self::SignedInt(number) => Display::fmt(number, f), + Self::Float(number) => Display::fmt(number, f), + } + } +} + +impl LowerExp for Number { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::UnsignedInt(number) => LowerExp::fmt(number, f), + Self::SignedInt(number) => LowerExp::fmt(number, f), + Self::Float(number) => LowerExp::fmt(number, f), + } + } +} + +impl UpperExp for Number { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::UnsignedInt(number) => UpperExp::fmt(number, f), + Self::SignedInt(number) => UpperExp::fmt(number, f), + Self::Float(number) => UpperExp::fmt(number, f), + } + } +} + +impl TryFrom for u64 { + type Error = TryFromNumberError; + + fn try_from(value: Number) -> Result { + match value { + Number::UnsignedInt(value) => Ok(value), + Number::SignedInt(value) => value.try_into().map_err(Into::into), + Number::Float(_) => Err(TryFromNumberError::FloatToInt), + } + } +} + +impl TryFrom for i64 { + type Error = TryFromNumberError; + + fn try_from(value: Number) -> Result { + match value { + Number::UnsignedInt(value) => value.try_into().map_err(Into::into), + Number::SignedInt(value) => Ok(value), + Number::Float(_) => Err(TryFromNumberError::FloatToInt), + } + } +} + +impl TryFrom for f64 { + type Error = TryFromIntError; + + fn try_from(value: Number) -> Result { + match value { + Number::UnsignedInt(value) => u32::try_from(value).map(Into::into), + Number::SignedInt(value) => i32::try_from(value).map(Into::into), + Number::Float(value) => Ok(value), + } + } +} + +/// Error returned when failing to convert a [`Number`] into another type. +#[derive(Error, Debug, Clone, PartialEq, Eq)] +pub enum TryFromNumberError { + /// Cannot convert from a [`Float`](Number::Float) to an integer. + #[error("cannot convert a float to an integer")] + FloatToInt, /// Error converting integer type. /// - /// For example, converting from [`Value::SignedInt`] to [`u64`] can fail. + /// For example, converting from [`Number::SignedInt`] to [`u64`] can fail. #[error("error converting integer type")] - InvalidInt(#[from] TryFromIntError), + TryFromInt(#[from] TryFromIntError), +} + +/// A string or number value. +#[derive(Serialize, Debug, Clone, PartialEq)] +#[serde(untagged)] +pub enum StringOrNumber { + /// A [`String`] value. + String(String), + + /// A [`Number`] value. + Number(Number), +} + +impl StringOrNumber { + /// Parse a string into a [`StringOrNumber`]. + /// + /// It is first attempted to parse the string into a [`Number`]. + /// If that fails a [`StringOrNumber::String`] is returned. + pub fn parse(value: T) -> Self + where + T: AsRef + Into, + { + value + .as_ref() + .parse() + .map_or_else(|_| Self::String(value.into()), Self::Number) + } + + /// Returns `true` if the value is a [`String`]. + #[must_use] + pub fn is_string(&self) -> bool { + matches!(self, Self::String(..)) + } + + /// Returns [`Some`] if the value is a [`String`]. + #[must_use] + pub fn as_string(&self) -> Option<&String> { + if let Self::String(v) = self { + Some(v) + } else { + None + } + } + + /// Returns `true` if the value is a [`Number`]. + #[must_use] + pub fn is_number(&self) -> bool { + matches!(self, Self::Number(..)) + } - /// Error parsing [`Value::String`] into an integer. - #[error("error parsing string value into an integer")] - ParseInt(#[from] ParseIntError), + /// Returns [`Some`] if the value is a [`Number`]. + #[must_use] + pub fn as_number(&self) -> Option { + if let Self::Number(v) = *self { + Some(v) + } else { + None + } + } +} - /// Error parsing [`Value::String`] into a float. - #[error("error parsing string value into a float")] - ParseFloat(#[from] ParseFloatError), +impl<'de> Deserialize<'de> for StringOrNumber { + fn deserialize>(deserializer: D) -> Result { + ValueEnumVisitor::new("a string, integer, float, or boolean") + .string(Self::String) + .u64(Into::into) + .i64(Into::into) + .f64(Into::into) + .deserialize(deserializer) + } +} + +impl From for StringOrNumber { + fn from(value: String) -> Self { + Self::String(value) + } +} + +impl From> for StringOrNumber { + fn from(value: Box) -> Self { + value.into_string().into() + } +} + +impl From<&str> for StringOrNumber { + fn from(value: &str) -> Self { + value.to_owned().into() + } +} + +impl From> for StringOrNumber { + fn from(value: Cow<'_, str>) -> Self { + value.into_owned().into() + } +} + +impl From for StringOrNumber { + fn from(value: Number) -> Self { + Self::Number(value) + } +} + +impl From for StringOrNumber { + fn from(value: u64) -> Self { + Number::from(value).into() + } +} + +impl From for StringOrNumber { + fn from(value: i64) -> Self { + Number::from(value).into() + } +} + +impl From for StringOrNumber { + fn from(value: f64) -> Self { + Number::from(value).into() + } +} + +impl Display for StringOrNumber { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::String(value) => f.write_str(value), + Self::Number(value) => Display::fmt(value, f), + } + } +} + +impl From for String { + fn from(value: StringOrNumber) -> Self { + match value { + StringOrNumber::String(value) => value, + StringOrNumber::Number(value) => value.to_string(), + } + } +} + +impl TryFrom for Number { + type Error = ParseNumberError; + + fn try_from(value: StringOrNumber) -> Result { + match value { + StringOrNumber::String(value) => value.parse(), + StringOrNumber::Number(value) => Ok(value), + } + } } #[cfg(test)] @@ -525,9 +871,9 @@ mod tests { #[test] fn value_parse() { assert_eq!(Value::parse("true"), Value::Bool(true)); - assert_eq!(Value::parse("1"), Value::UnsignedInt(1)); - assert_eq!(Value::parse("-1"), Value::SignedInt(-1)); - assert_eq!(Value::parse("1.23"), Value::Float(1.23)); + assert_eq!(Value::parse("1"), Value::Number(1_u64.into())); + assert_eq!(Value::parse("-1"), Value::Number((-1_i64).into())); + assert_eq!(Value::parse("1.23"), Value::Number(1.23.into())); assert_eq!( Value::parse("string"), Value::String(String::from("string")), diff --git a/src/lib.rs b/src/lib.rs index 69c6b9c..bbc7830 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ mod common; pub mod duration; mod include; mod name; +pub mod network; mod serde; pub mod service; @@ -24,15 +25,17 @@ use indexmap::IndexMap; pub use self::{ common::{ AsShort, AsShortIter, ExtensionKey, Extensions, Identifier, InvalidExtensionKeyError, - InvalidIdentifierError, InvalidMapKeyError, ItemOrList, ListOrMap, Map, MapKey, - ShortOrLong, Value, YamlValue, + InvalidIdentifierError, InvalidMapKeyError, ItemOrList, ListOrMap, Map, MapKey, Number, + ParseNumberError, ShortOrLong, StringOrNumber, TryFromNumberError, TryFromValueError, + Value, YamlValue, }, include::Include, name::{InvalidNameError, Name}, + network::Network, service::Service, }; -/// The Compose file is a YAML file defining a multi-containers based application. +/// The Compose file is a YAML file defining a containers based application. /// /// Note that the [`Deserialize`] implementations of many types within `Compose` make use of /// [`Deserializer::deserialize_any()`](::serde::de::Deserializer::deserialize_any). This means that @@ -59,9 +62,17 @@ pub struct Compose { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub include: Vec>, + /// The [`Service`]s (containerized computing components) of the application. + /// /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/05-services.md) pub services: IndexMap, + /// Named networks for [`Service`]s to communicate with each other. + /// + /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/06-networks.md) + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub networks: IndexMap>, + /// Extension values, which are (de)serialized via flattening. /// /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/11-extension.md) diff --git a/src/network.rs b/src/network.rs new file mode 100644 index 0000000..f704d3f --- /dev/null +++ b/src/network.rs @@ -0,0 +1,327 @@ +//! Provides [`Network`] for the top-level `networks` field of a [`Compose`](super::Compose) file. + +use std::{ + borrow::Cow, + collections::HashMap, + fmt::{self, Display, Formatter}, + net::IpAddr, + ops::Not, +}; + +use compose_spec_macros::{DeserializeTryFromString, SerializeDisplay}; +use indexmap::IndexMap; +use ipnet::IpNet; +use serde::{ + de::{self, IntoDeserializer}, + ser::SerializeStruct, + Deserialize, Deserializer, Serialize, Serializer, +}; + +use crate::{ + impl_from_str, service::Hostname, Extensions, Identifier, ListOrMap, MapKey, StringOrNumber, +}; + +/// A named network which allows for [`Service`](super::Service)s to communicate with each other. +/// +/// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/06-networks.md) +#[derive(Debug, Clone, PartialEq)] +pub enum Network { + /// Externally managed network. + /// + /// (De)serializes from/to the mapping `external: true`. + /// + /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/06-networks.md#external) + External { + /// A custom name for the network. + /// + /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/06-networks.md#name) + // #[serde(default, skip_serializing_if = "Option::is_none")] + name: Option, + }, + + /// Network configuration. + Config(Config), +} + +impl Network { + /// [`Self::External`] field name. + const EXTERNAL: &'static str = "external"; + + /// `Self::External.name` field. + const NAME: &'static str = "name"; + + /// Returns `true` if the network is [`External`]. + /// + /// [`External`]: Network::External + #[must_use] + pub fn is_external(&self) -> bool { + matches!(self, Self::External { .. }) + } + + /// Returns [`Some`] if the network is [`Config`]. + /// + /// [`Config`]: Network::Config + #[must_use] + pub fn as_config(&self) -> Option<&Config> { + if let Self::Config(v) = self { + Some(v) + } else { + None + } + } + + /// Custom network name, if set. + /// + /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/06-networks.md#name) + #[must_use] + pub fn name(&self) -> Option<&Identifier> { + match self { + Self::External { name } => name.as_ref(), + Self::Config(config) => config.name.as_ref(), + } + } +} + +impl From for Network { + fn from(value: Config) -> Self { + Self::Config(value) + } +} + +impl Serialize for Network { + fn serialize(&self, serializer: S) -> Result { + match self { + Network::External { name } => { + let mut state = + serializer.serialize_struct("Network", 1 + usize::from(name.is_some()))?; + state.serialize_field(Self::EXTERNAL, &true)?; + if let Some(name) = name { + state.serialize_field(Self::NAME, name)?; + } + state.end() + } + Network::Config(config) => config.serialize(serializer), + } + } +} + +impl<'de> Deserialize<'de> for Network { + fn deserialize>(deserializer: D) -> Result { + let mut map = HashMap::::deserialize(deserializer)?; + + let external = map + .remove(Self::EXTERNAL) + .map(bool::deserialize) + .transpose() + .map_err(de::Error::custom)? + .unwrap_or_default(); + + if external { + let name = map + .remove(Self::NAME) + .map(Identifier::deserialize) + .transpose() + .map_err(de::Error::custom)?; + + if map.is_empty() { + Ok(Self::External { name }) + } else { + Err(de::Error::custom( + "cannot set `external` and fields other than `name`", + )) + } + } else { + Config::deserialize(map.into_deserializer()) + .map(Self::Config) + .map_err(de::Error::custom) + } + } +} + +/// [`Network`] configuration. +/// +/// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/06-networks.md) +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +#[serde(rename = "Network")] +pub struct Config { + /// Which driver to use for this network. + /// + /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/06-networks.md#driver) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub driver: Option, + + /// Driver-dependent options. + /// + /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/06-networks.md#driver_opts) + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub driver_opts: IndexMap, + + /// Whether externally managed containers may attach to this network, in addition to + /// [`Service`](super::Service)s. + /// + /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/06-networks.md#attachable) + #[serde(default, skip_serializing_if = "Not::not")] + pub attachable: bool, + + /// Whether to enable IPv6 networking. + /// + /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/06-networks.md#enable_ipv6) + #[serde(default, skip_serializing_if = "Not::not")] + pub enable_ipv6: bool, + + /// Custom IPAM configuration. + /// + /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/06-networks.md#ipam) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ipam: Option, + + /// Whether to isolate this network from external connectivity. + /// + /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/06-networks.md#internal) + #[serde(default, skip_serializing_if = "Not::not")] + pub internal: bool, + + /// Add metadata to the network. + /// + /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/06-networks.md#labels) + #[serde(default, skip_serializing_if = "ListOrMap::is_empty")] + pub labels: ListOrMap, + + /// Custom name for the network. + /// + /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/06-networks.md#name) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// Extension values, which are (de)serialized via flattening. + /// + /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/11-extension.md) + #[serde(flatten)] + pub extensions: Extensions, +} + +/// [`Network`] driver. +/// +/// Default and available values are platform specific. +#[derive(SerializeDisplay, DeserializeTryFromString, Debug, Clone, PartialEq, Eq)] +pub enum Driver { + /// Use the host's networking stack. + Host, + /// Turn off networking. + None, + /// Other network driver. + Other(String), +} + +impl Driver { + /// [`Self::Host`] string value. + const HOST: &'static str = "host"; + + /// [`Self::None`] string value. + const NONE: &'static str = "none"; + + /// Parse a [`Driver`] from a string. + pub fn parse(driver: T) -> Self + where + T: AsRef + Into, + { + match driver.as_ref() { + Self::HOST => Self::Host, + Self::NONE => Self::None, + _ => Self::Other(driver.into()), + } + } + + /// Network driver as a string slice. + #[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 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 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 for String { + fn from(value: Driver) -> Self { + Cow::from(value).into_owned() + } +} + +/// IP address management (IPAM) options for a [`Network`] [`Config`]. +/// +/// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/06-networks.md#ipam) +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] +pub struct Ipam { + /// Custom IPAM driver. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub driver: Option, + + /// IPAM configuration. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub config: Option, + + /// Driver-specific options. + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub options: IndexMap, + + /// Extension values, which are (de)serialized via flattening. + /// + /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/11-extension.md) + #[serde(flatten)] + pub extensions: Extensions, +} + +/// [`Ipam`] configuration. +/// +/// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/06-networks.md#ipam) +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] +pub struct IpamConfig { + /// Network subnet. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub subnet: Option, + + /// Range of IPs from which to allocate container IPs. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ip_range: Option, + + /// IPv4 or IPv6 gateway for the subnet. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub gateway: Option, + + /// Auxiliary IPv4 or IPv6 addresses used by [`Network`] driver, as a mapping from hostnames to + /// IP addresses. + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub aux_addresses: IndexMap, + + /// Extension values, which are (de)serialized via flattening. + /// + /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/11-extension.md) + #[serde(flatten)] + pub extensions: Extensions, +} diff --git a/src/serde.rs b/src/serde.rs index 30a3874..c018966 100644 --- a/src/serde.rs +++ b/src/serde.rs @@ -10,10 +10,7 @@ use std::{ }; use serde::{ - de::{ - self, value::SeqAccessDeserializer, Expected, IntoDeserializer, SeqAccess, Unexpected, - Visitor, - }, + de::{self, value::SeqAccessDeserializer, IntoDeserializer, SeqAccess, Visitor}, Deserialize, Deserializer, }; @@ -40,75 +37,209 @@ macro_rules! forward_visitor { pub(crate) use forward_visitor; #[derive(Debug)] -pub(crate) struct ValueEnumVisitor { +pub(crate) struct ValueEnumVisitor { expecting: &'static str, - visit_bool: Option, - visit_i64: Option, - visit_u64: Option, - visit_f64: Option, - visit_string: Option, + visit_u64: U, + visit_i64: I, + visit_f64: F, + visit_bool: B, + visit_string: S, } -impl ValueEnumVisitor { +impl ValueEnumVisitor { pub fn new(expecting: &'static str) -> Self { Self { expecting, - visit_bool: None, - visit_i64: None, - visit_u64: None, - visit_f64: None, - visit_string: None, + visit_u64: (), + visit_i64: (), + visit_f64: (), + visit_bool: (), + visit_string: (), } } +} - pub fn bool(mut self, visit: B) -> Self { - self.visit_bool = Some(visit); - self +impl ValueEnumVisitor<(), I, F, B, S> { + pub fn u64 V, V>(self, visit_u64: U) -> ValueEnumVisitor { + let Self { + expecting, + visit_u64: (), + visit_i64, + visit_f64, + visit_bool, + visit_string, + } = self; + + ValueEnumVisitor { + expecting, + visit_u64, + visit_i64, + visit_f64, + visit_bool, + visit_string, + } } +} - pub fn i64(mut self, visit: I) -> Self { - self.visit_i64 = Some(visit); - self +impl ValueEnumVisitor { + pub fn i64(self, visit_i64: I) -> ValueEnumVisitor + where + I: FnOnce(i64) -> V, + { + let Self { + expecting, + visit_u64, + visit_i64: (), + visit_f64, + visit_bool, + visit_string, + } = self; + + ValueEnumVisitor { + expecting, + visit_u64, + visit_i64, + visit_f64, + visit_bool, + visit_string, + } } +} - pub fn u64(mut self, visit: U) -> Self { - self.visit_u64 = Some(visit); - self +impl ValueEnumVisitor { + pub fn f64(self, visit_f64: F) -> ValueEnumVisitor + where + F: FnOnce(f64) -> V, + { + let Self { + expecting, + visit_u64, + visit_i64, + visit_f64: (), + visit_bool, + visit_string, + } = self; + + ValueEnumVisitor { + expecting, + visit_u64, + visit_i64, + visit_f64, + visit_bool, + visit_string, + } } +} - pub fn f64(mut self, visit: F) -> Self { - self.visit_f64 = Some(visit); - self +impl ValueEnumVisitor { + pub fn bool(self, visit_bool: B) -> ValueEnumVisitor + where + B: FnOnce(bool) -> V, + { + let Self { + expecting, + visit_u64, + visit_i64, + visit_f64, + visit_bool: (), + visit_string, + } = self; + + ValueEnumVisitor { + expecting, + visit_u64, + visit_i64, + visit_f64, + visit_bool, + visit_string, + } } +} - pub fn string(mut self, visit: S) -> Self { - self.visit_string = Some(visit); - self +impl ValueEnumVisitor { + pub fn string(self, visit_string: S) -> ValueEnumVisitor + where + S: FnOnce(String) -> V, + { + let Self { + expecting, + visit_u64, + visit_i64, + visit_f64, + visit_bool, + visit_string: (), + } = self; + + ValueEnumVisitor { + expecting, + visit_u64, + visit_i64, + visit_f64, + visit_bool, + visit_string, + } } +} - pub fn deserialize<'de, D, V>(self, deserializer: D) -> Result +impl ValueEnumVisitor { + pub fn deserialize<'de, V, D>(self, deserializer: D) -> Result where D: Deserializer<'de>, Self: Visitor<'de, Value = V>, { deserializer.deserialize_any(self) } +} - fn invalid_type(&self, unexpected: Unexpected) -> E - where - Self: Expected, - { - de::Error::invalid_type(unexpected, self) +impl<'de, U, I, F, B, S, V> Visitor<'de> for ValueEnumVisitor +where + U: FnOnce(u64) -> V, + I: FnOnce(i64) -> V, + F: FnOnce(f64) -> V, + B: FnOnce(bool) -> V, + S: FnOnce(String) -> V, +{ + type Value = V; + + fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { + formatter.write_str(self.expecting) + } + + fn visit_bool(self, v: bool) -> Result { + let Self { visit_bool, .. } = self; + Ok(visit_bool(v)) + } + + fn visit_i64(self, v: i64) -> Result { + let Self { visit_i64, .. } = self; + Ok(visit_i64(v)) + } + + fn visit_u64(self, v: u64) -> Result { + let Self { visit_u64, .. } = self; + Ok(visit_u64(v)) + } + + fn visit_f64(self, v: f64) -> Result { + let Self { visit_f64, .. } = self; + Ok(visit_f64(v)) + } + + fn visit_str(self, v: &str) -> Result { + self.visit_string(v.to_owned()) + } + + fn visit_string(self, v: String) -> Result { + let Self { visit_string, .. } = self; + Ok(visit_string(v)) } } -impl<'de, B, I, U, F, S, V> Visitor<'de> for ValueEnumVisitor +impl<'de, U, I, F, V> Visitor<'de> for ValueEnumVisitor where - B: FnOnce(bool) -> V, - I: FnOnce(i64) -> V, U: FnOnce(u64) -> V, + I: FnOnce(i64) -> V, F: FnOnce(f64) -> V, - S: FnOnce(String) -> V, { type Value = V; @@ -116,70 +247,57 @@ where formatter.write_str(self.expecting) } - fn visit_bool(self, v: bool) -> Result - where - E: de::Error, - { - if let Some(visit_bool) = self.visit_bool { - Ok(visit_bool(v)) - } else { - Err(self.invalid_type(Unexpected::Bool(v))) - } + fn visit_i64(self, v: i64) -> Result { + let Self { visit_i64, .. } = self; + Ok(visit_i64(v)) } - fn visit_i64(self, v: i64) -> Result - where - E: de::Error, - { - if let Some(visit_i64) = self.visit_i64 { - Ok(visit_i64(v)) - } else { - Err(self.invalid_type(Unexpected::Signed(v))) - } + fn visit_u64(self, v: u64) -> Result { + let Self { visit_u64, .. } = self; + Ok(visit_u64(v)) } - fn visit_u64(self, v: u64) -> Result - where - E: de::Error, - { - if let Some(visit_u64) = self.visit_u64 { - Ok(visit_u64(v)) - } else { - Err(self.invalid_type(Unexpected::Unsigned(v))) - } + fn visit_f64(self, v: f64) -> Result { + let Self { visit_f64, .. } = self; + Ok(visit_f64(v)) } +} - fn visit_f64(self, v: f64) -> Result - where - E: de::Error, - { - if let Some(visit_f64) = self.visit_f64 { - Ok(visit_f64(v)) - } else { - Err(self.invalid_type(Unexpected::Float(v))) - } +impl<'de, U, I, F, S, V> Visitor<'de> for ValueEnumVisitor +where + U: FnOnce(u64) -> V, + I: FnOnce(i64) -> V, + F: FnOnce(f64) -> V, + S: FnOnce(String) -> V, +{ + type Value = V; + + fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { + formatter.write_str(self.expecting) } - fn visit_str(self, v: &str) -> Result - where - E: de::Error, - { - if let Some(visit_string) = self.visit_string { - Ok(visit_string(v.to_owned())) - } else { - Err(self.invalid_type(Unexpected::Str(v))) - } + fn visit_i64(self, v: i64) -> Result { + let Self { visit_i64, .. } = self; + Ok(visit_i64(v)) } - fn visit_string(self, v: String) -> Result - where - E: de::Error, - { - if let Some(visit_string) = self.visit_string { - Ok(visit_string(v)) - } else { - Err(self.invalid_type(Unexpected::Str(&v))) - } + fn visit_u64(self, v: u64) -> Result { + let Self { visit_u64, .. } = self; + Ok(visit_u64(v)) + } + + fn visit_f64(self, v: f64) -> Result { + let Self { visit_f64, .. } = self; + Ok(visit_f64(v)) + } + + fn visit_str(self, v: &str) -> Result { + self.visit_string(v.to_owned()) + } + + fn visit_string(self, v: String) -> Result { + let Self { visit_string, .. } = self; + Ok(visit_string(v)) } } diff --git a/src/service.rs b/src/service.rs index fa0e50a..7b82da6 100644 --- a/src/service.rs +++ b/src/service.rs @@ -43,7 +43,7 @@ use crate::{ ItemOrListVisitor, }, AsShortIter, Extensions, Identifier, InvalidIdentifierError, ItemOrList, ListOrMap, Map, - ShortOrLong, Value, + MapKey, ShortOrLong, StringOrNumber, Value, }; use self::build::Context; @@ -75,6 +75,10 @@ pub use self::{ /// A service is an abstract definition of a computing resource within an application which can be /// scaled or replaced independently from other components. /// +/// Services are backed by a set of containers, run by the platform according to replication +/// requirements and placement constraints. They are defined by a container image and set of runtime +/// arguments. All containers within a service are identically created with these arguments. +/// /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/05-services.md) #[allow(clippy::struct_excessive_bools)] #[derive(Serialize, Deserialize, Debug, compose_spec_macros::Default, Clone, PartialEq)] @@ -993,8 +997,8 @@ pub struct Logging { pub driver: Option, /// Driver specific options. - #[serde(default, skip_serializing_if = "Map::is_empty")] - pub options: Map, + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub options: IndexMap>, /// Extension values, which are (de)serialized via flattening. /// diff --git a/src/service/build/network.rs b/src/service/build/network.rs index 97919c3..4225569 100644 --- a/src/service/build/network.rs +++ b/src/service/build/network.rs @@ -7,7 +7,7 @@ use std::{ use compose_spec_macros::{DeserializeTryFromString, SerializeDisplay}; -use crate::impl_from_str; +use crate::{impl_from_str, Identifier, InvalidIdentifierError}; /// Network containers connect to during [`Build`](super::Build) for `RUN` instructions. /// @@ -15,9 +15,7 @@ use crate::impl_from_str; #[derive(SerializeDisplay, DeserializeTryFromString, Debug, Clone, PartialEq, Eq)] pub enum Network { /// Network to connect to during build. - /// - /// A compose implementation may have more specific network kinds such as "host". - String(String), + Identifier(Identifier), /// Disable networking during build. None, @@ -30,14 +28,18 @@ impl Network { /// Parse [`Network`] from a string. /// /// "none" converts to [`Network::None`]. - pub fn parse(network: T) -> Self + /// + /// # Errors + /// + /// Returns an error if the network is not a valid [`Identifier`] + pub fn parse(network: T) -> Result where - T: AsRef + Into, + T: AsRef + TryInto, { if network.as_ref() == Self::NONE { - Self::None + Ok(Self::None) } else { - Self::String(network.into()) + network.try_into().map(Self::Identifier) } } @@ -47,7 +49,7 @@ impl Network { #[must_use] pub fn as_str(&self) -> &str { match self { - Self::String(string) => string, + Self::Identifier(network) => network.as_str(), Self::None => Self::NONE, } } @@ -62,17 +64,18 @@ impl Network { /// Convert into [`Option`]. /// - /// [`Network::String`] converts to [`Option::Some`] and [`Network::None`] to [`Option::None`]. + /// [`Network::Identifier`] converts into [`Option::Some`] and [`Network::None`] into + /// [`Option::None`]. #[must_use] - pub fn into_option(self) -> Option { + pub fn into_option(self) -> Option { match self { - Self::String(string) => Some(string), + Self::Identifier(network) => Some(network), Self::None => None, } } } -impl_from_str!(Network); +impl_from_str!(Network => InvalidIdentifierError); impl AsRef for Network { fn as_ref(&self) -> &str { @@ -89,7 +92,7 @@ impl Display for Network { impl From for Cow<'static, str> { fn from(value: Network) -> Self { match value { - Network::String(string) => string.into(), + Network::Identifier(network) => Self::Owned(network.into()), Network::None => Self::Borrowed(Network::NONE), } } @@ -100,9 +103,3 @@ impl From for String { Cow::from(value).into_owned() } } - -impl From for Option { - fn from(value: Network) -> Self { - value.into_option() - } -}