Skip to content

Commit

Permalink
feat: Add support for CustomResource derive
Browse files Browse the repository at this point in the history
  • Loading branch information
Techassi committed Sep 10, 2024
1 parent 0af2a26 commit 14ddaf4
Show file tree
Hide file tree
Showing 9 changed files with 366 additions and 124 deletions.
6 changes: 6 additions & 0 deletions Cargo.lock

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

10 changes: 10 additions & 0 deletions crates/stackable-versioned-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,27 @@ repository.workspace = true
[lib]
proc-macro = true

[features]
full = ["k8s"]
k8s = ["dep:kube", "dep:k8s-openapi"]

[dependencies]
k8s-version = { path = "../k8s-version", features = ["darling"] }

convert_case.workspace = true
darling.workspace = true
itertools.workspace = true
k8s-openapi = { workspace = true, optional = true }
kube = { workspace = true, optional = true }
proc-macro2.workspace = true
strum.workspace = true
syn.workspace = true
quote.workspace = true

[dev-dependencies]
rstest.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_yaml.workspace = true
trybuild.workspace = true
60 changes: 46 additions & 14 deletions crates/stackable-versioned-macros/src/attrs/common/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ pub(crate) struct ContainerAttributes {
#[darling(multiple, rename = "version")]
pub(crate) versions: SpannedValue<Vec<VersionAttributes>>,

#[darling(default)]
pub(crate) options: ContainerOptions,
#[darling(rename = "k8s")]
pub(crate) kubernetes_attrs: Option<KubernetesAttributes>,

#[darling(default, rename = "options")]
pub(crate) common_option_attrs: OptionAttributes,
}

impl ContainerAttributes {
Expand All @@ -43,7 +46,7 @@ impl ContainerAttributes {

// Ensure that versions are defined in sorted (ascending) order to keep
// code consistent.
if !self.options.allow_unsorted.is_present() {
if !self.common_option_attrs.allow_unsorted.is_present() {
let original = self.versions.deref().clone();
self.versions
.sort_by(|lhs, rhs| lhs.name.partial_cmp(&rhs.name).unwrap_or(Ordering::Equal));
Expand Down Expand Up @@ -71,20 +74,28 @@ impl ContainerAttributes {
// place.

// Ensure every version is unique and isn't declared multiple times.
let duplicates = self
let duplicate_versions = self
.versions
.iter()
.duplicates_by(|e| e.name)
.map(|e| e.name)
.join(", ");

if !duplicates.is_empty() {
if !duplicate_versions.is_empty() {
return Err(Error::custom(format!(
"attribute macro `#[versioned()]` contains duplicate versions: {duplicates}",
"attribute macro `#[versioned()]` contains duplicate versions: {duplicate_versions}",
))
.with_span(&self.versions.span()));
}

// Ensure that the 'k8s' feature is enabled when the 'k8s()'
// attribute is used.
if self.kubernetes_attrs.is_some() && cfg!(not(feature = "k8s")) {
return Err(Error::custom(
"the `#[versioned(k8s())]` attribute can only be used when the `k8s` feature is enabled",
));
}

Ok(self)
}
}
Expand All @@ -101,29 +112,50 @@ impl ContainerAttributes {
pub(crate) struct VersionAttributes {
pub(crate) deprecated: Flag,
pub(crate) name: Version,
pub(crate) skip: Option<SkipOptions>,
pub(crate) skip: Option<CommonSkipAttributes>,
pub(crate) doc: Option<String>,
}

/// This struct contains supported container options.
/// This struct contains supported option attributes.
///
/// Supported options are:
/// Supported attributes are:
///
/// - `allow_unsorted`, which allows declaring versions in unsorted order,
/// instead of enforcing ascending order.
/// - `skip` option to skip generating various pieces of code.
#[derive(Clone, Debug, Default, FromMeta)]
pub(crate) struct ContainerOptions {
pub(crate) struct OptionAttributes {
pub(crate) allow_unsorted: Flag,
pub(crate) skip: Option<SkipOptions>,
pub(crate) skip: Option<CommonSkipAttributes>,
}

/// This struct contains supported skip options.
/// This struct contains supported Kubernetes attributes.
///
/// Supported options are:
/// Supported attributes are:
///
/// - `kind`, which allows overwriting the kind field of the CRD. This defaults
/// to the struct name (without the 'Spec' suffix).
/// - `group`, which sets the CRD group, usually the domain of the company.
#[derive(Clone, Debug, FromMeta)]
pub(crate) struct KubernetesAttributes {
pub(crate) skip: Option<KubernetesSkipAttributes>,
pub(crate) kind: Option<String>,
pub(crate) group: String,
}

#[derive(Clone, Debug, FromMeta)]
pub(crate) struct KubernetesSkipAttributes {
pub(crate) merged_crd: Flag,
}

/// This struct contains supported common skip attributes.
///
/// Supported attributes are:
///
/// - `from` flag, which skips generating [`From`] implementations when provided.
#[derive(Clone, Debug, Default, FromMeta)]
pub(crate) struct SkipOptions {
pub(crate) struct CommonSkipAttributes {
/// Whether the [`From`] implementation generation should be skipped for all
/// versions of this container.
pub(crate) from: Flag,
}
113 changes: 102 additions & 11 deletions crates/stackable-versioned-macros/src/codegen/common/container.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::ops::Deref;

use proc_macro2::TokenStream;
use quote::format_ident;
use syn::{Attribute, Ident, Visibility};

use crate::{attrs::common::ContainerAttributes, codegen::common::ContainerVersion};
Expand Down Expand Up @@ -32,6 +33,25 @@ where
fn generate_tokens(&self) -> TokenStream;
}

/// Provides extra functionality on top of [`Ident`]s.
pub(crate) trait IdentExt {
/// Removes the 'Spec' suffix from the [`Ident`].
fn as_cleaned_kubernetes_ident(&self) -> Ident;

/// Transforms the [`Ident`] into one usable in the [`From`] impl.
fn as_from_impl_ident(&self) -> Ident;
}

impl IdentExt for Ident {
fn as_cleaned_kubernetes_ident(&self) -> Ident {
format_ident!("{}", self.to_string().trim_end_matches("Spec"))
}

fn as_from_impl_ident(&self) -> Ident {
format_ident!("__sv_{}", self.to_string().to_lowercase())
}
}

/// This struct bundles values from [`DeriveInput`][1].
///
/// [`DeriveInput`][1] cannot be used directly when constructing a
Expand All @@ -58,24 +78,95 @@ pub(crate) struct VersionedContainer<I> {
/// definition with appropriate items.
pub(crate) versions: Vec<ContainerVersion>,

/// The original attributes that were added to the container.
pub(crate) original_attributes: Vec<Attribute>,

/// The visibility of the versioned container. Used to forward the
/// visibility during code generation.
pub(crate) visibility: Visibility,

/// List of items defined in the original container. How, and if, an item
/// should generate code, is decided by the currently generated version.
pub(crate) items: Vec<I>,

/// The ident, or name, of the versioned container.
pub(crate) ident: Ident,
/// Different options which influence code generation.
pub(crate) options: VersionedContainerOptions,

/// The visibility of the versioned container. Used to forward the
/// visibility during code generation.
pub(crate) visibility: Visibility,
/// A collection of container idents used for different purposes.
pub(crate) idents: VersionedContainerIdents,
}

/// The original attributes that were added to the container.
pub(crate) original_attributes: Vec<Attribute>,
impl<I> VersionedContainer<I> {
/// Creates a new versioned Container which contains common data shared
/// across structs and enums.
pub(crate) fn new(
input: ContainerInput,
attributes: ContainerAttributes,
versions: Vec<ContainerVersion>,
items: Vec<I>,
) -> Self {
let ContainerInput {
original_attributes,
visibility,
ident,
} = input;

let skip_from = attributes
.common_option_attrs
.skip
.map_or(false, |s| s.from.is_present());

let kubernetes_options = attributes.kubernetes_attrs.map(|a| KubernetesOptions {
skip_merged_crd: a.skip.map_or(false, |s| s.merged_crd.is_present()),
group: a.group,
kind: a.kind,
});

/// The name of the container used in `From` implementations.
pub(crate) from_ident: Ident,
let options = VersionedContainerOptions {
kubernetes_options,
skip_from,
};

/// Whether the [`From`] implementation generation should be skipped for all
/// versions of this container.
let idents = VersionedContainerIdents {
kubernetes: ident.as_cleaned_kubernetes_ident(),
from: ident.as_from_impl_ident(),
original: ident,
};

VersionedContainer {
original_attributes,
visibility,
versions,
options,
idents,
items,
}
}
}

/// A collection of container idents used for different purposes.
#[derive(Debug)]
pub(crate) struct VersionedContainerIdents {
/// The ident used in the context of Kubernetes specific code. This ident
/// removes the 'Spec' suffix present in the definition container.
pub(crate) kubernetes: Ident,

/// The original ident, or name, of the versioned container.
pub(crate) original: Ident,

/// The ident used in the [`From`] impl.
pub(crate) from: Ident,
}

#[derive(Debug)]
pub(crate) struct VersionedContainerOptions {
pub(crate) kubernetes_options: Option<KubernetesOptions>,
pub(crate) skip_from: bool,
}

#[derive(Debug)]
pub(crate) struct KubernetesOptions {
pub(crate) skip_merged_crd: bool,
pub(crate) kind: Option<String>,
pub(crate) group: String,
}
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,6 @@ where
let mut actions = BTreeMap::new();

for change in common_attributes.changes.iter().rev() {
dbg!(&ty, &change.since);
let from_ident = if let Some(from) = change.from_name.as_deref() {
format_ident!("{from}")
} else {
Expand Down
5 changes: 0 additions & 5 deletions crates/stackable-versioned-macros/src/codegen/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,6 @@ impl From<&ContainerAttributes> for Vec<ContainerVersion> {
}
}

/// Returns the container ident used in [`From`] implementations.
pub(crate) fn format_container_from_ident(ident: &Ident) -> Ident {
format_ident!("__sv_{ident}", ident = ident.to_string().to_lowercase())
}

/// Removes the deprecated prefix from a field ident.
///
/// See [`DEPRECATED_FIELD_PREFIX`].
Expand Down
Loading

0 comments on commit 14ddaf4

Please sign in to comment.