Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions crates/k8s-version/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added

- Add support for Serialization and Deserialization via `serde`. This feature is enabled via the
`serde` feature flag ([#1034]).

[#1034]: https://github.com/stackabletech/operator-rs/pull/1034

## [0.1.2] - 2024-09-19

### Changed
Expand Down
3 changes: 3 additions & 0 deletions crates/k8s-version/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@ repository.workspace = true

[features]
darling = ["dep:darling"]
serde = ["dep:serde"]

[dependencies]
darling = { workspace = true, optional = true }
regex.workspace = true
serde = { workspace = true, optional = true }
snafu.workspace = true

[dev-dependencies]
rstest.workspace = true
rstest_reuse.workspace = true
quote.workspace = true
proc-macro2.workspace = true
serde_yaml.workspace = true
syn.workspace = true
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ use snafu::{ResultExt, Snafu};

use crate::{Group, ParseGroupError, ParseVersionError, Version};

#[cfg(feature = "serde")]
mod serde;

/// Error variants which can be encountered when creating a new [`ApiVersion`]
/// from unparsed input.
#[derive(Debug, PartialEq, Snafu)]
Expand Down
62 changes: 62 additions & 0 deletions crates/k8s-version/src/api_version/serde.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use std::str::FromStr;

use serde::{Deserialize, Serialize, de::Visitor};

use crate::ApiVersion;

impl<'de> Deserialize<'de> for ApiVersion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct ApiVersionVisitor;

impl<'de> Visitor<'de> for ApiVersionVisitor {
type Value = ApiVersion;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "a valid Kubernetes API version")
}

fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
ApiVersion::from_str(v).map_err(serde::de::Error::custom)
}
}

deserializer.deserialize_str(ApiVersionVisitor)
}
}

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

#[cfg(feature = "serde")]
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn deserialize() {
let _: ApiVersion =
serde_yaml::from_str("extensions.k8s.io/v1alpha1").expect("api version is valid");
}

#[test]
fn serialize() {
let api_version =
ApiVersion::from_str("extensions.k8s.io/v1alpha1").expect("api version is valid");
assert_eq!(
"extensions.k8s.io/v1alpha1\n",
serde_yaml::to_string(&api_version).expect("api version must serialize")
);
}
}
21 changes: 21 additions & 0 deletions crates/k8s-version/src/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub enum ParseGroupError {
/// ### See
///
/// - <https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#api-conventions>
#[cfg_attr(feature = "serde", derive(::serde::Deserialize, ::serde::Serialize))]
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd)]
pub struct Group(String);

Expand Down Expand Up @@ -63,3 +64,23 @@ impl Deref for Group {
&self.0
}
}

#[cfg(feature = "serde")]
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn deserialize() {
let _: Group = serde_yaml::from_str("extensions.k8s.io").expect("group is valid");
}

#[test]
fn serialize() {
let group = Group("extensions.k8s.io".into());
assert_eq!(
"extensions.k8s.io\n",
serde_yaml::to_string(&group).expect("group must serialize")
);
}
}
34 changes: 34 additions & 0 deletions crates/k8s-version/src/level/darling.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use std::str::FromStr;

use darling::FromMeta;

use crate::Level;

impl FromMeta for Level {
fn from_string(value: &str) -> darling::Result<Self> {
Self::from_str(value).map_err(darling::Error::custom)
}
}

#[cfg(test)]
mod tests {
use quote::quote;
use rstest::rstest;

use super::*;

fn parse_meta(tokens: proc_macro2::TokenStream) -> ::std::result::Result<syn::Meta, String> {
let attribute: syn::Attribute = syn::parse_quote!(#[#tokens]);
Ok(attribute.meta)
}

#[rstest]
#[case(quote!(ignore = "alpha12"), Level::Alpha(12))]
#[case(quote!(ignore = "alpha1"), Level::Alpha(1))]
#[case(quote!(ignore = "beta1"), Level::Beta(1))]
fn from_meta(#[case] input: proc_macro2::TokenStream, #[case] expected: Level) {
let meta = parse_meta(input).expect("valid attribute tokens");
let version = Level::from_meta(&meta).expect("level must parse from attribute");
assert_eq!(version, expected);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ use std::{
sync::LazyLock,
};

#[cfg(feature = "darling")]
use darling::FromMeta;
use regex::Regex;
use snafu::{OptionExt, ResultExt, Snafu};

#[cfg(feature = "serde")]
mod serde;

#[cfg(feature = "darling")]
mod darling;

static LEVEL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^(?P<identifier>[a-z]+)(?P<version>\d+)$").expect("failed to compile level regex")
});
Expand Down Expand Up @@ -148,28 +152,13 @@ impl Display for Level {
}
}

#[cfg(feature = "darling")]
impl FromMeta for Level {
fn from_string(value: &str) -> darling::Result<Self> {
Self::from_str(value).map_err(darling::Error::custom)
}
}

#[cfg(test)]
mod test {
#[cfg(feature = "darling")]
use quote::quote;
use rstest::rstest;
use rstest_reuse::*;

use super::*;

#[cfg(feature = "darling")]
fn parse_meta(tokens: proc_macro2::TokenStream) -> ::std::result::Result<syn::Meta, String> {
let attribute: syn::Attribute = syn::parse_quote!(#[#tokens]);
Ok(attribute.meta)
}

#[template]
#[rstest]
#[case(Level::Beta(1), Level::Alpha(1), Ordering::Greater)]
Expand All @@ -191,15 +180,4 @@ mod test {
fn partial_ord(input: Level, other: Level, expected: Ordering) {
assert_eq!(input.partial_cmp(&other), Some(expected))
}

#[cfg(feature = "darling")]
#[rstest]
#[case(quote!(ignore = "alpha12"), Level::Alpha(12))]
#[case(quote!(ignore = "alpha1"), Level::Alpha(1))]
#[case(quote!(ignore = "beta1"), Level::Beta(1))]
fn from_meta(#[case] input: proc_macro2::TokenStream, #[case] expected: Level) {
let meta = parse_meta(input).expect("valid attribute tokens");
let version = Level::from_meta(&meta).expect("level must parse from attribute");
assert_eq!(version, expected);
}
}
60 changes: 60 additions & 0 deletions crates/k8s-version/src/level/serde.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use std::str::FromStr;

use serde::{Deserialize, Serialize, de::Visitor};

use crate::Level;

impl<'de> Deserialize<'de> for Level {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct LevelVisitor;

impl<'de> Visitor<'de> for LevelVisitor {
type Value = Level;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "a valid Kubernetes API version level")
}

fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Level::from_str(v).map_err(serde::de::Error::custom)
}
}

deserializer.deserialize_str(LevelVisitor)
}
}

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

#[cfg(feature = "serde")]
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn deserialize() {
let _: Level = serde_yaml::from_str("alpha1").expect("level is valid");
}

#[test]
fn serialize() {
let api_version = Level::from_str("alpha1").expect("level is valid");
assert_eq!(
"alpha1\n",
serde_yaml::to_string(&api_version).expect("level must serialize")
);
}
}
37 changes: 37 additions & 0 deletions crates/k8s-version/src/version/darling.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use std::str::FromStr;

use darling::FromMeta;

use crate::Version;

impl FromMeta for Version {
fn from_string(value: &str) -> darling::Result<Self> {
Self::from_str(value).map_err(darling::Error::custom)
}
}

#[cfg(test)]
mod tests {
use quote::quote;
use rstest::rstest;

use super::*;
use crate::Level;

fn parse_meta(tokens: proc_macro2::TokenStream) -> ::std::result::Result<syn::Meta, String> {
let attribute: syn::Attribute = syn::parse_quote!(#[#tokens]);
Ok(attribute.meta)
}

#[cfg(feature = "darling")]
#[rstest]
#[case(quote!(ignore = "v1alpha12"), Version { major: 1, level: Some(Level::Alpha(12)) })]
#[case(quote!(ignore = "v1alpha1"), Version { major: 1, level: Some(Level::Alpha(1)) })]
#[case(quote!(ignore = "v1beta1"), Version { major: 1, level: Some(Level::Beta(1)) })]
#[case(quote!(ignore = "v1"), Version { major: 1, level: None })]
fn from_meta(#[case] input: proc_macro2::TokenStream, #[case] expected: Version) {
let meta = parse_meta(input).expect("valid attribute tokens");
let version = Version::from_meta(&meta).expect("version must parse from attribute");
assert_eq!(version, expected);
}
}
Loading
Loading