risingwave_common_proc_macro/
serde_prefix_all.rs

1// Copyright 2025 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// From https://github.com/zeenix/serde-prefix-all:
16//
17// MIT License
18//
19// Copyright (c) 2019 Jonathan Sundqvist
20// Copyright (c) 2025 Zeeshan Ali Khan
21//
22// Permission is hereby granted, free of charge, to any person obtaining a copy
23// of this software and associated documentation files (the "Software"), to deal
24// in the Software without restriction, including without limitation the rights
25// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
26// copies of the Software, and to permit persons to whom the Software is
27// furnished to do so, subject to the following conditions:
28//
29// The above copyright notice and this permission notice shall be included in all
30// copies or substantial portions of the Software.
31//
32// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
33// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
34// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
35// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
36// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
37// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
38// SOFTWARE.
39
40use proc_macro::TokenStream;
41use proc_macro2::Span;
42use quote::ToTokens;
43use syn::spanned::Spanned;
44use syn::{
45    Attribute, AttributeArgs, Data, DataEnum, DataStruct, DeriveInput, Error, Lit, Meta,
46    MetaNameValue, NestedMeta, parse_quote,
47};
48
49#[derive(Copy, Clone)]
50enum Mode {
51    Rename,
52    Alias,
53}
54
55struct ParsedArgs {
56    prefix: String,
57    prefix_span: Span,
58    mode: Mode,
59}
60
61fn parse_args(args: AttributeArgs) -> syn::Result<ParsedArgs> {
62    let mut prefix: Option<(String, Span)> = None;
63    let mut mode: Option<Mode> = None;
64
65    for arg in args {
66        match arg {
67            NestedMeta::Lit(Lit::Str(lit_str)) if prefix.is_none() => {
68                prefix = Some((lit_str.value(), lit_str.span()));
69            }
70            NestedMeta::Lit(Lit::Str(lit_str)) => {
71                return Err(Error::new(
72                    lit_str.span(),
73                    "Prefix is already specified; duplicate string literal",
74                ));
75            }
76            NestedMeta::Lit(lit) => {
77                return Err(Error::new(lit.span(), "The attribute is not a string"));
78            }
79            NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, lit, .. }))
80                if path.is_ident("mode") =>
81            {
82                if mode.is_some() {
83                    return Err(Error::new(path.span(), "mode is already specified"));
84                }
85
86                let parsed_mode = match lit {
87                    Lit::Str(lit_str) => match lit_str.value().as_str() {
88                        "rename" => Mode::Rename,
89                        "alias" => Mode::Alias,
90                        _ => {
91                            return Err(Error::new(
92                                lit_str.span(),
93                                "mode must be \"rename\" or \"alias\"",
94                            ));
95                        }
96                    },
97                    _ => return Err(Error::new(lit.span(), "mode must be a string literal")),
98                };
99
100                mode = Some(parsed_mode);
101            }
102            NestedMeta::Meta(meta) => {
103                return Err(Error::new(
104                    meta.span(),
105                    "Unsupported attribute argument, expected a string literal prefix or mode",
106                ));
107            }
108        }
109    }
110
111    let (prefix, span) =
112        prefix.ok_or_else(|| Error::new(Span::call_site(), "Missing prefix string argument"))?;
113    Ok(ParsedArgs {
114        prefix,
115        prefix_span: span,
116        mode: mode.unwrap_or(Mode::Rename),
117    })
118}
119
120pub(crate) fn try_prefix_all(
121    args: AttributeArgs,
122    mut input: DeriveInput,
123) -> syn::Result<TokenStream> {
124    let ParsedArgs {
125        prefix,
126        prefix_span,
127        mode,
128    } = parse_args(args)?;
129
130    match &mut input.data {
131        Data::Enum(item_enum) => handle_enum(item_enum, &prefix[..], mode)?,
132        Data::Struct(item_struct) => handle_struct(item_struct, &prefix[..], mode)?,
133        _ => {
134            return Err(Error::new(
135                prefix_span,
136                "You can't use the macro on this type",
137            ));
138        }
139    };
140
141    Ok(input.to_token_stream().into())
142}
143
144fn create_attribute(prefix: &str, field_name: &str, mode: Mode) -> Attribute {
145    let attr_prefix = format!("{prefix}{field_name}");
146    match mode {
147        Mode::Rename => parse_quote! { #[serde(rename = #attr_prefix)] },
148        Mode::Alias => parse_quote! { #[serde(alias = #attr_prefix)] },
149    }
150}
151
152fn take_skip_attr(attrs: &mut Vec<Attribute>) -> syn::Result<bool> {
153    let mut found_skip = false;
154    let mut filtered = Vec::with_capacity(attrs.len());
155
156    for attr in attrs.drain(..) {
157        if !attr.path.is_ident("serde_prefix_all") {
158            filtered.push(attr);
159            continue;
160        }
161
162        let meta = attr.parse_meta()?;
163        match meta {
164            Meta::List(list) => {
165                let mut has_skip = false;
166                for nested in &list.nested {
167                    match nested {
168                        NestedMeta::Meta(Meta::Path(path)) if path.is_ident("skip") => {
169                            has_skip = true;
170                        }
171                        _ => {
172                            return Err(Error::new(
173                                nested.span(),
174                                "Expected #[serde_prefix_all(skip)]",
175                            ));
176                        }
177                    }
178                }
179
180                if !has_skip {
181                    return Err(Error::new(
182                        list.span(),
183                        "Expected #[serde_prefix_all(skip)]",
184                    ));
185                }
186
187                found_skip = true;
188            }
189            _ => {
190                return Err(Error::new(
191                    meta.span(),
192                    "Expected #[serde_prefix_all(skip)]",
193                ));
194            }
195        }
196    }
197
198    *attrs = filtered;
199    Ok(found_skip)
200}
201
202fn handle_enum(input: &mut DataEnum, prefix: &str, mode: Mode) -> syn::Result<()> {
203    let variants = &mut input.variants;
204    for variant in variants.iter_mut() {
205        if take_skip_attr(&mut variant.attrs)? {
206            continue;
207        }
208
209        let field_name = variant.ident.to_string();
210        let attr = create_attribute(prefix, &field_name[..], mode);
211        variant.attrs.push(attr);
212    }
213
214    Ok(())
215}
216
217fn handle_struct(input: &mut DataStruct, prefix: &str, mode: Mode) -> syn::Result<()> {
218    let fields = &mut input.fields;
219    for field in fields.iter_mut() {
220        if take_skip_attr(&mut field.attrs)? {
221            continue;
222        }
223
224        let field_name = field.ident.as_ref().unwrap().to_string();
225        let attr = create_attribute(prefix, &field_name[..], mode);
226        field.attrs.push(attr);
227    }
228
229    Ok(())
230}