From b174a48bed39335c066c70ad74de8bad7f9f7e30 Mon Sep 17 00:00:00 2001 From: Techassi Date: Tue, 23 Jan 2024 11:57:37 +0100 Subject: [PATCH 1/5] Add Serde impl for CpuQuantity --- src/cpu.rs | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 98 insertions(+), 2 deletions(-) diff --git a/src/cpu.rs b/src/cpu.rs index 422810b87..e3271596c 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -1,10 +1,12 @@ use std::{ + fmt::Display, iter::Sum, ops::{Add, AddAssign, Div, Mul, MulAssign}, str::FromStr, }; use k8s_openapi::apimachinery::pkg::api::resource::Quantity; +use serde::{de::Visitor, Deserialize, Serialize}; use crate::error::{Error, OperatorResult}; @@ -33,6 +35,50 @@ impl CpuQuantity { } } +impl Serialize for CpuQuantity { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for CpuQuantity { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct CpuQuantityVisitor; + + impl<'de> Visitor<'de> for CpuQuantityVisitor { + type Value = CpuQuantity; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a valid CPU quantiry") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + CpuQuantity::from_str(v).map_err(serde::de::Error::custom) + } + } + + deserializer.deserialize_str(CpuQuantityVisitor) + } +} + +impl Display for CpuQuantity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.millis < 1000 { + true => write!(f, "{}m", self.millis), + false => write!(f, "{}", self.as_cpu_count()), + } + } +} + impl FromStr for CpuQuantity { type Err = Error; @@ -171,7 +217,7 @@ mod test { #[case("0.2", 200)] #[case("0.02", 20)] #[case("0.002", 2)] - fn test_from_str(#[case] s: &str, #[case] millis: usize) { + fn from_str(#[case] s: &str, #[case] millis: usize) { let result = CpuQuantity::from_str(s).unwrap(); assert_eq!(millis, result.as_milli_cpus()) } @@ -181,8 +227,58 @@ mod test { #[case("1000.1m")] #[case("500k")] #[case("0.0002")] - fn test_from_str_err(#[case] s: &str) { + fn from_str_err(#[case] s: &str) { let result = CpuQuantity::from_str(s); assert!(result.is_err()); } + + #[rstest] + #[case(CpuQuantity::from_millis(10000), "10")] + #[case(CpuQuantity::from_millis(1500), "1.5")] + #[case(CpuQuantity::from_millis(999), "999m")] + #[case(CpuQuantity::from_millis(500), "500m")] + #[case(CpuQuantity::from_millis(100), "100m")] + #[case(CpuQuantity::from_millis(2000), "2")] + #[case(CpuQuantity::from_millis(1000), "1")] + fn display_to_string(#[case] cpu: CpuQuantity, #[case] expected: &str) { + assert_eq!(cpu.to_string(), expected) + } + + #[rstest] + #[case(CpuQuantity::from_millis(10000), "cpu: '10'\n")] + #[case(CpuQuantity::from_millis(1500), "cpu: '1.5'\n")] + #[case(CpuQuantity::from_millis(999), "cpu: 999m\n")] + #[case(CpuQuantity::from_millis(500), "cpu: 500m\n")] + #[case(CpuQuantity::from_millis(100), "cpu: 100m\n")] + #[case(CpuQuantity::from_millis(2000), "cpu: '2'\n")] + #[case(CpuQuantity::from_millis(1000), "cpu: '1'\n")] + fn serialize(#[case] cpu: CpuQuantity, #[case] expected: &str) { + #[derive(Serialize)] + struct Cpu { + cpu: CpuQuantity, + } + + let cpu = Cpu { cpu }; + let output = serde_yaml::to_string(&cpu).unwrap(); + + assert_eq!(output, expected) + } + + #[rstest] + #[case("cpu: '10'", CpuQuantity::from_millis(10000))] + #[case("cpu: '1.5'", CpuQuantity::from_millis(1500))] + #[case("cpu: 999m", CpuQuantity::from_millis(999))] + #[case("cpu: 500m", CpuQuantity::from_millis(500))] + #[case("cpu: 100m", CpuQuantity::from_millis(100))] + #[case("cpu: 2", CpuQuantity::from_millis(2000))] + #[case("cpu: 1", CpuQuantity::from_millis(1000))] + fn deserialize(#[case] input: &str, #[case] expected: CpuQuantity) { + #[derive(Deserialize)] + struct Cpu { + cpu: CpuQuantity, + } + + let cpu: Cpu = serde_yaml::from_str(input).unwrap(); + assert_eq!(cpu.cpu, expected) + } } From 3982c610eccbdccf70fb509ac1a7684112a0eb13 Mon Sep 17 00:00:00 2001 From: Techassi Date: Tue, 23 Jan 2024 12:30:55 +0100 Subject: [PATCH 2/5] Add Serde impl for MemoryQuantity --- src/memory.rs | 138 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 103 insertions(+), 35 deletions(-) diff --git a/src/memory.rs b/src/memory.rs index 7a357f72a..d50c19d32 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -9,6 +9,7 @@ //! For details on Kubernetes quantities see: use k8s_openapi::apimachinery::pkg::api::resource::Quantity; +use serde::{de::Visitor, Deserialize, Serialize}; use crate::error::{Error, OperatorResult}; use std::{ @@ -301,6 +302,66 @@ impl MemoryQuantity { } } +impl Serialize for MemoryQuantity { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for MemoryQuantity { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct MemoryQuantityVisitor; + + impl<'de> Visitor<'de> for MemoryQuantityVisitor { + type Value = MemoryQuantity; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a valid memory quantity") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + MemoryQuantity::from_str(v).map_err(serde::de::Error::custom) + } + } + + deserializer.deserialize_str(MemoryQuantityVisitor) + } +} + +impl FromStr for MemoryQuantity { + type Err = Error; + + fn from_str(q: &str) -> OperatorResult { + let start_of_unit = + q.find(|c: char| c != '.' && !c.is_numeric()) + .ok_or(Error::NoQuantityUnit { + value: q.to_owned(), + })?; + let (value, unit) = q.split_at(start_of_unit); + Ok(MemoryQuantity { + value: value.parse::().map_err(|_| Error::InvalidQuantity { + value: q.to_owned(), + })?, + unit: unit.parse()?, + }) + } +} + +impl Display for MemoryQuantity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}{}", self.value, self.unit) + } +} + impl Mul for MemoryQuantity { type Output = MemoryQuantity; @@ -395,31 +456,6 @@ impl PartialEq for MemoryQuantity { impl Eq for MemoryQuantity {} -impl FromStr for MemoryQuantity { - type Err = Error; - - fn from_str(q: &str) -> OperatorResult { - let start_of_unit = - q.find(|c: char| c != '.' && !c.is_numeric()) - .ok_or(Error::NoQuantityUnit { - value: q.to_owned(), - })?; - let (value, unit) = q.split_at(start_of_unit); - Ok(MemoryQuantity { - value: value.parse::().map_err(|_| Error::InvalidQuantity { - value: q.to_owned(), - })?, - unit: unit.parse()?, - }) - } -} - -impl Display for MemoryQuantity { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}{}", self.value, self.unit) - } -} - impl TryFrom for MemoryQuantity { type Error = Error; @@ -474,7 +510,7 @@ mod test { #[case("1.2Gi")] #[case("1.6Gi")] #[case("1Gi")] - pub fn test_fmt(#[case] q: String) { + fn test_try_from_quantity(#[case] q: String) { let m = MemoryQuantity::try_from(Quantity(q.clone())).unwrap(); let actual = format!("{m}"); assert_eq!(q, actual); @@ -486,7 +522,7 @@ mod test { #[case("2Mi", 0.8, "-Xmx1638k")] #[case("1.5Gi", 0.8, "-Xmx1229m")] #[case("2Gi", 0.8, "-Xmx1638m")] - pub fn test_to_java_heap(#[case] q: &str, #[case] factor: f32, #[case] heap: &str) { + fn test_to_java_heap(#[case] q: &str, #[case] factor: f32, #[case] heap: &str) { #[allow(deprecated)] // allow use of the deprecated 'to_java_heap' function to test it let actual = to_java_heap(&Quantity(q.to_owned()), factor).unwrap(); assert_eq!(heap, actual); @@ -498,7 +534,7 @@ mod test { #[case("1.2Gi", "1228m")] #[case("1.6Gi", "1638m")] #[case("1Gi", "1g")] - pub fn test_format_java(#[case] q: String, #[case] expected: String) { + fn test_format_java(#[case] q: String, #[case] expected: String) { let m = MemoryQuantity::try_from(Quantity(q)).unwrap(); let actual = m.format_for_java().unwrap(); assert_eq!(expected, actual); @@ -513,7 +549,7 @@ mod test { #[case(2000f32, BinaryMultiple::Pebi, BinaryMultiple::Mebi, 2000f32*1024f32*1024f32*1024f32)] #[case(2000f32, BinaryMultiple::Pebi, BinaryMultiple::Kibi, 2000f32*1024f32*1024f32*1024f32*1024f32)] #[case(2000f32, BinaryMultiple::Exbi, BinaryMultiple::Pebi, 2000f32*1024f32)] - pub fn test_scale_to( + fn test_scale_to( #[case] value: f32, #[case] unit: BinaryMultiple, #[case] target_unit: BinaryMultiple, @@ -537,7 +573,7 @@ mod test { #[case("2000Ki", 1.0, BinaryMultiple::Mebi, 1)] #[case("4000Mi", 1.0, BinaryMultiple::Gibi, 3)] #[case("4000Mi", 0.8, BinaryMultiple::Gibi, 3)] - pub fn test_to_java_heap_value( + fn test_to_java_heap_value( #[case] q: &str, #[case] factor: f32, #[case] target_unit: BinaryMultiple, @@ -555,7 +591,7 @@ mod test { #[case("1000Mi", 1.0, BinaryMultiple::Gibi)] #[case("1023Mi", 1.0, BinaryMultiple::Gibi)] #[case("1024Mi", 0.8, BinaryMultiple::Gibi)] - pub fn test_to_java_heap_value_failure( + fn test_to_java_heap_value_failure( #[case] q: &str, #[case] factor: f32, #[case] target_unit: BinaryMultiple, @@ -570,7 +606,7 @@ mod test { #[case("1Mi", "512Ki", "512Ki")] #[case("2Mi", "512Ki", "1536Ki")] #[case("2048Ki", "1Mi", "1024Ki")] - pub fn test_subtraction(#[case] lhs: &str, #[case] rhs: &str, #[case] res: &str) { + fn test_subtraction(#[case] lhs: &str, #[case] rhs: &str, #[case] res: &str) { let lhs = MemoryQuantity::try_from(Quantity(lhs.to_owned())).unwrap(); let rhs = MemoryQuantity::try_from(Quantity(rhs.to_owned())).unwrap(); let expected = MemoryQuantity::try_from(Quantity(res.to_owned())).unwrap(); @@ -587,7 +623,7 @@ mod test { #[case("1Mi", "512Ki", "1536Ki")] #[case("2Mi", "512Ki", "2560Ki")] #[case("2048Ki", "1Mi", "3072Ki")] - pub fn test_addition(#[case] lhs: &str, #[case] rhs: &str, #[case] res: &str) { + fn test_addition(#[case] lhs: &str, #[case] rhs: &str, #[case] res: &str) { let lhs = MemoryQuantity::try_from(Quantity(lhs.to_owned())).unwrap(); let rhs = MemoryQuantity::try_from(Quantity(rhs.to_owned())).unwrap(); let expected = MemoryQuantity::try_from(Quantity(res.to_owned())).unwrap(); @@ -608,7 +644,7 @@ mod test { #[case("100Ki", "101Ki", false)] #[case("1Mi", "100Ki", true)] #[case("2000Ki", "1Mi", true)] - pub fn test_comparison(#[case] lhs: &str, #[case] rhs: &str, #[case] res: bool) { + fn test_comparison(#[case] lhs: &str, #[case] rhs: &str, #[case] res: bool) { let lhs = MemoryQuantity::try_from(Quantity(lhs.to_owned())).unwrap(); let rhs = MemoryQuantity::try_from(Quantity(rhs.to_owned())).unwrap(); assert_eq!(lhs > rhs, res) @@ -619,9 +655,41 @@ mod test { #[case("100Ki", "200Ki", false)] #[case("1Mi", "1024Ki", true)] #[case("1024Ki", "1Mi", true)] - pub fn test_eq(#[case] lhs: &str, #[case] rhs: &str, #[case] res: bool) { + fn test_eq(#[case] lhs: &str, #[case] rhs: &str, #[case] res: bool) { let lhs = MemoryQuantity::try_from(Quantity(lhs.to_owned())).unwrap(); let rhs = MemoryQuantity::try_from(Quantity(rhs.to_owned())).unwrap(); assert_eq!(lhs == rhs, res) } + + #[rstest] + #[case(MemoryQuantity::from_mebi(1536.0), "memory: 1536Mi\n")] + #[case(MemoryQuantity::from_mebi(100.0), "memory: 100Mi\n")] + #[case(MemoryQuantity::from_gibi(10.0), "memory: 10Gi\n")] + #[case(MemoryQuantity::from_gibi(1.0), "memory: 1Gi\n")] + fn test_serialize(#[case] memory: MemoryQuantity, #[case] expected: &str) { + #[derive(Serialize)] + struct Memory { + memory: MemoryQuantity, + } + + let memory = Memory { memory }; + let output = serde_yaml::to_string(&memory).unwrap(); + + assert_eq!(output, expected); + } + + #[rstest] + #[case("memory: 1536Mi", MemoryQuantity::from_mebi(1536.0))] + #[case("memory: 100Mi", MemoryQuantity::from_mebi(100.0))] + #[case("memory: 10Gi", MemoryQuantity::from_gibi(10.0))] + #[case("memory: 1Gi", MemoryQuantity::from_gibi(1.0))] + fn test_deserialize(#[case] input: &str, #[case] expected: MemoryQuantity) { + #[derive(Deserialize)] + struct Memory { + memory: MemoryQuantity, + } + + let memory: Memory = serde_yaml::from_str(input).unwrap(); + assert_eq!(memory.memory, expected); + } } From 1e76591bd08a0dbbe2392416ac70f5ab378539d2 Mon Sep 17 00:00:00 2001 From: Techassi Date: Tue, 23 Jan 2024 12:31:15 +0100 Subject: [PATCH 3/5] Adjust test names --- src/cpu.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/cpu.rs b/src/cpu.rs index e3271596c..0f89fb96f 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -217,7 +217,7 @@ mod test { #[case("0.2", 200)] #[case("0.02", 20)] #[case("0.002", 2)] - fn from_str(#[case] s: &str, #[case] millis: usize) { + fn test_from_str(#[case] s: &str, #[case] millis: usize) { let result = CpuQuantity::from_str(s).unwrap(); assert_eq!(millis, result.as_milli_cpus()) } @@ -227,7 +227,7 @@ mod test { #[case("1000.1m")] #[case("500k")] #[case("0.0002")] - fn from_str_err(#[case] s: &str) { + fn test_from_str_err(#[case] s: &str) { let result = CpuQuantity::from_str(s); assert!(result.is_err()); } @@ -240,7 +240,7 @@ mod test { #[case(CpuQuantity::from_millis(100), "100m")] #[case(CpuQuantity::from_millis(2000), "2")] #[case(CpuQuantity::from_millis(1000), "1")] - fn display_to_string(#[case] cpu: CpuQuantity, #[case] expected: &str) { + fn test_display_to_string(#[case] cpu: CpuQuantity, #[case] expected: &str) { assert_eq!(cpu.to_string(), expected) } @@ -252,7 +252,7 @@ mod test { #[case(CpuQuantity::from_millis(100), "cpu: 100m\n")] #[case(CpuQuantity::from_millis(2000), "cpu: '2'\n")] #[case(CpuQuantity::from_millis(1000), "cpu: '1'\n")] - fn serialize(#[case] cpu: CpuQuantity, #[case] expected: &str) { + fn test_serialize(#[case] cpu: CpuQuantity, #[case] expected: &str) { #[derive(Serialize)] struct Cpu { cpu: CpuQuantity, @@ -261,7 +261,7 @@ mod test { let cpu = Cpu { cpu }; let output = serde_yaml::to_string(&cpu).unwrap(); - assert_eq!(output, expected) + assert_eq!(output, expected); } #[rstest] @@ -272,13 +272,13 @@ mod test { #[case("cpu: 100m", CpuQuantity::from_millis(100))] #[case("cpu: 2", CpuQuantity::from_millis(2000))] #[case("cpu: 1", CpuQuantity::from_millis(1000))] - fn deserialize(#[case] input: &str, #[case] expected: CpuQuantity) { + fn test_deserialize(#[case] input: &str, #[case] expected: CpuQuantity) { #[derive(Deserialize)] struct Cpu { cpu: CpuQuantity, } let cpu: Cpu = serde_yaml::from_str(input).unwrap(); - assert_eq!(cpu.cpu, expected) + assert_eq!(cpu.cpu, expected); } } From 505e73c7addcaf8f8cc0d7efbba3b03e36226283 Mon Sep 17 00:00:00 2001 From: Techassi Date: Tue, 23 Jan 2024 12:35:48 +0100 Subject: [PATCH 4/5] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b76728232..95ae7c97a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added + +- Add Serde `Deserialize` and `Serialize` support for `CpuQuantity` and `MemoryQuantity` ([#724]). + +[#724]: https://github.com/stackabletech/operator-rs/pull/724 + ## [0.62.0] - 2024-01-19 ### Added From 0fd6760e601c8b7cb7d04d7759dbd4c8ff85c643 Mon Sep 17 00:00:00 2001 From: Techassi Date: Tue, 23 Jan 2024 14:22:22 +0100 Subject: [PATCH 5/5] Fix typo --- src/cpu.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cpu.rs b/src/cpu.rs index 0f89fb96f..e59ab7869 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -55,7 +55,7 @@ impl<'de> Deserialize<'de> for CpuQuantity { type Value = CpuQuantity; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a valid CPU quantiry") + formatter.write_str("a valid CPU quantity") } fn visit_str(self, v: &str) -> Result