Skip to content

Commit

Permalink
Merge branch 'main' into refactor/kvp-deref-target
Browse files Browse the repository at this point in the history
  • Loading branch information
Techassi committed Jan 23, 2024
2 parents 78670be + 6bbf707 commit fb5d04d
Show file tree
Hide file tree
Showing 3 changed files with 205 additions and 35 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

### Changed

- Change Deref target of `KeyPrefix` and `KeyName` from `String` to `str` ([#725]).
Expand Down
96 changes: 96 additions & 0 deletions src/cpu.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -33,6 +35,50 @@ impl CpuQuantity {
}
}

impl Serialize for CpuQuantity {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}

impl<'de> Deserialize<'de> for CpuQuantity {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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 quantity")
}

fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
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;

Expand Down Expand Up @@ -185,4 +231,54 @@ mod test {
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 test_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 test_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 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);
}
}
138 changes: 103 additions & 35 deletions src/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
//! For details on Kubernetes quantities see: <https://github.com/kubernetes/apimachinery/blob/master/pkg/api/resource/quantity.go>
use k8s_openapi::apimachinery::pkg::api::resource::Quantity;
use serde::{de::Visitor, Deserialize, Serialize};

use crate::error::{Error, OperatorResult};
use std::{
Expand Down Expand Up @@ -301,6 +302,66 @@ impl MemoryQuantity {
}
}

impl Serialize for MemoryQuantity {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}

impl<'de> Deserialize<'de> for MemoryQuantity {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, v: &str) -> Result<Self::Value, E>
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<Self> {
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::<f32>().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<f32> for MemoryQuantity {
type Output = MemoryQuantity;

Expand Down Expand Up @@ -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<Self> {
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::<f32>().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<Quantity> for MemoryQuantity {
type Error = Error;

Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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)
Expand All @@ -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);
}
}

0 comments on commit fb5d04d

Please sign in to comment.