k8s_openapi_derive/
lib.rs

1#![recursion_limit = "1024"]
2#![warn(rust_2018_idioms)]
3#![deny(clippy::all, clippy::pedantic)]
4#![allow(
5    clippy::too_many_lines,
6)]
7
8//! This crate contains custom derives related to the [`k8s-openapi`](https://crates.io/crates/k8s-openapi) crate.
9
10mod custom_resource_definition;
11
12trait CustomDerive: Sized {
13    fn parse(input: syn::DeriveInput, tokens: proc_macro2::TokenStream) -> Result<Self, syn::Error>;
14    fn emit(self) -> Result<proc_macro2::TokenStream, syn::Error>;
15}
16
17fn run_custom_derive<T>(input: proc_macro::TokenStream) -> proc_macro::TokenStream where T: CustomDerive {
18    let input: proc_macro2::TokenStream = input.into();
19    let tokens = input.clone();
20    let token_stream = match syn::parse2(input).and_then(|input| <T as CustomDerive>::parse(input, tokens)).and_then(<T as CustomDerive>::emit) {
21        Ok(token_stream) => token_stream,
22        Err(err) => err.to_compile_error(),
23    };
24    token_stream.into()
25}
26
27trait ResultExt<T> {
28    fn spanning(self, spanned: impl quote::ToTokens) -> Result<T, syn::Error>;
29}
30
31impl<T, E> ResultExt<T> for Result<T, E> where E: std::fmt::Display {
32    fn spanning(self, spanned: impl quote::ToTokens) -> Result<T, syn::Error> {
33        self.map_err(|err| syn::Error::new_spanned(spanned, err))
34    }
35}
36
37/// This custom derive can be used on a Kubernetes custom resource spec type to generate a custom resource definition object type
38/// and related trait impls.
39///
40/// # Example
41///
42/// ```rust,ignore
43/// #[derive(
44///     Clone, Debug, PartialEq,
45///     k8s_openapi_derive::CustomResourceDefinition,
46///     serde::Deserialize, serde::Serialize,
47/// )]
48/// #[custom_resource_definition(
49///     group = "k8s-openapi-tests-custom-resource-definition.com",
50///     version = "v1",
51///     plural = "foobars",
52///     generate_schema,
53///     namespaced,
54///     has_subresources = "v1",
55///     impl_deep_merge,
56/// )]
57/// struct FooBarSpec {
58///     prop1: String,
59///     prop2: Vec<bool>,
60///     #[serde(skip_serializing_if = "Option::is_none")]
61///     prop3: Option<i32>,
62/// }
63/// ```
64///
65/// Note:
66///
67/// - The spec type must impl the following traits (either manually or via `#[derive]`): `Clone`, `Debug`, `PartialEq`,
68///   `serde::Deserialize` and `serde::Serialize`
69///
70/// - The name of the spec type must end with `Spec`. This suffix is trimmed to generate the name of the object type.
71///
72/// - The `k8s_openapi` crate must have been added as a dependency, since the macro expansion refers to types from it.
73///
74/// The custom derive then generates a `FooBar` type that represents a custom resource corresponding to this definition:
75///
76/// ```rust,ignore
77/// /// Custom resource for FooBarSpec
78/// #[derive(Clone, Debug, Default, PartialEq)]
79/// struct FooBar {
80///     metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta,
81///     spec: Option<FooBarSpec>,
82///     subresources: k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceSubresources,
83/// }
84///
85/// impl k8s_openapi::Resource for FooBar { ... }
86///
87/// impl k8s_openapi::ListableResource for FooBar { ... }
88///
89/// impl k8s_openapi::Metadata for FooBar {
90///     type Ty = k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
91///
92///     ...
93/// }
94///
95/// impl<'de> k8s_openapi::serde::Deserialize<'de> for FooBar { ... }
96///
97/// impl k8s_openapi::serde::Serialize for FooBar { ... }
98///
99/// impl k8s_openapi::schemars::JsonSchema for FooBar { ... }
100/// ```
101///
102/// The name of this type is automatically derived from the name of the spec type by truncating the `Spec` suffix.
103///
104/// The `group` and `version` meta items of the `#[custom_resource_definition]` attribute of the macro are used to set
105/// the "group" and "API version" in the `k8s_openapi::Resource` impl respectively. The "kind" is automatically set to be the same as the resource type name,
106/// ie `"FooBar"` in this example. The `plural` meta item is used to construct the URLs of API operations for this custom resource.
107///
108/// The `generate_schema` meta item is optional. If set, the generated custom resource type will have an impl of `schemars::JsonSchema` from the `schemars` crate.
109/// The `schemars` feature of the `k8s-openapi` crate must be enabled so that the types in that crate also have their `schemars::JsonSchema` impls enabled.
110/// You will also need to impl `schemars::JsonSchema` on the `Spec` type itself, either manually or via `#[derive(schemars::JsonSchema)]`.
111///
112/// The `has_subresources` meta item is optional. If set, the generated custom resource type will have a `subresources` field. The value of the meta item
113/// specifies which namespace the type will be used from. For example, setting `has_subresources = "v1"` causes the field to be of the
114/// `k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceSubresources` type.
115///
116/// The `impl_deep_merge` meta item is optional. If set, the generated custom resource type will impl the `k8s_openapi::DeepMerge` trait. This impl will require
117/// you to impl `k8s_openapi::DeepMerge` on the spec type yourself.
118///
119/// You would then register this custom resource definition with Kubernetes, with code like this:
120///
121/// ```rust,ignore
122/// use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1 as apiextensions;
123/// use k8s_openapi::apimachinery::pkg::apis::meta::v1 as meta;
124///
125/// // Same as the `plurals` meta item in the `#[custom_resource_definition]` attribute
126/// let plural = "foobars";
127///
128/// let custom_resource_definition_spec = apiextensions::CustomResourceDefinitionSpec {
129///     group: <FooBar as k8s_openapi::Resource>::GROUP.to_owned(),
130///     names: apiextensions::CustomResourceDefinitionNames {
131///         kind: <FooBar as k8s_openapi::Resource>::KIND.to_owned(),
132///         plural: plural.to_owned(),
133///         short_names: Some(vec!["fb".to_owned()]),
134///         singular: Some("foobar".to_owned()),
135///         ..Default::default()
136///     },
137///     scope: "Namespaced".to_owned(),
138///     version: <FooBar as k8s_openapi::Resource>::VERSION.to_owned().into(),
139///     ..Default::default()
140/// };
141///
142/// let custom_resource_definition = apiextensions::CustomResourceDefinition {
143///     metadata: meta::ObjectMeta {
144///         name: Some(format!("{plural}.{}", <FooBar as k8s_openapi::Resource>::GROUP)),
145///         ..Default::default()
146///     },
147///     spec: custom_resource_definition_spec.into(),
148///     ..Default::default()
149/// };
150///
151/// client.create(custom_resource_definition);
152/// ```
153///
154/// (You may wish to generate run your crate through `cargo-expand` to see the macro expansion.)
155///
156/// See the [`custom_resource_definition` test in the repository](https://github.com/Arnavion/k8s-openapi/blob/master/k8s-openapi-tests/src/custom_resource_definition.rs)
157/// for a full example of using this custom derive.
158#[proc_macro_derive(CustomResourceDefinition, attributes(custom_resource_definition))]
159pub fn derive_custom_resource_definition(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
160    run_custom_derive::<custom_resource_definition::CustomResourceDefinition>(input)
161}