use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
use url::Url;
#[cfg(doc)]
use crate::api::compute::v2::FormationsRequest;
use crate::{
api::compute::{
error::{ComputeError, FormationValidation},
v2::validate_formation_name,
},
error::Result,
rexports::{
container_image_ref::ImageReference,
seaplane_oid::{OidPrefix, TypedOid},
},
};
#[doc(hidden)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Frm;
impl OidPrefix for Frm {}
pub type FormationId = TypedOid<Frm>;
#[doc(hidden)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Flt;
impl OidPrefix for Flt {}
pub type FlightId = TypedOid<Flt>;
#[derive(Debug, Copy, Clone, PartialEq, Eq, EnumString, Display, Default)]
#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
pub enum FlightStatus {
Healthy,
Unhealthy,
#[default]
Starting,
}
impl FlightStatus {
pub fn is_starting(&self) -> bool { self == &FlightStatus::Starting }
}
impl_serde_str!(FlightStatus);
#[cfg(test)]
mod flight_health_status_tests {
use super::*;
#[test]
fn deser() {
assert_eq!(FlightStatus::Healthy, "healthy".parse().unwrap());
assert_eq!(FlightStatus::Healthy, "Healthy".parse().unwrap());
assert_eq!(FlightStatus::Healthy, "HEALTHY".parse().unwrap());
assert_eq!(FlightStatus::Unhealthy, "unhealthy".parse().unwrap());
assert_eq!(FlightStatus::Unhealthy, "Unhealthy".parse().unwrap());
assert_eq!(FlightStatus::Unhealthy, "UNHEALTHY".parse().unwrap());
assert_eq!(FlightStatus::Starting, "starting".parse().unwrap());
assert_eq!(FlightStatus::Starting, "Starting".parse().unwrap());
assert_eq!(FlightStatus::Starting, "STARTING".parse().unwrap());
}
#[test]
fn ser() {
assert_eq!(FlightStatus::Healthy.to_string(), "healthy".to_string());
assert_eq!(FlightStatus::Unhealthy.to_string(), "unhealthy".to_string());
assert_eq!(FlightStatus::Starting.to_string(), "starting".to_string());
}
}
#[derive(Debug, Default)]
pub struct FormationBuilder {
flights: Vec<Flight>,
name: String,
gateway_flight: Option<String>,
}
impl FormationBuilder {
#[must_use]
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
#[must_use]
pub fn add_flight(mut self, flight: Flight) -> Self {
self.flights.push(flight);
self
}
pub fn gateway_flight(mut self, flight: impl Into<String>) -> Self {
self.gateway_flight = Some(flight.into());
self
}
pub fn clear_flights(&mut self) { self.flights.clear(); }
pub fn build(self) -> Result<Formation> {
use FormationValidation::*;
if self.flights.is_empty() {
return Err(ComputeError::FormationValidation(EmptyFlights).into());
}
if self
.gateway_flight
.as_ref()
.map(|gw_f| self.flights.iter().any(|f| &f.name == gw_f))
== Some(false)
{
return Err(ComputeError::FormationValidation(InvalidGatewayFlight).into());
}
validate_formation_name(&self.name).map_err(ComputeError::FormationValidation)?;
Ok(Formation {
name: self.name,
oid: None,
url: None,
flights: self.flights,
gateway_flight: self.gateway_flight,
})
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub struct Formation {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub oid: Option<FormationId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<Url>,
pub flights: Vec<Flight>,
pub gateway_flight: Option<String>,
}
impl Formation {
pub fn builder() -> FormationBuilder { FormationBuilder::default() }
pub fn add_flight(&mut self, flight: Flight) { self.flights.push(flight); }
pub fn remove_flight(&mut self, name: &str) -> Option<Flight> {
if let Some(i) =
self.flights
.iter()
.enumerate()
.find_map(|(i, f)| if f.name == name { Some(i) } else { None })
{
Some(self.flights.swap_remove(i))
} else {
None
}
}
pub fn set_flights(&mut self, flights: Vec<Flight>) { self.flights = flights; }
pub fn flights(&self) -> &[Flight] { &self.flights }
}
#[cfg(test)]
mod formation_tests {
use super::*;
#[test]
fn deser() {
let json = r#"{
"name": "example-formation",
"oid": "frm-sjt5inobm97i317b95uerqv080",
"url": "https://example-formation.tenant.on.cplane.cloud",
"flights": [{
"name":"example-flight",
"oid":"flt-6f9asfo8ql0ar3mihb0ruv14i0",
"image":"foo.com/bar:latest"
}],
"gateway-flight": "example-flight"
}"#;
let model = Formation {
name: "example-formation".into(),
oid: Some("frm-sjt5inobm97i317b95uerqv080".parse().unwrap()),
url: Some(
"https://example-formation.tenant.on.cplane.cloud"
.parse()
.unwrap(),
),
flights: vec![Flight {
name: "example-flight".into(),
oid: Some("flt-6f9asfo8ql0ar3mihb0ruv14i0".parse().unwrap()),
image: "foo.com/bar:latest".parse::<ImageReference>().unwrap(),
status: FlightStatus::Starting,
}],
gateway_flight: Some("example-flight".into()),
};
assert_eq!(model, serde_json::from_str(json).unwrap());
}
#[test]
fn ser() {
let json = r#"{"name":"example-formation","oid":"frm-sjt5inobm97i317b95uerqv080","flights":[{"name":"example-flight","oid":"flt-6f9asfo8ql0ar3mihb0ruv14i0","image":"foo.com/bar:latest"}],"gateway-flight":"example-flight"}"#;
let model = Formation {
name: "example-formation".into(),
oid: Some("frm-sjt5inobm97i317b95uerqv080".parse().unwrap()),
url: None,
flights: vec![Flight {
name: "example-flight".into(),
oid: Some("flt-6f9asfo8ql0ar3mihb0ruv14i0".parse().unwrap()),
image: "foo.com/bar:latest".parse::<ImageReference>().unwrap(),
status: FlightStatus::Starting,
}],
gateway_flight: Some("example-flight".into()),
};
assert_eq!(json.to_string(), serde_json::to_string(&model).unwrap());
}
#[test]
fn ser_no_oid() {
let json = r#"{"name":"example-formation","flights":[{"name":"example-flight","image":"foo.com/bar:latest","status":"healthy"}],"gateway-flight":"example-flight"}"#;
let model = Formation {
name: "example-formation".into(),
url: None,
oid: None,
flights: vec![Flight {
name: "example-flight".into(),
oid: None,
image: "foo.com/bar:latest".parse::<ImageReference>().unwrap(),
status: FlightStatus::Healthy,
}],
gateway_flight: Some("example-flight".into()),
};
assert_eq!(json.to_string(), serde_json::to_string(&model).unwrap());
}
}
#[derive(Debug, Default)]
pub struct FlightBuilder {
name: Option<String>,
image: Option<ImageReference>,
}
impl FlightBuilder {
pub fn new() -> Self { Self::default() }
#[must_use]
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn image<R: AsRef<str>>(mut self, image_ref: R) -> Self {
self.image = Some(
image_ref
.as_ref()
.parse::<ImageReference>()
.expect("Failed to parse image reference"),
);
self
}
#[must_use]
pub fn image_reference(mut self, image_ref: ImageReference) -> Self {
self.image = Some(image_ref);
self
}
pub fn build(self) -> Result<Flight> {
use FormationValidation::*;
if self.name.is_none() {
return Err(ComputeError::FormationValidation(MissingFlightName).into());
} else if self.image.is_none() {
return Err(ComputeError::FormationValidation(MissingFlightImageReference).into());
}
Ok(Flight {
name: self.name.unwrap(),
oid: None,
image: self.image.unwrap(),
status: FlightStatus::default(),
})
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
#[non_exhaustive]
#[serde(rename_all = "kebab-case")]
pub struct Flight {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub oid: Option<FlightId>,
pub image: ImageReference,
#[serde(default, skip_serializing_if = "FlightStatus::is_starting")]
pub status: FlightStatus,
}
impl Flight {
pub fn builder() -> FlightBuilder { FlightBuilder::new() }
pub fn new<S, R>(name: S, image_ref: R) -> Flight
where
S: Into<String>,
R: AsRef<str>,
{
FlightBuilder::new()
.name(name)
.image(image_ref)
.build()
.unwrap()
}
#[inline]
pub fn name(&self) -> &str { &self.name }
#[inline]
pub fn image_str(&self) -> String { self.image.to_string() }
#[inline]
pub fn image(&self) -> &ImageReference { &self.image }
}
#[cfg(test)]
mod flight_tests {
use super::*;
#[test]
fn deser() {
let json = r#"{
"name":"example-flight",
"oid":"flt-6f9asfo8ql0ar3mihb0ruv14i0",
"image":"foo.com/bar:latest"
}"#;
let model = Flight {
name: "example-flight".into(),
oid: Some("flt-6f9asfo8ql0ar3mihb0ruv14i0".parse().unwrap()),
image: "foo.com/bar:latest".parse::<ImageReference>().unwrap(),
status: FlightStatus::Starting,
};
assert_eq!(model, serde_json::from_str(json).unwrap());
}
#[test]
fn ser() {
let json = r#"{"name":"example-flight","oid":"flt-6f9asfo8ql0ar3mihb0ruv14i0","image":"foo.com/bar:latest","status":"healthy"}"#;
let model = Flight {
name: "example-flight".into(),
oid: Some("flt-6f9asfo8ql0ar3mihb0ruv14i0".parse().unwrap()),
image: "foo.com/bar:latest".parse::<ImageReference>().unwrap(),
status: FlightStatus::Healthy,
};
assert_eq!(json, serde_json::to_string(&model).unwrap());
}
#[test]
fn ser_no_oid() {
let json = r#"{"name":"example-flight","image":"foo.com/bar:latest","status":"healthy"}"#;
let model = Flight {
name: "example-flight".into(),
oid: None,
image: "foo.com/bar:latest".parse::<ImageReference>().unwrap(),
status: FlightStatus::Healthy,
};
assert_eq!(json, serde_json::to_string(&model).unwrap());
}
}