Skip to main content

stackable_versioned_macros/
lib.rs

1use darling::{FromMeta, ast::NestedMeta};
2#[cfg(doc)]
3use kube::core::conversion::ConversionReview;
4use proc_macro::TokenStream;
5use syn::{Error, Item, spanned::Spanned};
6
7use crate::{attrs::module::ModuleAttributes, codegen::module::Module};
8
9#[cfg(test)]
10mod test_utils;
11
12mod attrs;
13mod codegen;
14mod utils;
15
16/// This macro enables generating versioned structs and enums.
17///
18/// In this guide, code blocks usually come in pairs. The first code block
19/// describes how the macro is used. The second expandable block displays the
20/// generated piece of code for explanation purposes. It should be noted, that
21/// the exact code can diverge from what is being depicted in this guide. Most
22/// code is heavily simplified. For example, `#[automatically_derived]` and
23/// `#[allow(deprecated)]` are removed in most examples to reduce visual clutter.
24///
25/// <div class="warning">
26///
27/// It is **important** to note that this macro must be placed before any other
28/// (derive) macros and attributes. Macros supplied before the versioned macro
29/// will be erased, because the original struct, enum or module (container) is
30/// erased, and new containers are generated. This ensures that the macros and
31/// attributes are applied to the generated versioned instances of the
32/// container.
33///
34/// </div>
35///
36/// # Version Declarations
37///
38/// Before any of the fields or variants can be versioned, versions need to be
39/// declared at the module level. Each version currently supports two
40/// parameters: `name` and the `deprecated` flag. The `name` must be a valid
41/// (and supported) format.
42///
43/// <div class="warning">
44///
45/// Currently, only [Kubernetes API versions][k8s-version-format] are supported.
46/// The macro checks each declared version and reports any error encountered
47/// during parsing.
48///
49/// </div>
50///
51/// It should be noted that the defined struct always represents the **latest**
52/// version, eg: when defining three versions `v1alpha1`, `v1beta1`, and `v1`,
53/// the struct will describe the structure of the data in `v1`. This behaviour
54/// is especially noticeable in the [`changed()`](#changed-action) action which
55/// works "backwards" by describing how a field looked before the current
56/// (latest) version.
57///
58/// TODO: Version declarations should eventually be moved back to containers.
59///
60/// ```
61/// # use stackable_versioned_macros::versioned;
62/// #[versioned(version(name = "v1alpha1"))]
63/// mod versioned {
64///     struct Foo {
65///         bar: usize,
66///     }
67/// }
68/// ```
69///
70/// <details>
71/// <summary>Generated code</summary>
72///
73/// 1. The `#[automatically_derived]` attribute indicates that the following
74///    piece of code is automatically generated by a macro instead of being
75///    handwritten by a developer. This information is used by cargo and rustc.
76/// 2. For each declared version, a new module containing the containers is
77///    generated. This enables you to reference the container by versions via
78///    `v1alpha1::Foo`.
79/// 3. This `use` statement gives the generated containers access to the imports
80///    at the top of the file. This is a convenience, because otherwise you
81///    would need to prefix used items with `super::`. Additionally, other
82///    macros can have trouble using items referred to with `super::`.
83///
84/// ```ignore
85/// #[automatically_derived] // 1
86/// mod v1alpha1 {           // 2
87///     use super::*;        // 3
88///     pub struct Foo {
89///         bar: usize,
90///     }
91/// }
92/// ```
93/// </details>
94///
95/// ## Version Deprecation
96///
97/// The `deprecated` flag marks the version as deprecated. This currently adds
98/// the `#[deprecated]` attribute to the appropriate piece of code. In the
99/// future, this will additionally mark the CRD version with `deprecated: true`.
100/// See the official docs on [version deprecation][k8s-crd-ver-deprecation].
101///
102/// ```
103/// # use stackable_versioned_macros::versioned;
104/// #[versioned(version(name = "v1alpha1", deprecated))]
105/// mod versioned {
106///     struct Foo {
107///         bar: usize,
108///     }
109/// }
110/// ```
111///
112/// <details>
113/// <summary>Generated code</summary>
114///
115/// 1. The `deprecated` flag will generate a `#[deprecated]` attribute and the
116///    note is automatically generated.
117///
118/// ```ignore
119/// #[automatically_derived]
120/// #[deprecated = "Version v1alpha1 is deprecated"] // 1
121/// mod v1alpha1 {
122///     use super::*;
123///     pub struct Foo {
124///         pub bar: usize,
125///     }
126/// }
127/// ```
128/// </details>
129///
130/// ## Version Sorting
131///
132/// Additionally, it is ensured that each version is unique. Declaring the same
133/// version multiple times will result in an error. Furthermore, declaring the
134/// versions out-of-order is prohibited by default. It is possible to opt-out
135/// of this check by setting `options(allow_unsorted)`.
136///
137/// <div class="warning">
138///
139/// It is **not** recommended to use this setting and instead use sorted versions
140/// across all versioned items.
141///
142/// </div>
143///
144/// ```
145/// # use stackable_versioned_macros::versioned;
146/// #[versioned(
147///     version(name = "v1beta1"),
148///     version(name = "v1alpha1"),
149///     options(allow_unsorted)
150/// )]
151/// mod versioned {
152///     struct Foo {
153///         bar: usize,
154///     }
155/// }
156/// ```
157///
158/// # Versioning Module
159///
160/// The purpose of the macro is to version Kubernetes CustomResourceDefinitions
161/// (CRDs). As such, the design and how it works is focused on defining and
162/// versioning these CRDs. These CRDs are defined as a top-level struct which
163/// can them self contain many sub structs.
164///
165/// To be able to maximize the visibility on items comprising the CRD, the macro
166/// needs to be applied to module blocks. The name of the module can be freely
167/// chosen. Throughout this guide, the name `versioned` is used. The module is
168/// erased in the generated code. This behaviour can however be
169/// [customized](#preserve-module).
170///
171/// TODO: Mention visibility of module
172///
173/// ## Preserve Module
174///
175/// The previous examples completely replaced the `versioned` module with
176/// top-level version modules. This is the default behaviour. Preserving the
177/// module can however be enabled by setting the `preserve_module` flag.
178///
179/// ```
180/// # use stackable_versioned_macros::versioned;
181/// #[versioned(
182///     version(name = "v1alpha1"),
183///     version(name = "v1"),
184///     options(preserve_module)
185/// )]
186/// mod versioned {
187///     struct Foo {
188///         bar: usize,
189///     }
190///
191///     struct Bar {
192///         baz: String,
193///     }
194/// }
195/// ```
196///
197/// ## Crate Overrides
198///
199/// Override the import path of specific crates which is especially useful if
200/// the crates are brought into scope through re-exports. The following code
201/// block depicts supported overrides and their default values.
202///
203/// ```ignore
204/// # use stackable_versioned_macros::versioned;
205/// #[versioned(
206///     version(name = "v1alpha1"),
207///     version(name = "v1beta1"),
208///     crates(
209///         versioned = "::stackable_versioned",
210///         kube_client = "::kube::client",
211///         k8s_openapi = "::k8s_openapi",
212///         serde_json = "::serde_json",
213///         kube_core = "::kube::core",
214///         schemars = "::schemars",
215///         serde = "::serde",
216///         // Mutually exclusive with kube_core and kube_client
217///         kube = "::kube",
218///     )
219/// )]
220/// mod versioned {
221///     // ...
222/// }
223/// ```
224///
225/// ## Additional Options
226///
227/// This section contains optional options which influence parts of the code
228/// generation.
229///
230/// ```
231/// # use stackable_versioned_macros::versioned;
232/// #[versioned(
233///     version(name = "v1alpha1"),
234///     version(name = "v1beta1"),
235///     options(k8s(
236///         // Highly experimental conversion tracking. Opting into this feature will
237///         // introduce frequent breaking changes.
238///         experimental_conversion_tracking,
239///         // Enables instrumentation and log events via the tracing crate.
240///         enable_tracing,
241///     ))
242/// )]
243/// mod versioned {
244///     // ...
245/// }
246/// ```
247///
248/// ## Merging Submodules
249///
250/// Modules defined in the versioned module will be re-emitted. This allows for
251/// composition of re-exports to compose easier to use imports for downstream
252/// consumers of versioned containers. The following rules apply:
253///
254/// 1. Only modules named the same like defined versions will be re-emitted.
255///    Using modules with invalid names will return an error.
256/// 2. Only `use` statements defined in the module will be emitted. Declaring
257///    other items will return an error.
258///
259/// ```
260/// # use stackable_versioned_macros::versioned;
261/// # mod a {
262/// #     pub mod v1alpha1 {}
263/// # }
264/// # mod b {
265/// #     pub mod v1alpha1 {}
266/// # }
267/// #[versioned(version(name = "v1alpha1"), version(name = "v1"))]
268/// mod versioned {
269///     mod v1alpha1 {
270///         pub use a::v1alpha1::*;
271///         pub use b::v1alpha1::*;
272///     }
273///
274///     struct Foo {
275///         bar: usize,
276///     }
277/// }
278/// # fn main() {}
279/// ```
280///
281/// <details>
282/// <summary>Expand Generated Code</summary>
283///
284/// ```ignore
285/// mod v1alpha1 {
286///     use super::*;
287///     pub use a::v1alpha1::*;
288///     pub use b::v1alpha1::*;
289///     pub struct Foo {
290///         pub bar: usize,
291///     }
292/// }
293///
294/// mod v1 {
295///     use super::*;
296///     pub struct Foo {
297///         pub bar: usize,
298///     }
299/// }
300/// ```
301///
302/// </details>
303///
304/// # CRD Spec Definition
305///
306/// ## Arguments
307///
308/// <div class="warning">
309///
310/// It should be noted that not every `#[kube]` argument is supported or
311/// forwarded without changes.
312///
313/// </div>
314///
315/// Structs annotated with `#[versioned(crd()]` are treated as top-level CRD
316/// spec definitions. This section lists all currently supported arguments.
317/// Most of these arguments are directly forwarded to the underlying `#[kube]`
318/// attribute. Some arguments are specific to this macro and don't exist in the
319/// upstream [`kube`] crate.
320///
321/// ```
322/// # use kube::CustomResource;
323/// # use schemars::JsonSchema;
324/// # use serde::{Deserialize, Serialize};
325/// # use stackable_versioned_macros::versioned;
326/// #[versioned(version(name = "v1alpha1"), version(name = "v1beta1"))]
327/// mod versioned {
328///     #[versioned(crd(
329///         // **Required.** Set the group of the CRD, usually the domain of the
330///         // company, like `example.com`.
331///         group = "example.com",
332///         // **Required.** Set the root description of the generated CRD.
333///         doc = "My custom resource.",
334///         // Override the kind field of the CRD. This defaults to the struct
335///         // name (without the `Spec` suffix). Overriding this value will also
336///         // influence the names of other generated items, like the status
337///         // struct (if used) or the version enum.
338///         kind = "CustomKind",
339///         // Set the singular name. Defaults to lowercased `kind` value.
340///         singular = "...",
341///         // Set the plural name. Defaults to inferring from singular.
342///         plural = "...",
343///         // Indicate that this is a namespaced scoped resource rather than a
344///         // cluster scoped resource.
345///         namespaced,
346///         // Set the specified struct as the status subresource. If conversion
347///         // tracking is enabled, this struct will be automatically merged into
348///         // the generated tracking status struct.
349///         status = "FooStatus",
350///         // Set a shortname. This can be specified multiple times.
351///         shortname = "..."
352///     ))]
353/// #   #[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)]
354///     pub struct FooSpec {
355///         #[versioned(deprecated(since = "v1beta1"))]
356///         deprecated_bar: usize,
357///         baz: bool,
358///     }
359/// }
360/// # #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
361/// # pub struct FooStatus {}
362/// # fn main() {}
363/// ```
364///
365/// ## Field Actions
366///
367/// This crate currently supports three different item actions. Items can
368/// be added, changed, and deprecated. The macro ensures that these actions
369/// adhere to the following set of rules:
370///
371/// 1. Items cannot be added and deprecated in the same version.
372/// 2. Items cannot be added and changed in the same version.
373/// 3. Items cannot be changed and deprecated in the same version.
374/// 4. Items added in version _a_, renamed _0...n_ times in versions
375///    b<sub>1</sub>, ..., b<sub>n</sub> and deprecated in
376///    version _c_ must ensure _a < b<sub>1</sub>, ..., b<sub>n</sub> < c_.
377/// 5. All item actions must use previously declared versions. Using versions
378///    not present at the container level will result in an error.
379///
380/// For items marked as deprecated, one additional rule applies:
381///
382/// - Fields must start with the `deprecated_` and variants with the
383///   `Deprecated` prefix. This is enforced because Kubernetes doesn't allow
384///   removing fields in CRDs entirely. Instead, they should be marked as
385///   deprecated. By convention this is done with the `deprecated` prefix.
386///
387/// ### Added Action
388///
389/// This action indicates that an item is added in a particular version.
390/// Available arguments are:
391///
392/// - `since` to indicate since which version the item is present.
393/// - `default` to customize the default function used to populate the item
394///   in auto-generated conversion implementations.
395///
396/// ```
397/// # use stackable_versioned_macros::versioned;
398/// #[versioned(version(name = "v1alpha1"), version(name = "v1beta1"))]
399/// mod versioned {
400///     pub struct Foo {
401///         #[versioned(added(since = "v1beta1"))]
402///         bar: usize,
403///         baz: bool,
404///     }
405/// }
406/// ```
407///
408/// <details>
409/// <summary>Expand Generated Code</summary>
410///
411/// 1. The field `bar` is not yet present in version `v1alpha1` and is therefore
412///    not generated.
413/// 2. Now the field `bar` is present and uses `Default::default()` to populate
414///    the field during conversion. This function can be customized as shown
415///    later in this guide.
416///
417/// ```ignore
418/// pub mod v1alpha1 {
419///     use super::*;
420///     pub struct Foo {                 // 1
421///         pub baz: bool,
422///     }
423/// }
424///
425/// impl From<v1alpha1::Foo> for v1beta1::Foo {
426///     fn from(foo: v1alpha1::Foo) -> Self {
427///         Self {
428///             bar: Default::default(), // 2
429///             baz: foo.baz,
430///         }
431///     }
432/// }
433///
434/// pub mod v1beta1 {
435///     use super::*;
436///     pub struct Foo {
437///         pub bar: usize,              // 2
438///         pub baz: bool,
439///     }
440/// }
441/// ```
442/// </details>
443///
444/// #### Custom Default Function
445///
446/// To customize the default function used in the generated conversion
447/// implementations the `added` action provides the `default` argument. It
448/// expects a path to a function without braces. This path can for example point
449/// at free-standing or associated functions.
450///
451/// ```
452/// # use stackable_versioned_macros::versioned;
453/// #[versioned(version(name = "v1alpha1"), version(name = "v1beta1"))]
454/// mod versioned {
455///     pub struct Foo {
456///         #[versioned(added(since = "v1beta1", default = "default_bar"))]
457///         bar: usize,
458///         baz: bool,
459///     }
460/// }
461///
462/// fn default_bar() -> usize {
463///     42
464/// }
465/// ```
466///
467/// <details>
468/// <summary>Expand Generated Code</summary>
469///
470/// 1. Instead of `Default::default()`, the provided function `default_bar()` is
471///    used. It is of course fully type checked and needs to return the expected
472///    type (`usize` in this case).
473///
474/// ```ignore
475/// // Snip
476///
477/// impl From<v1alpha1::Foo> for v1beta1::Foo {
478///     fn from(foo: v1alpha1::Foo) -> Self {
479///         Self {
480///             bar: default_bar(), // 1
481///             baz: foo.baz,
482///         }
483///     }
484/// }
485///
486/// // Snip
487/// ```
488/// </details>
489///
490/// ### Changed Action
491///
492/// This action indicates that an item is changed in a particular version. It
493/// combines renames and type changes into a single action. You can choose to
494/// change the name, change the type or do both. Available arguments are:
495///
496/// - `since` to indicate since which version the item is changed.
497/// - `from_name` to indicate from which previous name the field is renamed.
498/// - `from_type` to indicate from which previous type the field is changed.
499/// - `upgrade_with` to provide a custom upgrade function. This argument can
500///   only be used in combination with the `from_type` argument. The expected
501///   function signature is: `fn (OLD_TYPE) -> NEW_TYPE`. This function must
502///   not fail.
503/// - `downgrade_with` to provide a custom downgrade function. This argument can
504///   only be used in combination with the `from_type` argument. The expected
505///   function signature is: `fn (NEW_TYPE) -> OLD_TYPE`. This function must
506///   not fail.
507/// ```
508/// # use stackable_versioned_macros::versioned;
509/// #[versioned(version(name = "v1alpha1"), version(name = "v1beta1"))]
510/// mod versioned {
511///     pub struct Foo {
512///         #[versioned(changed(
513///             since = "v1beta1",
514///             from_name = "prev_bar",
515///             from_type = "u16",
516///             downgrade_with = usize_to_u16
517///         ))]
518///         bar: usize,
519///         baz: bool,
520///     }
521/// }
522///
523/// fn usize_to_u16(input: usize) -> u16 {
524///     input.try_into().unwrap()
525/// }
526/// ```
527///
528/// <details>
529/// <summary>Expand Generated Code</summary>
530///
531/// 1. In version `v1alpha1` the field is named `prev_bar` and uses a `u16`.
532/// 2. In the next version, `v1beta1`, the field is now named `bar` and uses
533///    `usize` instead of a `u16`. The conversion implementations transforms the
534///    type automatically.
535///
536/// ```ignore
537/// pub mod v1alpha1 {
538///     use super::*;
539///     pub struct Foo {
540///         pub prev_bar: u16,            // 1
541///         pub baz: bool,
542///     }
543/// }
544///
545/// impl From<v1alpha1::Foo> for v1beta1::Foo {
546///     fn from(foo: v1alpha1::Foo) -> Self {
547///         Self {
548///             bar: foo.prev_bar.into(), // 2
549///             baz: foo.baz,
550///         }
551///     }
552/// }
553///
554/// pub mod v1beta1 {
555///     use super::*;
556///     pub struct Foo {
557///         pub bar: usize,               // 2
558///         pub baz: bool,
559///     }
560/// }
561/// ```
562/// </details>
563///
564/// ### Deprecated Action
565///
566/// This action indicates that an item is deprecated in a particular version.
567/// Deprecated items are not removed. Available arguments are:
568///
569/// - `since` to indicate since which version the item is deprecated.
570/// - `note` to specify an optional deprecation note.
571///
572/// ```
573/// # use stackable_versioned_macros::versioned;
574/// #[versioned(version(name = "v1alpha1"), version(name = "v1beta1"))]
575/// mod versioned {
576///     pub struct Foo {
577///         #[versioned(deprecated(since = "v1beta1"))]
578///         deprecated_bar: usize,
579///         baz: bool,
580///     }
581/// }
582/// ```
583///
584/// <details>
585/// <summary>Expand Generated Code</summary>
586///
587/// 1. In version `v1alpha1` the field `bar` is not yet deprecated and thus uses
588///    the name without the `deprecated_` prefix.
589/// 2. In version `v1beta1` the field is deprecated and now includes the
590///    `deprecated_` prefix. It also uses the `#[deprecated]` attribute to
591///    indicate to Clippy this part of Rust code is deprecated. Therefore, the
592///    conversion implementations include `#[allow(deprecated)]` to allow the
593///    usage of deprecated items in automatically generated code.
594///
595/// ```ignore
596/// pub mod v1alpha1 {
597///     use super::*;
598///     pub struct Foo {
599///         pub bar: usize,                     // 1
600///         pub baz: bool,
601///     }
602/// }
603///
604/// #[allow(deprecated)]                        // 2
605/// impl From<v1alpha1::Foo> for v1beta1::Foo {
606///     fn from(foo: v1alpha1::Foo) -> Self {
607///         Self {
608///             deprecated_bar: foo.bar,        // 2
609///             baz: foo.baz,
610///         }
611///     }
612/// }
613///
614/// pub mod v1beta1 {
615///     use super::*;
616///     pub struct Foo {
617///         #[deprecated]                       // 2
618///         pub deprecated_bar: usize,
619///         pub baz: bool,
620///     }
621/// }
622/// ```
623/// </details>
624///
625/// ## Additional Arguments
626///
627/// In addition to the field actions, the following top-level field arguments
628/// are available:
629///
630/// ### Hinting Wrapper Types
631///
632/// With `#[versioned(hint(...))]` it is possible to give hints to the macro
633/// that the field contains a wrapped type. Currently, these following hints
634/// are supported:
635///
636/// - `hint(option)`: Indicates that the field contains an `Option<T>`.
637/// - `hint(vec)`: Indicates that the field contains a `Vec<T>`.
638/// - `hint(map)`: Indicates that the field contains a map, e.g. `HashMap` or `BTreeMap`.
639///
640/// These hints are especially useful for generated conversion functions. With
641/// these hints in place, the types are correctly mapped using `Into::into`
642/// (assuming the necessary `From` trait methods are implemented on the target
643/// types for the conversion to be done correctly).
644///
645/// ```
646/// # use stackable_versioned_macros::versioned;
647/// #[versioned(version(name = "v1alpha1"), version(name = "v1beta1"))]
648/// mod versioned {
649///     pub struct Foo {
650///         #[versioned(changed(since = "v1beta1", from_type = "Vec<usize>"), hint(vec))]
651///         bar: Vec<usize>,
652///         baz: bool,
653///     }
654/// }
655/// ```
656///
657/// # Generated Helpers
658///
659/// This macro generates a few different helpers to enable different operations
660/// around CRD versioning and conversion. The following sections explain these
661/// helpers and (some) of the code behind them in detail.
662///
663/// All these helpers are generated as associated functions on what this macro
664/// calls an entry enum. When defining the following three versions: `v1alpha1`,
665/// `v1beta1`, and `v1` the following entry enum will be generated:
666///
667/// ```ignore
668/// pub enum Foo {
669///     V1Alpha1(v1alpha1::Foo),
670///     V1Beta1(v1beta1::Foo),
671///     V1(v1::Foo),
672/// }
673/// ```
674///
675/// ## Merge CRD Versions
676///
677/// The generated `merged_crd` method is a wrapper around [kube's `merge_crds`][2]
678/// function. It automatically calls the `crd` methods of the CRD in all of its
679/// versions and additionally provides a strongly typed selector for the stored
680/// API version.
681///
682/// ```
683/// # use stackable_versioned_macros::versioned;
684/// # use kube::CustomResource;
685/// # use schemars::JsonSchema;
686/// # use serde::{Deserialize, Serialize};
687/// #[versioned(version(name = "v1alpha1"), version(name = "v1beta1"))]
688/// mod versioned {
689///     #[versioned(crd(group = "example.com", doc = "My custom resource."))]
690///     #[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)]
691///     pub struct FooSpec {
692///         #[versioned(added(since = "v1beta1"))]
693///         bar: usize,
694///         baz: bool,
695///     }
696/// }
697///
698/// # fn main() {
699/// let merged_crd = Foo::merged_crd(FooVersion::V1Beta1).unwrap();
700/// println!("{yaml}", yaml = serde_yaml::to_string(&merged_crd).unwrap());
701/// # }
702/// ```
703///
704/// The strongly typed version enum looks very similar to the entry enum
705/// described above. It additionally provides various associated functions used
706/// for parsing and string representations.
707///
708/// ```
709/// #[derive(Copy, Clone, Debug)]
710/// pub enum FooVersion {
711///     V1Alpha1,
712///     V1Beta1,
713/// }
714/// ```
715///
716/// ---
717///
718/// The generation of merging helpers can be skipped if manual implementation
719/// is desired. The following piece of code lists all possible locations where
720/// this skip flag can be provided.
721///
722/// ```
723/// # use stackable_versioned_macros::versioned;
724/// # use kube::CustomResource;
725/// # use schemars::JsonSchema;
726/// # use serde::{Deserialize, Serialize};
727/// #
728/// # #[versioned(version(name = "v1alpha1"))]
729/// #[versioned(skip(merged_crd))] // Skip generation for ALL specs
730/// mod versioned {
731///     #[versioned(skip(merged_crd))] // Skip generation for specific specs
732///
733/// #   #[versioned(crd(group = "example.com", doc = "My custom resource."))]
734/// #   #[derive(Clone, Debug, CustomResource, Deserialize, Serialize, JsonSchema)]
735///     pub struct FooSpec {}
736/// }
737/// #
738/// # fn main() {}
739/// ```
740///
741/// ## Convert CustomResources
742///
743/// The conversion of CRs is tightly integrated with [`ConversionReview`]s, the
744/// payload which a conversion webhook receives from the Kubernetes apiserver.
745/// Naturally, the `try_convert` function takes in [`ConversionReview`] as a
746/// parameter and also returns a [`ConversionReview`] indicating success or
747/// failure.
748///
749/// ```ignore
750/// # use stackable_versioned_macros::versioned;
751/// # use kube::CustomResource;
752/// # use schemars::JsonSchema;
753/// # use serde::{Deserialize, Serialize};
754/// #[versioned(
755///     version(name = "v1alpha1"),
756///     version(name = "v1beta1"),
757///     version(name = "v1")
758/// )]
759/// mod versioned {
760///     #[versioned(crd(group = "example.com", doc = "My custom resource."))]
761///     #[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)]
762///     pub struct FooSpec {
763///         #[versioned(added(since = "v1beta1"))]
764///         bar: usize,
765///
766///         #[versioned(added(since = "v1"))]
767///         baz: bool,
768///
769///         quox: String,
770///     }
771/// }
772///
773/// # fn main() {
774/// let conversion_review = Foo::try_convert(conversion_review);
775/// # }
776/// ```
777///
778/// The generation of conversion helpers can be skipped if manual implementation
779/// is desired. The following piece of code lists all possible locations where
780/// this skip flag can be provided:
781///
782/// ```
783/// # use stackable_versioned_macros::versioned;
784/// # use kube::CustomResource;
785/// # use schemars::JsonSchema;
786/// # use serde::{Deserialize, Serialize};
787/// #
788/// # #[versioned(version(name = "v1alpha1"))]
789/// #[versioned(skip(try_convert))] // Skip generation for ALL specs
790/// mod versioned {
791///     #[versioned(skip(try_convert))] // Skip generation for specific specs
792///
793/// #   #[versioned(crd(group = "example.com", doc = "My custom resource."))]
794/// #   #[derive(Clone, Debug, CustomResource, Deserialize, Serialize, JsonSchema)]
795///     pub struct FooSpec {}
796/// }
797/// #
798/// # fn main() {}
799/// ```
800///
801/// ### Conversion Tracking
802///
803/// <div class="warning">
804///
805/// This is a highly experimental feature. To enable it, provide the
806/// `experimental_conversion_tracking` flag. See the
807/// [additional options](#additional-options) section for more information.
808///
809/// </div>
810///
811/// As per recommendation by the Kubernetes project, conversions should aim to
812/// be infallible and lossless. The above example perfectly illustrates that
813/// achieving this is not as easy as it looks on the surface. Let's assume the
814/// following conditions:
815///
816/// - The CRD's latest version `v1` is marked as the stored version.
817/// - A client requests a CR in an earlier version, in this case `v1alpha1`.
818///
819/// The Kubernetes apiserver retrieves the stored object in `v1` from etcd.
820/// It needs to be downgraded to `v1alpha1` to be able to serve the client the
821/// correct requested version. As defined above, the field `baz` was only added
822/// in `v1` and `bar` was added in `v1beta1` and as such both don't exist in
823/// `v1alpha1`. During the downgrade, the conversion would lose these pieces of
824/// data when upgrading to `v1` again after the client did it's changes. This
825/// macro however provides a mechanism to automatically track values across
826/// conversations without data loss.
827///
828/// <div class="warning">
829///
830/// Currently, only tracking of **added** fields is supported. This will be
831/// expanded to removed fields, field type changes, and fields containing
832/// collections in the future.
833///
834/// </div>
835///
836/// There are many moving parts to enable this mechanism to work automatically
837/// with minimal manual developer input. Pretty much all of the required code
838/// can be generated based on a simple CRD definition described above. The
839/// following paragraphs explain various parts of the system in more detail.
840/// For a complete overview, it is however advised to look at the source code
841/// of the macro and the code it produces.
842///
843/// #### Tracking Values in the Status
844///
845/// Tracking changed values across conversions requires state. In this context,
846/// storing this state as close to the CustomResource as possible is essential.
847/// This is due to various factors such as avoiding external calls (which are
848/// susceptible to network errors), reducing the risk of state drift, and
849/// making the use of conversions as easy as possible straight out of the box;
850/// for both users and cluster administrators.
851///
852/// As such, values are tracked using the CustomResource's status via the
853/// `changedValues` field. This field contains two sections, one for upgrades
854/// and one for downgrades. Both of these sections contain version keys which
855/// list all tracked values for a particular version.
856///
857/// ```yaml
858/// status:
859///   changedValues:
860///     downgrades: null
861///     upgrades:
862///       v1beta1:
863///         - fieldName: "bar"
864///           value: 42
865///       v1:
866///         - fieldName: "baz"
867///           value: true
868/// ```
869///
870/// To continue the above example, upgrading the CustomResource from `v1alpha1`
871/// back to `v1` requires us to re-hydrate the resource with the tracked values.
872/// First, the resource is upgraded to `v1beta1`. During this step, the tracked
873/// value for the `bar` field is applied and removed from the status afterwards.
874/// The final upgrade to `v1` will apply the tracked value for the field `baz`.
875/// Again, it is removed from the status afterwards.
876///
877/// ### Tracking Nested Changes
878///
879/// To be able to automatically track values of changed fields in nested sub
880/// structs of specs, the fields needs to be marked with `#[versioned(nested)]`.
881/// This will indicate the macro to generate the appropriate conversion
882/// functions.
883///
884/// ```
885/// # use stackable_versioned_macros::versioned;
886/// # use kube::CustomResource;
887/// # use schemars::JsonSchema;
888/// # use serde::{Deserialize, Serialize};
889/// #[versioned(
890///     version(name = "v1alpha1"),
891///     version(name = "v1beta1"),
892///     options(k8s(experimental_conversion_tracking))
893/// )]
894/// mod versioned {
895///     #[versioned(crd(group = "example.com", doc = "My custom resource."))]
896///     #[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)]
897///     struct FooSpec {
898///         bar: usize,
899///
900///         // TODO: This technically needs to be combined with a change, but
901///         // we want proper, per-container versioning before we add the correct
902///         // attributes here.
903///         #[versioned(nested)]
904///         baz: Baz,
905///     }
906///
907///     #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
908///     struct Baz {
909///         quax: String,
910///
911///         #[versioned(added(since = "v1beta1"))]
912///         quox: bool,
913///     }
914/// }
915/// # fn main() {}
916/// ```
917///
918/// # OpenTelemetry Semantic Conventions
919///
920/// If tracing is enabled, various traces and events are emitted. The fields of
921/// these signals follow the general rules of OpenTelemetry semantic conventions.
922/// There are currently no agreed-upon semantic conventions for CRD conversions.
923/// In the meantime these fields are used:
924///
925/// | Field | Type (Example) | Description |
926/// | :---- | :------------- | :---------- |
927/// | `k8s.crd.conversion.converted_object_count` | usize (6) | The number of successfully converted objects sent back in a conversion review |
928/// | `k8s.crd.conversion.desired_api_version` | String (v1alpha1) | The desired api version received via a conversion review |
929/// | `k8s.crd.conversion.api_version` | String (v1beta1) | The current api version of an object received via a conversion review |
930/// | `k8s.crd.conversion.steps` | usize (2) | The number of steps required to convert a single object from the current to the desired version |
931/// | `k8s.crd.conversion.kind` | String (Foo) | The kind of the CRD |
932///
933/// [1]: https://docs.rs/schemars/latest/schemars/derive.JsonSchema.html
934/// [2]: https://docs.rs/kube/latest/kube/core/crd/fn.merge_crds.html
935/// [k8s-version-format]: https://kubernetes.io/docs/reference/using-api/#api-versioning
936/// [k8s-crd-ver-deprecation]: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/#version-deprecation
937#[proc_macro_attribute]
938pub fn versioned(attrs: TokenStream, input: TokenStream) -> TokenStream {
939    let input = syn::parse_macro_input!(input as Item);
940    versioned_impl(attrs.into(), input).into()
941}
942
943fn versioned_impl(attrs: proc_macro2::TokenStream, input: Item) -> proc_macro2::TokenStream {
944    // TODO (@Techassi): Think about how we can handle nested structs / enums which
945    // are also versioned.
946
947    match input {
948        Item::Mod(item_mod) => {
949            let module_attributes: ModuleAttributes = match parse_outer_attributes(attrs) {
950                Ok(ma) => ma,
951                Err(err) => return err.write_errors(),
952            };
953
954            let module = match Module::new(item_mod, module_attributes) {
955                Ok(module) => module,
956                Err(err) => return err.write_errors(),
957            };
958
959            module.generate_tokens()
960        }
961        _ => Error::new(
962            input.span(),
963            "attribute macro `versioned` can be only be applied to modules",
964        )
965        .into_compile_error(),
966    }
967}
968
969fn parse_outer_attributes<T>(attrs: proc_macro2::TokenStream) -> Result<T, darling::Error>
970where
971    T: FromMeta,
972{
973    let nm = NestedMeta::parse_meta_list(attrs)?;
974    T::from_list(&nm)
975}
976
977#[cfg(test)]
978mod snapshots {
979    use insta::{assert_snapshot, glob};
980
981    use super::*;
982
983    #[test]
984    fn pass() {
985        // TODO (@Techassi): Re-add skip tests
986        let _settings_guard = test_utils::set_snapshot_path().bind_to_scope();
987
988        glob!("../tests/inputs/pass", "*.rs", |path| {
989            let formatted = test_utils::expand_from_file(path)
990                .inspect_err(|err| eprintln!("{err}"))
991                .unwrap();
992            assert_snapshot!(formatted);
993        });
994    }
995}