knative_derive/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{
4    parse_macro_input,
5    Data::Enum,
6    DeriveInput,
7    Ident
8};
9
10/// Derive [`knative_conditions::ConditionType`] on your own `enum` to adhere to the Knative Source schema and condition
11/// management.
12///
13/// Automatically implements [`Default`] on your type, which must be the top level condition.
14///
15/// # Example
16/// ```rust
17/// use knative_derive::ConditionType;
18///
19/// #[derive(ConditionType, Debug, Copy, Clone, PartialEq)]
20/// enum MyCondition {
21///   // First condition must be Ready or Succeeded
22///   Ready,
23///   // Use the dependent attribute to mark conditions
24///   // that are required to consider the resource ready
25///   #[dependent]
26///   SinkProvided,
27///   #[dependent]
28///   Important,
29///   // Conditions that are not marked dependent do not
30///   // determine overall resource readiness
31///   Informational,
32/// }
33/// ```
34#[proc_macro_derive(ConditionType, attributes(dependent))]
35pub fn derive(input: TokenStream) -> TokenStream {
36    let ast = parse_macro_input!(input as DeriveInput);
37
38    let name = &ast.ident;
39    let variants = match ast.data {
40        Enum(syn::DataEnum { ref variants, .. }) => variants,
41        _ => panic!("ConditionType may only be derived on enums")
42    };
43
44    let required_variants = ["Ready", "Succeeded"];
45    let dependents = variants.clone().into_iter()
46        .filter(|v| v.attrs.iter().any(|a| a.path.segments.iter().any(|p| p.ident == "dependent")))
47        .map(|v| v.ident);
48    let dependents_again = dependents.clone();
49
50    let variant_strings: Vec<String> = variants.iter().map(|v| v.ident.to_string()).collect();
51    let dependent_strings: Vec<String> = dependents.clone().map(|d| d.to_string()).collect();
52
53    if let Some(required) = dependent_strings.iter().find(|d| required_variants.contains(&d.as_str())) {
54         panic!("{} variant may not be a dependent", required)
55    }
56
57    // check if both happy variants exist on the enum
58    if required_variants.iter().all(|req| variant_strings.iter().any(|v| v == *req)) {
59         panic!("ConditionType may only contain one of either Ready or Succeeded variants")
60    }
61
62    let happy = variants.iter().find(|v| required_variants.contains(&v.ident.to_string().as_str()))
63            .expect("ConditionType must contain either Ready or Succeeded variant");
64    let capitalized = variants.iter()
65        .map(|v| v.ident.clone())
66        .filter(|v| !required_variants.contains(&v.to_string().as_str()));
67    let lower_case = capitalized.clone()
68        .map(|v| Ident::new(&v.to_string().to_lowercase(), v.span()));
69    let lower_case_doc = capitalized.clone().map(|c| format!("Returns the `{c}` variant of the [`ConditionType`]"));
70    let lower_case_again = lower_case.clone();
71    let lower_case_again_again = lower_case.clone();
72
73    let mark = lower_case.clone().map(|l| Ident::new(&format!("mark_{l}"), l.span()));
74    let mark_with_reason = lower_case.clone().map(|l| Ident::new(&format!("mark_{l}_with_reason"), l.span()));
75    let mark_not = lower_case.clone().map(|l| Ident::new(&format!("mark_not_{l}"), l.span()));
76
77    let condition_type_name = Ident::new(&format!("{name}Type"), name.span());
78    let condition_type_doc = format!("A [`ConditionType`] that implement this trait duck types to [`{name}`].");
79    let manager_name = Ident::new(&format!("{name}Manager"), name.span());
80    let manager_doc = format!("Allows a status to manage [`{name}`].");
81
82    quote! {
83        #[doc = #condition_type_doc]
84        pub trait #condition_type_name: ::knative_conditions::ConditionType {
85            #(
86                #[doc = #lower_case_doc]
87                fn #lower_case() -> Self;
88            )*
89        }
90
91        #[automatically_derived]
92        impl #condition_type_name for #name {
93            #(
94                fn #lower_case_again() -> Self {
95                    #name::#capitalized
96                }
97            )*
98        }
99
100        #[automatically_derived]
101        impl ::knative_conditions::ConditionType for #name {
102            fn happy() -> Self {
103                #name::#happy
104            }
105
106            fn dependents() -> &'static [Self] {
107                &[#(#name::#dependents_again),*]
108            }
109        }
110
111        #[automatically_derived]
112        impl Default for #name {
113            fn default() -> Self {
114                #name::#happy
115            }
116        }
117
118        #[doc = #manager_doc]
119        pub trait #manager_name<S>: ::knative_conditions::ConditionAccessor<S>
120        where S: #condition_type_name {
121            #(
122                fn #mark(&mut self) {
123                    self.manager().mark_true(S::#lower_case_again_again());
124                }
125
126                fn #mark_with_reason(&mut self, reason: &str, message: Option<String>) {
127                    self.manager().mark_true_with_reason(S::#lower_case_again_again(), reason, message);
128                }
129
130                fn #mark_not(&mut self, reason: &str, message: Option<String>) {
131                    self.manager().mark_false(S::#lower_case_again_again(), reason, message);
132                }
133            )*
134        }
135
136        impl<S: #condition_type_name, T: ::knative_conditions::ConditionAccessor<S> + ?Sized> #manager_name<S> for T {}
137    }.into()
138}
139