Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Serde impl for CpuQuantity and MemoryQuantity #724

Merged
merged 5 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

## [0.62.0] - 2024-01-19

### Added
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);
}
}