Skip to content
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
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