Skip to content

Commit f029ed6

Browse files
authored
feat(k8s-version): Add serde support (#1034)
* feat(k8s-version): Add serde support * refactor(k8s-version): Move darling code into separate files * chore(k8s-version): Add changelog entry * test(k8s-version): Add unit tests for (de)serialization * chore(k8s-version): Correct typos in changelog * chore: Apply suggestion
1 parent 61596d6 commit f029ed6

File tree

12 files changed

+301
-57
lines changed

12 files changed

+301
-57
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/k8s-version/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### Added
8+
9+
- Add support for serialization and deserialization via `serde`. This feature is enabled via the
10+
`serde` feature flag ([#1034]).
11+
12+
[#1034]: https://github.com/stackabletech/operator-rs/pull/1034
13+
714
## [0.1.2] - 2024-09-19
815

916
### Changed

crates/k8s-version/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@ repository.workspace = true
88

99
[features]
1010
darling = ["dep:darling"]
11+
serde = ["dep:serde"]
1112

1213
[dependencies]
1314
darling = { workspace = true, optional = true }
1415
regex.workspace = true
16+
serde = { workspace = true, optional = true }
1517
snafu.workspace = true
1618

1719
[dev-dependencies]
1820
rstest.workspace = true
1921
rstest_reuse.workspace = true
2022
quote.workspace = true
2123
proc-macro2.workspace = true
24+
serde_yaml.workspace = true
2225
syn.workspace = true

crates/k8s-version/src/api_version.rs renamed to crates/k8s-version/src/api_version/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ use snafu::{ResultExt, Snafu};
66

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

9+
#[cfg(feature = "serde")]
10+
mod serde;
11+
912
/// Error variants which can be encountered when creating a new [`ApiVersion`]
1013
/// from unparsed input.
1114
#[derive(Debug, PartialEq, Snafu)]
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use std::str::FromStr;
2+
3+
use serde::{Deserialize, Serialize, de::Visitor};
4+
5+
use crate::ApiVersion;
6+
7+
impl<'de> Deserialize<'de> for ApiVersion {
8+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
9+
where
10+
D: serde::Deserializer<'de>,
11+
{
12+
struct ApiVersionVisitor;
13+
14+
impl<'de> Visitor<'de> for ApiVersionVisitor {
15+
type Value = ApiVersion;
16+
17+
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
18+
write!(formatter, "a valid Kubernetes API version")
19+
}
20+
21+
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
22+
where
23+
E: serde::de::Error,
24+
{
25+
ApiVersion::from_str(v).map_err(serde::de::Error::custom)
26+
}
27+
}
28+
29+
deserializer.deserialize_str(ApiVersionVisitor)
30+
}
31+
}
32+
33+
impl Serialize for ApiVersion {
34+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
35+
where
36+
S: serde::Serializer,
37+
{
38+
serializer.serialize_str(&self.to_string())
39+
}
40+
}
41+
42+
#[cfg(feature = "serde")]
43+
#[cfg(test)]
44+
mod tests {
45+
use super::*;
46+
47+
#[test]
48+
fn deserialize() {
49+
let _: ApiVersion =
50+
serde_yaml::from_str("extensions.k8s.io/v1alpha1").expect("api version is valid");
51+
}
52+
53+
#[test]
54+
fn serialize() {
55+
let api_version =
56+
ApiVersion::from_str("extensions.k8s.io/v1alpha1").expect("api version is valid");
57+
assert_eq!(
58+
"extensions.k8s.io/v1alpha1\n",
59+
serde_yaml::to_string(&api_version).expect("api version must serialize")
60+
);
61+
}
62+
}

crates/k8s-version/src/group.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ pub enum ParseGroupError {
3535
/// ### See
3636
///
3737
/// - <https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#api-conventions>
38+
#[cfg_attr(feature = "serde", derive(::serde::Deserialize, ::serde::Serialize))]
3839
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd)]
3940
pub struct Group(String);
4041

@@ -63,3 +64,23 @@ impl Deref for Group {
6364
&self.0
6465
}
6566
}
67+
68+
#[cfg(feature = "serde")]
69+
#[cfg(test)]
70+
mod tests {
71+
use super::*;
72+
73+
#[test]
74+
fn deserialize() {
75+
let _: Group = serde_yaml::from_str("extensions.k8s.io").expect("group is valid");
76+
}
77+
78+
#[test]
79+
fn serialize() {
80+
let group = Group("extensions.k8s.io".into());
81+
assert_eq!(
82+
"extensions.k8s.io\n",
83+
serde_yaml::to_string(&group).expect("group must serialize")
84+
);
85+
}
86+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
use std::str::FromStr;
2+
3+
use darling::FromMeta;
4+
5+
use crate::Level;
6+
7+
impl FromMeta for Level {
8+
fn from_string(value: &str) -> darling::Result<Self> {
9+
Self::from_str(value).map_err(darling::Error::custom)
10+
}
11+
}
12+
13+
#[cfg(test)]
14+
mod tests {
15+
use quote::quote;
16+
use rstest::rstest;
17+
18+
use super::*;
19+
20+
fn parse_meta(tokens: proc_macro2::TokenStream) -> ::std::result::Result<syn::Meta, String> {
21+
let attribute: syn::Attribute = syn::parse_quote!(#[#tokens]);
22+
Ok(attribute.meta)
23+
}
24+
25+
#[rstest]
26+
#[case(quote!(ignore = "alpha12"), Level::Alpha(12))]
27+
#[case(quote!(ignore = "alpha1"), Level::Alpha(1))]
28+
#[case(quote!(ignore = "beta1"), Level::Beta(1))]
29+
fn from_meta(#[case] input: proc_macro2::TokenStream, #[case] expected: Level) {
30+
let meta = parse_meta(input).expect("valid attribute tokens");
31+
let version = Level::from_meta(&meta).expect("level must parse from attribute");
32+
assert_eq!(version, expected);
33+
}
34+
}

crates/k8s-version/src/level.rs renamed to crates/k8s-version/src/level/mod.rs

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@ use std::{
77
sync::LazyLock,
88
};
99

10-
#[cfg(feature = "darling")]
11-
use darling::FromMeta;
1210
use regex::Regex;
1311
use snafu::{OptionExt, ResultExt, Snafu};
1412

13+
#[cfg(feature = "serde")]
14+
mod serde;
15+
16+
#[cfg(feature = "darling")]
17+
mod darling;
18+
1519
static LEVEL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
1620
Regex::new(r"^(?P<identifier>[a-z]+)(?P<version>\d+)$").expect("failed to compile level regex")
1721
});
@@ -148,28 +152,13 @@ impl Display for Level {
148152
}
149153
}
150154

151-
#[cfg(feature = "darling")]
152-
impl FromMeta for Level {
153-
fn from_string(value: &str) -> darling::Result<Self> {
154-
Self::from_str(value).map_err(darling::Error::custom)
155-
}
156-
}
157-
158155
#[cfg(test)]
159156
mod test {
160-
#[cfg(feature = "darling")]
161-
use quote::quote;
162157
use rstest::rstest;
163158
use rstest_reuse::*;
164159

165160
use super::*;
166161

167-
#[cfg(feature = "darling")]
168-
fn parse_meta(tokens: proc_macro2::TokenStream) -> ::std::result::Result<syn::Meta, String> {
169-
let attribute: syn::Attribute = syn::parse_quote!(#[#tokens]);
170-
Ok(attribute.meta)
171-
}
172-
173162
#[template]
174163
#[rstest]
175164
#[case(Level::Beta(1), Level::Alpha(1), Ordering::Greater)]
@@ -191,15 +180,4 @@ mod test {
191180
fn partial_ord(input: Level, other: Level, expected: Ordering) {
192181
assert_eq!(input.partial_cmp(&other), Some(expected))
193182
}
194-
195-
#[cfg(feature = "darling")]
196-
#[rstest]
197-
#[case(quote!(ignore = "alpha12"), Level::Alpha(12))]
198-
#[case(quote!(ignore = "alpha1"), Level::Alpha(1))]
199-
#[case(quote!(ignore = "beta1"), Level::Beta(1))]
200-
fn from_meta(#[case] input: proc_macro2::TokenStream, #[case] expected: Level) {
201-
let meta = parse_meta(input).expect("valid attribute tokens");
202-
let version = Level::from_meta(&meta).expect("level must parse from attribute");
203-
assert_eq!(version, expected);
204-
}
205183
}

crates/k8s-version/src/level/serde.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use std::str::FromStr;
2+
3+
use serde::{Deserialize, Serialize, de::Visitor};
4+
5+
use crate::Level;
6+
7+
impl<'de> Deserialize<'de> for Level {
8+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
9+
where
10+
D: serde::Deserializer<'de>,
11+
{
12+
struct LevelVisitor;
13+
14+
impl<'de> Visitor<'de> for LevelVisitor {
15+
type Value = Level;
16+
17+
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
18+
write!(formatter, "a valid Kubernetes API version level")
19+
}
20+
21+
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
22+
where
23+
E: serde::de::Error,
24+
{
25+
Level::from_str(v).map_err(serde::de::Error::custom)
26+
}
27+
}
28+
29+
deserializer.deserialize_str(LevelVisitor)
30+
}
31+
}
32+
33+
impl Serialize for Level {
34+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
35+
where
36+
S: serde::Serializer,
37+
{
38+
serializer.serialize_str(&self.to_string())
39+
}
40+
}
41+
42+
#[cfg(feature = "serde")]
43+
#[cfg(test)]
44+
mod tests {
45+
use super::*;
46+
47+
#[test]
48+
fn deserialize() {
49+
let _: Level = serde_yaml::from_str("alpha1").expect("level is valid");
50+
}
51+
52+
#[test]
53+
fn serialize() {
54+
let api_version = Level::from_str("alpha1").expect("level is valid");
55+
assert_eq!(
56+
"alpha1\n",
57+
serde_yaml::to_string(&api_version).expect("level must serialize")
58+
);
59+
}
60+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
use std::str::FromStr;
2+
3+
use darling::FromMeta;
4+
5+
use crate::Version;
6+
7+
impl FromMeta for Version {
8+
fn from_string(value: &str) -> darling::Result<Self> {
9+
Self::from_str(value).map_err(darling::Error::custom)
10+
}
11+
}
12+
13+
#[cfg(test)]
14+
mod tests {
15+
use quote::quote;
16+
use rstest::rstest;
17+
18+
use super::*;
19+
use crate::Level;
20+
21+
fn parse_meta(tokens: proc_macro2::TokenStream) -> ::std::result::Result<syn::Meta, String> {
22+
let attribute: syn::Attribute = syn::parse_quote!(#[#tokens]);
23+
Ok(attribute.meta)
24+
}
25+
26+
#[cfg(feature = "darling")]
27+
#[rstest]
28+
#[case(quote!(ignore = "v1alpha12"), Version { major: 1, level: Some(Level::Alpha(12)) })]
29+
#[case(quote!(ignore = "v1alpha1"), Version { major: 1, level: Some(Level::Alpha(1)) })]
30+
#[case(quote!(ignore = "v1beta1"), Version { major: 1, level: Some(Level::Beta(1)) })]
31+
#[case(quote!(ignore = "v1"), Version { major: 1, level: None })]
32+
fn from_meta(#[case] input: proc_macro2::TokenStream, #[case] expected: Version) {
33+
let meta = parse_meta(input).expect("valid attribute tokens");
34+
let version = Version::from_meta(&meta).expect("version must parse from attribute");
35+
assert_eq!(version, expected);
36+
}
37+
}

0 commit comments

Comments
 (0)