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(stackable-versioned): Add version-specific item docs #866

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
27 changes: 20 additions & 7 deletions crates/stackable-versioned-macros/src/attrs/common/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,20 +143,20 @@ impl ItemAttributes {

let mut errors = Error::accumulator();

// Semantic validation
// Common validation
errors.handle(self.validate_action_combinations(item_ident, item_type));
errors.handle(self.validate_action_order(item_ident, item_type));
errors.handle(self.validate_item_name(item_ident, item_type));
errors.handle(self.validate_changed_item_name(item_type));
errors.handle(self.validate_item_attributes(item_attrs));

// Action specific validation
errors.handle(self.validate_changed_action(item_ident, item_type));

// TODO (@Techassi): Add hint if a field or variant is added in the
// first version that it might be clever to remove the 'added'
// attribute.

errors.finish()?;

Ok(())
errors.finish()
}

/// This associated function is called by the top-level validation function
Expand Down Expand Up @@ -293,22 +293,35 @@ impl ItemAttributes {
}
}
}

Ok(())
}

/// This associated function is called by the top-level validation function
/// and validates that parameters provided to the `changed` actions are
/// valid.
fn validate_changed_item_name(&self, item_type: &ItemType) -> Result<(), Error> {
fn validate_changed_action(
&self,
item_ident: &Ident,
item_type: &ItemType,
) -> Result<(), Error> {
let prefix = match item_type {
ItemType::Field => DEPRECATED_FIELD_PREFIX,
ItemType::Variant => DEPRECATED_VARIANT_PREFIX,
};

let mut errors = Error::accumulator();

// This ensures that `from_name` doesn't include the deprecation prefix.
for change in &self.changes {
// Ensure that from_name and from_type are not empty at the same
// time.
if change.from_name.is_none() && change.from_type.is_none() {
errors.push(Error::custom(format!(
"{item_type} was marked as `changed`, but both `from_name` and `from_type` are unset"
)).with_span(item_ident));
}

// Ensure that `from_name` doesn't include the deprecation prefix.
if let Some(from_name) = change.from_name.as_ref() {
if from_name.starts_with(prefix) {
errors.push(
Expand Down
67 changes: 46 additions & 21 deletions crates/stackable-versioned-macros/src/codegen/common/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::collections::BTreeMap;

use k8s_version::Version;
use proc_macro2::Span;
use quote::format_ident;
use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote, ToTokens};
use syn::Ident;

use crate::{
Expand Down Expand Up @@ -34,24 +34,7 @@ pub(crate) struct ContainerVersion {
pub(crate) ident: Ident,

/// Store additional doc-comment lines for this version.
pub(crate) version_specific_docs: Vec<String>,
}

/// Converts lines of doc-comments into a trimmed list.
fn process_docs(input: &Option<String>) -> Vec<String> {
if let Some(input) = input {
input
// Trim the leading and trailing whitespace, deleting suprefluous
// empty lines.
.trim()
.lines()
// Trim the leading and trailing whitespace on each line that can be
// introduced when the developer indents multi-line comments.
.map(|line| line.trim().to_owned())
.collect()
} else {
Vec::new()
}
pub(crate) docs: Docs,
}

impl From<&ContainerAttributes> for Vec<ContainerVersion> {
Expand All @@ -63,13 +46,55 @@ impl From<&ContainerAttributes> for Vec<ContainerVersion> {
skip_from: v.skip.as_ref().map_or(false, |s| s.from.is_present()),
ident: Ident::new(&v.name.to_string(), Span::call_site()),
deprecated: v.deprecated.is_present(),
docs: v.doc.clone().into(),
inner: v.name,
version_specific_docs: process_docs(&v.doc),
})
.collect()
}
}

#[derive(Clone, Debug)]
pub(crate) struct Docs(Vec<String>);

impl From<Option<String>> for Docs {
fn from(doc: Option<String>) -> Self {
let lines = if let Some(doc) = doc {
doc
// Trim the leading and trailing whitespace, deleting
// superfluous empty lines.
.trim()
.lines()
// Trim the leading and trailing whitespace on each line that
// can be introduced when the developer indents multi-line
// comments.
.map(|line| line.trim().into())
.collect()
} else {
Vec::new()
};

Self(lines)
}
}

impl ToTokens for Docs {
fn to_tokens(&self, tokens: &mut TokenStream) {
for (index, line) in self.0.iter().enumerate() {
if index == 0 {
// Prepend an empty line to clearly separate the version/action
// specific docs.
tokens.extend(quote! {
#[doc = ""]
})
}

tokens.extend(quote! {
#[doc = #line]
})
}
}
}

/// Removes the deprecated prefix from a field ident.
///
/// See [`DEPRECATED_FIELD_PREFIX`].
Expand Down
26 changes: 2 additions & 24 deletions crates/stackable-versioned-macros/src/codegen/venum/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,25 +106,23 @@ impl VersionedEnum {
// enable the attribute macro to be applied to a module which
// generates versioned versions of all contained containers.

let version_specific_docs = &version.docs;
let version_ident = &version.ident;

let deprecated_note = format!("Version {version} is deprecated", version = version_ident);
let deprecated_attr = version
.deprecated
.then_some(quote! {#[deprecated = #deprecated_note]});

// Generate doc comments for the container (enum)
let version_specific_docs = self.generate_enum_docs(version);

// Generate tokens for the module and the contained enum
token_stream.extend(quote! {
#[automatically_derived]
#deprecated_attr
#visibility mod #version_ident {
use super::*;

#version_specific_docs
#(#original_attributes)*
#version_specific_docs
pub enum #enum_name {
#variants
}
Expand All @@ -139,26 +137,6 @@ impl VersionedEnum {
token_stream
}

/// Generates version specific doc comments for the enum.
fn generate_enum_docs(&self, version: &ContainerVersion) -> TokenStream {
let mut tokens = TokenStream::new();

for (i, doc) in version.version_specific_docs.iter().enumerate() {
if i == 0 {
// Prepend an empty line to clearly separate the version
// specific docs.
tokens.extend(quote! {
#[doc = ""]
})
}
tokens.extend(quote! {
#[doc = #doc]
})
}

tokens
}

fn generate_enum_variants(&self, version: &ContainerVersion) -> TokenStream {
let mut token_stream = TokenStream::new();

Expand Down
26 changes: 2 additions & 24 deletions crates/stackable-versioned-macros/src/codegen/vstruct/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,16 +126,14 @@ impl VersionedStruct {
// enable the attribute macro to be applied to a module which
// generates versioned versions of all contained containers.

let version_specific_docs = &version.docs;
let version_ident = &version.ident;

let deprecated_note = format!("Version {version} is deprecated", version = version_ident);
let deprecated_attr = version
.deprecated
.then_some(quote! {#[deprecated = #deprecated_note]});

// Generate doc comments for the container (struct)
let version_specific_docs = self.generate_struct_docs(version);

// Generate K8s specific code
let kubernetes_cr_derive = self.generate_kubernetes_cr_derive(version);

Expand All @@ -146,8 +144,8 @@ impl VersionedStruct {
#visibility mod #version_ident {
use super::*;

#version_specific_docs
#(#original_attributes)*
#version_specific_docs
#kubernetes_cr_derive
pub struct #struct_name {
#fields
Expand All @@ -163,26 +161,6 @@ impl VersionedStruct {
token_stream
}

/// Generates version specific doc comments for the struct.
fn generate_struct_docs(&self, version: &ContainerVersion) -> TokenStream {
let mut tokens = TokenStream::new();

for (i, doc) in version.version_specific_docs.iter().enumerate() {
if i == 0 {
// Prepend an empty line to clearly separate the version
// specific docs.
tokens.extend(quote! {
#[doc = ""]
})
}
tokens.extend(quote! {
#[doc = #doc]
})
}

tokens
}

/// Generates struct fields following the `name: type` format which includes
/// a trailing comma.
fn generate_struct_fields(&self, version: &ContainerVersion) -> TokenStream {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ fn main() {
struct Foo {
#[versioned(
changed(since = "v1beta1", from_name = "deprecated_bar"),
changed(since = "v1", from_name = "deprecated_baz")
changed(since = "v1")
)]
bar: usize,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ error: the previous field name must not start with the deprecation prefix
11 | changed(since = "v1beta1", from_name = "deprecated_bar"),
| ^^^^^^^^^^^^^^^^

error: the previous field name must not start with the deprecation prefix
--> tests/default/fail/changed.rs:12:47
error: field was marked as `changed`, but both `from_name` and `from_type` are unset
--> tests/default/fail/changed.rs:14:9
|
12 | changed(since = "v1", from_name = "deprecated_baz")
| ^^^^^^^^^^^^^^^^
14 | bar: usize,
| ^^^