Merge pull request 'feat: PostgreSQLScore happy path using cnpg operator' (#200) from feat/postgresqlScore into master
Reviewed-on: #200 Reviewed-by: wjro <wrolleman@nationtech.io>
This commit is contained in:
@@ -9,4 +9,5 @@ license.workspace = true
|
||||
serde.workspace = true
|
||||
url.workspace = true
|
||||
rand.workspace = true
|
||||
serde_json.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod id;
|
||||
pub mod k8s_name;
|
||||
pub mod net;
|
||||
pub mod rfc1123;
|
||||
pub mod storage;
|
||||
pub mod switch;
|
||||
|
||||
231
harmony_types/src/rfc1123.rs
Normal file
231
harmony_types/src/rfc1123.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
/// A String that can be used as a subdomain.
|
||||
///
|
||||
/// This means the name must:
|
||||
///
|
||||
/// - contain no more than 253 characters
|
||||
/// - contain only lowercase alphanumeric characters, '-' or '.'
|
||||
/// - start with an alphanumeric character
|
||||
/// - end with an alphanumeric character
|
||||
///
|
||||
/// https://datatracker.ietf.org/doc/html/rfc1123
|
||||
///
|
||||
/// This is relevant in harmony since most k8s resource names are required to be usable as dns
|
||||
/// subdomains.
|
||||
///
|
||||
/// See https://kubernetes.io/docs/concepts/overview/working-with-objects/names/
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Rfc1123Name {
|
||||
content: String,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Rfc1123Name {
|
||||
fn try_from(s: &str) -> Result<Self, String> {
|
||||
let mut content = s.to_lowercase();
|
||||
|
||||
// Remove invalid characters
|
||||
content.retain(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.');
|
||||
|
||||
// Enforce max length
|
||||
if content.len() > 253 {
|
||||
content.truncate(253);
|
||||
}
|
||||
|
||||
// Trim leading/trailing dots
|
||||
content = content.trim_matches('.').to_string();
|
||||
|
||||
// Deduplicate consecutive dots
|
||||
loop {
|
||||
let new_content = content.replace("..", ".");
|
||||
if new_content == content {
|
||||
break;
|
||||
}
|
||||
content = new_content;
|
||||
}
|
||||
|
||||
// Trim leading/trailing non-alphanumeric
|
||||
content = content
|
||||
.trim_matches(|c: char| !c.is_ascii_alphanumeric())
|
||||
.to_string();
|
||||
|
||||
if content.is_empty() {
|
||||
return Err(format!("Input '{}' resulted in empty string", s));
|
||||
}
|
||||
|
||||
Ok(Self { content })
|
||||
}
|
||||
|
||||
type Error = String;
|
||||
}
|
||||
|
||||
/// Converts an `Rfc1123Name` into a `String`.
|
||||
///
|
||||
/// This allows using `Rfc1123Name` in contexts where a `String` is expected.
|
||||
impl From<Rfc1123Name> for String {
|
||||
fn from(name: Rfc1123Name) -> Self {
|
||||
name.content
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes the `Rfc1123Name` as a string.
|
||||
///
|
||||
/// This directly serializes the inner `String` content without additional wrapping.
|
||||
impl serde::Serialize for Rfc1123Name {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.content)
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserializes an `Rfc1123Name` from a string.
|
||||
///
|
||||
/// This directly deserializes into the inner `String` content without additional wrapping.
|
||||
impl<'de> serde::Deserialize<'de> for Rfc1123Name {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let content = String::deserialize(deserializer)?;
|
||||
Ok(Self { content })
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays the `Rfc1123Name` as a string.
|
||||
///
|
||||
/// This directly displays the inner `String` content without additional wrapping.
|
||||
impl std::fmt::Display for Rfc1123Name {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.content)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Rfc1123Name;
|
||||
|
||||
#[test]
|
||||
fn test_try_from_empty() {
|
||||
let name = Rfc1123Name::try_from("");
|
||||
assert!(name.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_from_valid() {
|
||||
let name = Rfc1123Name::try_from("hello-world").unwrap();
|
||||
assert_eq!(name.content, "hello-world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_from_uppercase() {
|
||||
let name = Rfc1123Name::try_from("Hello-World").unwrap();
|
||||
assert_eq!(name.content, "hello-world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_from_invalid_chars() {
|
||||
let name = Rfc1123Name::try_from("hel@lo#w!or%ld123").unwrap();
|
||||
assert_eq!(name.content, "helloworld123");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_from_leading_dot() {
|
||||
let name = Rfc1123Name::try_from(".hello").unwrap();
|
||||
assert_eq!(name.content, "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_from_trailing_dot() {
|
||||
let name = Rfc1123Name::try_from("hello.").unwrap();
|
||||
assert_eq!(name.content, "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_from_leading_hyphen() {
|
||||
let name = Rfc1123Name::try_from("-hello").unwrap();
|
||||
assert_eq!(name.content, "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_from_complicated_string() {
|
||||
let name = Rfc1123Name::try_from("--h--e,}{}12!$#)\np_aulbS\r\t.!@o--._--").unwrap();
|
||||
assert_eq!(name.content, "h--e12paulbs.o");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_from_trailing_hyphen() {
|
||||
let name = Rfc1123Name::try_from("hello-").unwrap();
|
||||
assert_eq!(name.content, "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_from_single_hyphen() {
|
||||
let name = Rfc1123Name::try_from("-");
|
||||
assert!(name.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str() {
|
||||
let name: Rfc1123Name = "test-name".try_into().unwrap();
|
||||
assert_eq!(name.content, "test-name");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_string() {
|
||||
let name = Rfc1123Name::try_from("test").unwrap();
|
||||
let s: String = name.into();
|
||||
assert_eq!(s, "test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compliance() {
|
||||
let inputs = vec![
|
||||
"valid",
|
||||
"in-VALID",
|
||||
".dots",
|
||||
"-hyphen",
|
||||
"hyphen-",
|
||||
"!!1@",
|
||||
"aaaaaaaaaa",
|
||||
"--abc--",
|
||||
"a.b-c",
|
||||
];
|
||||
|
||||
for input in inputs {
|
||||
let name = Rfc1123Name::try_from(input).unwrap();
|
||||
let s = &name.content;
|
||||
// Check only allowed characters
|
||||
for c in s.chars() {
|
||||
assert!(c.is_ascii_alphanumeric() || c == '-' || c == '.');
|
||||
}
|
||||
// Check starts and ends with alphanumeric
|
||||
if !s.is_empty() {
|
||||
assert!(s.chars().next().unwrap().is_ascii_alphanumeric());
|
||||
assert!(s.chars().last().unwrap().is_ascii_alphanumeric());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enforces_max_length() {
|
||||
let long_input = "a".repeat(300);
|
||||
let name = Rfc1123Name::try_from(long_input.as_str()).unwrap();
|
||||
assert_eq!(name.content.len(), 253);
|
||||
assert_eq!(name.content, "a".repeat(253));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_trim_end() {
|
||||
let input = "a".repeat(252) + "-";
|
||||
let name = Rfc1123Name::try_from(input.as_str()).unwrap();
|
||||
assert_eq!(name.content.len(), 252);
|
||||
assert_eq!(name.content, "a".repeat(252));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dedup_dots() {
|
||||
let input = "a..b...c";
|
||||
let name = Rfc1123Name::try_from(input).unwrap();
|
||||
assert_eq!(name.content, "a.b.c");
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,171 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord, Debug)]
|
||||
#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord, Debug)]
|
||||
pub struct StorageSize {
|
||||
size_bytes: u64,
|
||||
#[serde(skip)]
|
||||
display_value: Option<u64>,
|
||||
#[serde(skip)]
|
||||
display_suffix: Option<String>,
|
||||
}
|
||||
|
||||
impl StorageSize {
|
||||
pub fn new(size_bytes: u64) -> Self {
|
||||
Self {
|
||||
size_bytes,
|
||||
display_value: None,
|
||||
display_suffix: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn b(size: u64) -> Self {
|
||||
Self {
|
||||
size_bytes: size,
|
||||
display_value: Some(size),
|
||||
display_suffix: Some("B".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kb(size: u64) -> Self {
|
||||
Self {
|
||||
size_bytes: size * 1024,
|
||||
display_value: Some(size),
|
||||
display_suffix: Some("KB".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mb(size: u64) -> Self {
|
||||
Self {
|
||||
size_bytes: size * 1024 * 1024,
|
||||
display_value: Some(size),
|
||||
display_suffix: Some("MB".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gb(size: u64) -> Self {
|
||||
Self {
|
||||
size_bytes: size * 1024 * 1024 * 1024,
|
||||
display_value: Some(size),
|
||||
display_suffix: Some("GB".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gi(size: u64) -> Self {
|
||||
Self {
|
||||
size_bytes: size * 1024 * 1024 * 1024,
|
||||
display_value: Some(size),
|
||||
display_suffix: Some("Gi".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tb(size: u64) -> Self {
|
||||
Self {
|
||||
size_bytes: size * 1024 * 1024 * 1024 * 1024,
|
||||
display_value: Some(size),
|
||||
display_suffix: Some("TB".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ti(size: u64) -> Self {
|
||||
Self {
|
||||
size_bytes: size * 1024 * 1024 * 1024 * 1024,
|
||||
display_value: Some(size),
|
||||
display_suffix: Some("Ti".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bytes(&self) -> u64 {
|
||||
self.size_bytes
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for StorageSize {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if let Some(suffix) = &self.display_suffix {
|
||||
let value = self.display_value.unwrap_or(self.size_bytes);
|
||||
write!(f, "{}{}", value, suffix)
|
||||
} else {
|
||||
write!(f, "{}B", self.size_bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_bytes() {
|
||||
let size = StorageSize::b(123);
|
||||
assert_eq!(size.bytes(), 123);
|
||||
assert_eq!(size.to_string(), "123B");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kilobytes() {
|
||||
let size = StorageSize::kb(2);
|
||||
assert_eq!(size.bytes(), 2048);
|
||||
assert_eq!(size.to_string(), "2KB");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_megabytes() {
|
||||
let size = StorageSize::mb(3);
|
||||
assert_eq!(size.bytes(), 3 * 1024 * 1024);
|
||||
assert_eq!(size.to_string(), "3MB");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gigabytes() {
|
||||
let size = StorageSize::gb(4);
|
||||
assert_eq!(size.bytes(), 4 * 1024 * 1024 * 1024);
|
||||
assert_eq!(size.to_string(), "4GB");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gibibytes() {
|
||||
let size = StorageSize::gi(1);
|
||||
assert_eq!(size.bytes(), 1024 * 1024 * 1024);
|
||||
assert_eq!(size.to_string(), "1Gi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_terabytes() {
|
||||
let size = StorageSize::tb(5);
|
||||
assert_eq!(size.bytes(), 5 * 1024 * 1024 * 1024 * 1024);
|
||||
assert_eq!(size.to_string(), "5TB");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tebibytes() {
|
||||
let size = StorageSize::ti(1);
|
||||
assert_eq!(size.bytes(), 1024 * 1024 * 1024 * 1024);
|
||||
assert_eq!(size.to_string(), "1Ti");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_without_suffix() {
|
||||
let size = StorageSize::new(999);
|
||||
assert_eq!(size.bytes(), 999);
|
||||
assert_eq!(size.to_string(), "999B");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serde_roundtrip() {
|
||||
let original = StorageSize::gi(1);
|
||||
let serialized = serde_json::to_string(&original).unwrap();
|
||||
let deserialized: StorageSize = serde_json::from_str(&serialized).unwrap();
|
||||
|
||||
assert_eq!(original.bytes(), deserialized.bytes());
|
||||
// Note: suffix is lost during serialization/deserialization
|
||||
assert_ne!(original.to_string(), deserialized.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ord() {
|
||||
let one_gb = StorageSize::gb(1);
|
||||
let one_gi = StorageSize::gi(1);
|
||||
assert!(one_gb < one_gi); // 1GB = 1000MB, 1Gi = 1024MB
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user