use itertools::Itertools;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use std::sync::LazyLock;
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub enum AuthString {
Basic { realm: Option<String>, username: String },
Fwd { authentication_service_endpoint: String, headers: Option<String> },
SystemFwd { accepted_roles: String },
}
impl AuthString {
pub fn basic<T, U>(realm: Option<T>, username: U) -> Self
where
T: Into<String>,
U: Into<String>,
{
Self::Basic { realm: realm.map(Into::<String>::into), username: username.into() }
}
pub fn fwd<T, U>(endpoint: T, headers: Option<U>) -> Self
where
T: Into<String>,
U: Into<String>,
{
Self::Fwd { authentication_service_endpoint: endpoint.into(), headers: headers.map(Into::into) }
}
pub fn system_fwd<T>(roles: T) -> Self
where
T: Into<String>,
{
Self::SystemFwd { accepted_roles: roles.into() }
}
}
impl FromStr for AuthString {
type Err = String;
fn from_str(auth_string: &str) -> Result<Self, Self::Err> {
if let Some(basic_authentication_string) = auth_string.strip_prefix("basic-auth@") {
parse_basic_authentication_string(basic_authentication_string)
} else if let Some(fwd_auth_string) = auth_string.strip_prefix("fwd-auth@") {
let split_string = fwd_auth_string.split("@").collect_vec();
match split_string.first() {
Some(authentication_service) => Ok(Self::fwd(*authentication_service, split_string.get(1).map(|headers| headers.to_string()))),
None => Err(format!("invalid forward authentication string (\"{}\")", auth_string)),
}
} else if let Some(roles) = auth_string.strip_prefix("system-fwd-auth@") {
Ok(Self::system_fwd(roles))
} else {
parse_basic_authentication_string(auth_string)
}
}
}
impl Display for AuthString {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Basic { realm, username } => match realm {
Some(realm) => write!(f, "basic@{}:{}", realm, username),
None => write!(f, "basic@{}", username),
},
Self::Fwd { authentication_service_endpoint, headers } => match headers {
Some(headers) => write!(f, "fwd@{}@{}", authentication_service_endpoint, headers),
None => write!(f, "fwd@{}", authentication_service_endpoint),
},
Self::SystemFwd { accepted_roles } => write!(f, "sys-fwd@{}", accepted_roles),
}
}
}
#[derive(Clone, Debug, Deserialize, Hash, PartialEq, Serialize)]
pub struct RegistryImage {
pub tenant: String,
pub id: String,
pub version: String,
}
#[derive(Clone, Debug, Deserialize, Hash, PartialEq, Serialize)]
pub struct AppImage {
pub stage: String,
pub supplier: String,
pub tenant: String,
pub id: String,
pub version: String,
}
#[derive(Clone, Debug, Deserialize, Hash, PartialEq, Serialize)]
pub enum ImageString {
Registry { image: RegistryImage },
App { image: AppImage },
Unrecognized { image: String },
}
impl ImageString {
pub fn registry(tenant: String, id: String, version: String) -> Self {
Self::Registry { image: RegistryImage { tenant, id, version } }
}
pub fn app(stage: String, supplier: String, tenant: String, id: String, version: String) -> Self {
Self::App { image: AppImage { stage, supplier, tenant, id, version } }
}
pub fn id(&self) -> String {
match self {
ImageString::Registry { image } => image.id.clone(),
ImageString::App { image } => image.id.clone(),
ImageString::Unrecognized { image } => image.to_string(),
}
}
pub fn source(&self) -> &str {
match self {
ImageString::Registry { .. } => "harbor",
ImageString::App { .. } => "app-catalog",
ImageString::Unrecognized { .. } => "",
}
}
pub fn tenant(&self) -> String {
match self {
ImageString::Registry { image } => image.tenant.clone(),
ImageString::App { image } => image.tenant.clone(),
ImageString::Unrecognized { .. } => "".to_string(),
}
}
pub fn version(&self) -> String {
match self {
ImageString::Registry { image } => image.version.clone(),
ImageString::App { image } => image.version.clone(),
ImageString::Unrecognized { .. } => "".to_string(),
}
}
}
impl From<&str> for ImageString {
fn from(image_string: &str) -> Self {
static APP_CATALOG_IMAGE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"APPCATALOG_REGISTRY/dsh-appcatalog/tenant/([a-z0-9-_]+)/([0-9]+)/([0-9]+)/(release|draft)/(klarrio|kpn)/([a-zA-Z][a-zA-Z0-9-_]*):([a-zA-Z0-9-_.]*)").unwrap()
});
static REGISTRY_IMAGE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"registry.cp.kpn-dsh.com/([a-z0-9-_]+)/([a-zA-Z][a-zA-Z0-9-_]*):([a-zA-Z0-9-_.]*)").unwrap());
match APP_CATALOG_IMAGE_REGEX.captures(image_string) {
Some(captures) => Self::app(
captures.get(4).map(|stage| stage.as_str().to_string()).unwrap_or_default(),
captures.get(5).map(|supplier| supplier.as_str().to_string()).unwrap_or_default(),
captures.get(1).map(|tenant| tenant.as_str().to_string()).unwrap_or_default(),
captures.get(6).map(|id| id.as_str().to_string()).unwrap_or_default(),
captures.get(7).map(|version| version.as_str().to_string()).unwrap_or_default(),
),
None => match REGISTRY_IMAGE_REGEX.captures(image_string) {
Some(registry_captures) => Self::registry(
registry_captures.get(1).map(|tenant| tenant.as_str().to_string()).unwrap_or_default(),
registry_captures.get(2).map(|id| id.as_str().to_string()).unwrap_or_default(),
registry_captures.get(3).map(|version| version.as_str().to_string()).unwrap_or_default(),
),
None => ImageString::Unrecognized { image: image_string.to_string() },
},
}
}
}
impl Display for ImageString {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ImageString::Registry { image } => write!(f, "registry:{}:{}:{}", image.tenant, image.id, image.version),
ImageString::App { image } => write!(f, "app:{}:{}:{}:{}:{}", image.stage, image.supplier, image.tenant, image.id, image.version),
ImageString::Unrecognized { image } => write!(f, "{}", image),
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub enum TopicString<'a> {
Internal { name: &'a str, tenant: &'a str },
Scratch { name: &'a str, tenant: &'a str },
Stream { name: &'a str, tenant: &'a str },
}
impl<'a> TopicString<'a> {
pub fn internal(name: &'a str, tenant: &'a str) -> Self {
Self::Internal { name, tenant }
}
pub fn scratch(name: &'a str, tenant: &'a str) -> Self {
Self::Scratch { name, tenant }
}
pub fn stream(name: &'a str, tenant: &'a str) -> Self {
Self::Stream { name, tenant }
}
pub fn name(&self) -> &str {
match self {
TopicString::Internal { name, .. } => name,
TopicString::Scratch { name, .. } => name,
TopicString::Stream { name, .. } => name,
}
}
pub fn tenant(&self) -> &str {
match self {
TopicString::Internal { tenant, .. } => tenant,
TopicString::Scratch { tenant, .. } => tenant,
TopicString::Stream { tenant, .. } => tenant,
}
}
}
impl<'a> TryFrom<&'a str> for TopicString<'a> {
type Error = String;
fn try_from(topic_string: &'a str) -> Result<Self, Self::Error> {
parse_topic_string(topic_string)
}
}
impl Display for TopicString<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
TopicString::Internal { name, tenant } => write!(f, "internal.{}.{}", name, tenant),
TopicString::Scratch { name, tenant } => write!(f, "scratch.{}.{}", name, tenant),
TopicString::Stream { name, tenant } => write!(f, "stream.{}.{}", name, tenant),
}
}
}
pub fn parse_basic_authentication_string(basic_authentication_string: &str) -> Result<AuthString, String> {
let parts = basic_authentication_string.split(":").collect_vec();
if parts.len() == 2 {
Ok(AuthString::basic(None::<String>, *parts.first().unwrap()))
} else if parts.len() == 3 {
Ok(AuthString::basic(Some(*parts.first().unwrap()), *parts.get(1).unwrap()))
} else {
Err(format!("invalid basic authentication string (\"{}\")", basic_authentication_string))
}
}
pub fn parse_function1<'a>(string: &'a str, f_name: &str) -> Result<&'a str, String> {
static FUNCTION1_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\{\s*([a-z][a-z0-9_]*)\(\s*'([a-zA-Z0-9_.-]*)'\s*\)\s*}").unwrap());
match FUNCTION1_REGEX.captures(string).map(|captures| {
(
captures.get(1).map(|first_match| first_match.as_str()),
captures.get(2).map(|second_match| second_match.as_str()),
)
}) {
Some((Some(function), Some(par))) if function == f_name => Ok(par),
_ => Err(format!("invalid {} string (\"{}\")", f_name, string)),
}
}
pub fn parse_function2<'a>(string: &'a str, f_name: &str) -> Result<(&'a str, &'a str), String> {
static FUNCTION2_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\{\s*([a-z][a-z0-9_]*)\(\s*'([a-zA-Z0-9_.-]*)'\s*,\s*'([a-zA-Z0-9_.-]*)'\s*\)\s*}").unwrap());
match FUNCTION2_REGEX.captures(string).map(|captures| {
(
captures.get(1).map(|first_match| first_match.as_str()),
captures.get(2).map(|second_match| second_match.as_str()),
captures.get(3).map(|second_match| second_match.as_str()),
)
}) {
Some((Some(function), Some(par1), Some(par2))) if function == f_name => Ok((par1, par2)),
_ => Err(format!("invalid {} string (\"{}\")", f_name, string)),
}
}
pub fn parse_function<'a>(string: &'a str, f_name: &str) -> Result<(&'a str, Option<&'a str>), String> {
match parse_function2(string, f_name) {
Ok((first_parameter, second_parameter)) => Ok((first_parameter, Some(second_parameter))),
Err(_) => match parse_function1(string, f_name) {
Ok(parameter) => Ok((parameter, None)),
Err(_) => Err(format!("invalid {} string (\"{}\")", f_name, string)),
},
}
}
pub fn parse_volume_string(volume_string: &str) -> Result<&str, String> {
parse_function1(volume_string, "volume")
}
pub fn parse_topic_string<'a>(topic_string: &'a str) -> Result<TopicString<'a>, String> {
static TOPIC_STRING_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(internal|stream|scratch)\.([a-z][a-z0-9-]*)\.([a-z][a-z0-9-]*)").unwrap());
match TOPIC_STRING_REGEX.captures(topic_string) {
Some(registry_captures) => {
let name = registry_captures.get(2).map(|name| name.as_str()).unwrap();
let tenant = registry_captures.get(3).map(|tenant| tenant.as_str()).unwrap();
match registry_captures.get(1).map(|kind| kind.as_str()) {
Some("internal") => Ok(TopicString::internal(name, tenant)),
Some("stream") => Ok(TopicString::stream(name, tenant)),
Some("scratch") => Ok(TopicString::scratch(name, tenant)),
_ => unreachable!(),
}
}
None => Err(format!("illegal topic name {}", topic_string)),
}
}