Skip to main content

prost_helpers/
lib.rs

1// Copyright 2022 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![cfg_attr(coverage, feature(coverage_attribute))]
16#![feature(iterator_try_collect)]
17
18use proc_macro::TokenStream;
19use proc_macro2::{Span, TokenStream as TokenStream2};
20use quote::{format_ident, quote};
21use syn::{Data, DataEnum, DataStruct, DeriveInput, Result, parse_macro_input};
22
23mod generate;
24
25/// This attribute will be placed before any pb types, including messages and enums.
26/// See `prost/helpers/README.md` for more details.
27#[proc_macro_derive(AnyPB)]
28pub fn any_pb(input: TokenStream) -> TokenStream {
29    // Parse the string representation
30    let ast = parse_macro_input!(input as DeriveInput);
31
32    match produce(&ast) {
33        Ok(tokens) => tokens.into(),
34        Err(e) => e.to_compile_error().into(),
35    }
36}
37
38#[proc_macro_derive(StreamNodeBodyVariants)]
39pub fn stream_node_body_variants(input: TokenStream) -> TokenStream {
40    let ast = parse_macro_input!(input as DeriveInput);
41
42    match produce_stream_node_body_variants(&ast) {
43        Ok(tokens) => tokens.into(),
44        Err(e) => e.to_compile_error().into(),
45    }
46}
47
48fn produce_stream_node_body_variants(ast: &DeriveInput) -> Result<TokenStream2> {
49    if ast.ident != "NodeBody" {
50        return Err(syn::Error::new_spanned(
51            &ast.ident,
52            "StreamNodeBodyVariants can only be derived for stream_plan::stream_node::NodeBody",
53        ));
54    }
55
56    let Data::Enum(DataEnum { variants, .. }) = &ast.data else {
57        return Err(syn::Error::new_spanned(
58            ast,
59            "StreamNodeBodyVariants can only be derived for enums",
60        ));
61    };
62
63    let variants: Vec<_> = variants.iter().map(|variant| &variant.ident).collect();
64    let marker_types: Vec<_> = variants
65        .iter()
66        .map(|variant| format_ident!("{variant}Variant"))
67        .collect();
68    let dollar = quote!($);
69
70    Ok(quote! {
71        #(
72            #[derive(Debug, Clone, Copy)]
73            pub struct #marker_types;
74        )*
75
76        #[macro_export]
77        #[doc(hidden)]
78        macro_rules! __dispatch_stream_node_body {
79            (#dollar body:expr, #dollar node_body:ident, #dollar node:ident => #dollar call:expr) => {
80                match #dollar body {
81                    #(
82                        ::risingwave_pb::stream_plan::stream_node::NodeBody::#variants(#dollar node) => {
83                            type #dollar node_body = ::risingwave_pb::stream_plan::stream_node::#marker_types;
84                            #dollar call
85                        }
86                    )*
87                }
88            };
89        }
90    })
91}
92
93// Procedure macros can not be tested from the same crate.
94fn produce(ast: &DeriveInput) -> Result<TokenStream2> {
95    let name = &ast.ident;
96
97    // Is it a struct?
98    let struct_get = if let syn::Data::Struct(DataStruct { ref fields, .. }) = ast.data {
99        let generated: Vec<_> = fields.iter().map(generate::implement).try_collect()?;
100        quote! {
101            impl #name {
102                #(#generated)*
103            }
104        }
105    } else {
106        // Do nothing.
107        quote! {}
108    };
109
110    // Add a `Pb`-prefixed alias for all types.
111    // No need to add docs for this alias as rust-analyzer will forward the docs to the original type.
112    let pb_alias = {
113        let pb_name = format_ident!("Pb{name}");
114        quote! {
115            pub type #pb_name = #name;
116        }
117    };
118
119    Ok(quote! {
120        #pb_alias
121        #struct_get
122    })
123}
124
125#[proc_macro_derive(Version)]
126pub fn version(input: TokenStream) -> TokenStream {
127    fn version_inner(ast: &DeriveInput) -> syn::Result<TokenStream2> {
128        let last_variant = match &ast.data {
129            Data::Enum(v) => v.variants.iter().next_back().ok_or_else(|| {
130                syn::Error::new(
131                    Span::call_site(),
132                    "This macro requires at least one variant in the enum.",
133                )
134            })?,
135            _ => {
136                return Err(syn::Error::new(
137                    Span::call_site(),
138                    "This macro only supports enums.",
139                ));
140            }
141        };
142
143        let enum_name = &ast.ident;
144        let last_variant_name = &last_variant.ident;
145
146        Ok(quote! {
147            impl #enum_name {
148                pub const LATEST: Self = Self::#last_variant_name;
149            }
150        })
151    }
152
153    let ast = parse_macro_input!(input as DeriveInput);
154
155    match version_inner(&ast) {
156        Ok(tokens) => tokens.into(),
157        Err(e) => e.to_compile_error().into(),
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use quote::quote;
164    use syn::parse_quote;
165
166    use super::*;
167
168    fn normalize(tokens: TokenStream2) -> String {
169        tokens
170            .to_string()
171            .split_whitespace()
172            .collect::<Vec<_>>()
173            .join(" ")
174    }
175
176    #[test]
177    fn stream_node_body_variants_generates_markers_and_dispatch() {
178        let input = parse_quote! {
179            enum NodeBody {
180                Source(Box<SourceNode>),
181                Project(Box<ProjectNode>),
182            }
183        };
184
185        let dollar = quote!($);
186        let expected_code = quote! {
187            #[derive(Debug, Clone, Copy)]
188            pub struct SourceVariant;
189
190            #[derive(Debug, Clone, Copy)]
191            pub struct ProjectVariant;
192
193            #[macro_export]
194            #[doc(hidden)]
195            macro_rules! __dispatch_stream_node_body {
196                (#dollar body:expr, #dollar node_body:ident, #dollar node:ident => #dollar call:expr) => {
197                    match #dollar body {
198                        ::risingwave_pb::stream_plan::stream_node::NodeBody::Source(#dollar node) => {
199                            type #dollar node_body = ::risingwave_pb::stream_plan::stream_node::SourceVariant;
200                            #dollar call
201                        }
202                        ::risingwave_pb::stream_plan::stream_node::NodeBody::Project(#dollar node) => {
203                            type #dollar node_body = ::risingwave_pb::stream_plan::stream_node::ProjectVariant;
204                            #dollar call
205                        }
206                    }
207                };
208            }
209        };
210
211        assert_eq!(
212            normalize(produce_stream_node_body_variants(&input).unwrap()),
213            normalize(expected_code)
214        );
215    }
216
217    #[test]
218    fn stream_node_body_variants_rejects_unexpected_enum_name() {
219        let input = parse_quote! {
220            enum OtherBody {
221                Source(Box<SourceNode>),
222            }
223        };
224
225        let err = produce_stream_node_body_variants(&input).unwrap_err();
226        assert_eq!(
227            err.to_string(),
228            "StreamNodeBodyVariants can only be derived for stream_plan::stream_node::NodeBody"
229        );
230    }
231}