use std::{any::type_name, collections::BTreeMap};
use anyhow::Context;
use scion_proto::address::IsdAsn;
use serde::{Deserialize, Serialize};
use serde_with::{base64::Base64, serde_as};
use utoipa::{
PartialSchema, ToSchema,
openapi::{Object, Type, schema::SchemaType},
};
use crate::network::scion::{
topology::{ScionAs, ScionLink, ScionRouter, ScionTopology},
trust_store::TrustStore,
};
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ScionTopologyDto {
trust_store: TrustStore,
as_list: Vec<ScionAsDto>,
links: Vec<ScionLinkDto>,
routers: BTreeMap<IsdAsn, Vec<ScionRouter>>,
}
impl TryFrom<ScionTopologyDto> for ScionTopology {
type Error = anyhow::Error;
fn try_from(value: ScionTopologyDto) -> Result<Self, Self::Error> {
let mut topo = ScionTopology::default();
for as_info in value.as_list {
let scion_as: ScionAs = as_info.into();
topo.add_as(scion_as.clone())
.with_context(|| format!("error adding AS {:?} to topology", scion_as))?;
}
for link in value.links {
let scion_link: ScionLink = link.0;
topo.add_link(scion_link)
.context("error adding link to topology")?;
}
topo.set_trust_store(value.trust_store)
.context("Topology trust store misconfigured")?;
for (isd_as, routers) in value.routers {
for router in routers {
topo.add_router(isd_as, router)
.context("error adding router to topology")?;
}
}
Ok(topo)
}
}
impl From<ScionTopology> for ScionTopologyDto {
fn from(topo: ScionTopology) -> Self {
let mut registered_ases = Vec::new();
let mut links = Vec::new();
for (_isd_as, scion_as) in topo.as_map.into_iter() {
registered_ases.push(scion_as.into());
}
for scion_link in topo.link_map.into_values() {
links.push(ScionLinkDto(scion_link));
}
Self {
as_list: registered_ases,
links,
trust_store: topo.trust_store,
routers: topo.router_map,
}
}
}
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ScionAsDto {
Simulated {
isd_asn: IsdAsn,
is_core_as: bool,
#[serde_as(as = "Base64")]
forwarding_key: [u8; 16],
},
External {
isd_asn: IsdAsn,
is_core_as: bool,
},
}
impl From<ScionAs> for ScionAsDto {
fn from(scion_as: ScionAs) -> Self {
match scion_as {
ScionAs::Simulated {
isd_as,
core,
forwarding_key,
} => {
Self::Simulated {
isd_asn: isd_as,
is_core_as: core,
forwarding_key,
}
}
ScionAs::External { isd_as, core } => {
Self::External {
isd_asn: isd_as,
is_core_as: core,
}
}
}
}
}
impl From<ScionAsDto> for ScionAs {
fn from(dto: ScionAsDto) -> Self {
match dto {
ScionAsDto::Simulated {
isd_asn,
is_core_as,
forwarding_key,
} => {
ScionAs::Simulated {
isd_as: isd_asn,
core: is_core_as,
forwarding_key,
}
}
ScionAsDto::External {
isd_asn,
is_core_as,
} => {
ScionAs::External {
isd_as: isd_asn,
core: is_core_as,
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct ScionLinkDto(pub ScionLink);
impl<'de> Deserialize<'de> for ScionLinkDto {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let string = String::deserialize(deserializer)?;
let inner = string.parse().map_err(serde::de::Error::custom)?;
Ok(Self(inner))
}
}
impl Serialize for ScionLinkDto {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.0.to_string())
}
}
impl ToSchema for ScionLinkDto {
fn name() -> std::borrow::Cow<'static, str> {
type_name::<Self>().into()
}
}
impl PartialSchema for ScionLinkDto {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
Object::builder()
.schema_type(SchemaType::Type(Type::String))
.examples(vec![serde_json::json!("1-ff00:0:110 parent_of ff00:0:111")])
.into()
}
}
#[cfg(test)]
mod test {
use std::str::FromStr;
use super::*;
#[test]
fn should_convert_to_from_domain_losslessly() {
let mut topo = ScionTopology::default();
let isd_asn1 = IsdAsn::from_str("1-ff00:0:110").unwrap();
topo.add_as(ScionAs::new_core(isd_asn1).with_forwarding_key([1; 16]))
.unwrap();
topo.add_as(
ScionAs::new_core(IsdAsn::from_str("1-ff00:0:111").unwrap())
.with_forwarding_key([2; 16]),
)
.unwrap();
topo.add_as(ScionAs::External {
isd_as: IsdAsn::from_str("2-ff00:0:210").unwrap(),
core: false,
})
.unwrap();
let link = ScionLink::from_str("1-ff00:0:110#1 core 1-ff00:0:111#1").unwrap();
topo.add_link(link).expect("adding link to topology");
let state: ScionTopologyDto = topo.clone().into();
let converted_topo: ScionTopology = state.try_into().unwrap();
assert_eq!(
topo.as_map, converted_topo.as_map,
"AS map did not match after conversion"
);
assert_eq!(
topo.link_map, converted_topo.link_map,
"Link map did not match after conversion"
);
assert_eq!(
topo.trust_store, converted_topo.trust_store,
"Trust store did not match after conversion"
);
assert_eq!(
topo, converted_topo,
"Topologies did not match after conversion"
)
}
}