util_macros.rs

  1use convert_case::{Case, Casing};
  2use proc_macro::TokenStream;
  3use proc_macro2::TokenStream as TokenStream2;
  4use quote::{format_ident, quote};
  5use syn::{
  6    Data, DeriveInput, Expr, ExprArray, ExprLit, Fields, Lit, LitStr, MetaNameValue, Token,
  7    parse_macro_input, punctuated::Punctuated,
  8};
  9
 10/// A macro used in tests for cross-platform path string literals in tests. On Windows it replaces
 11/// `/` with `\\` and adds `C:` to the beginning of absolute paths. On other platforms, the path is
 12/// returned unmodified.
 13///
 14/// # Example
 15/// ```rust
 16/// use util_macros::path;
 17///
 18/// let path = path!("/Users/user/file.txt");
 19/// #[cfg(target_os = "windows")]
 20/// assert_eq!(path, "C:\\Users\\user\\file.txt");
 21/// #[cfg(not(target_os = "windows"))]
 22/// assert_eq!(path, "/Users/user/file.txt");
 23/// ```
 24#[proc_macro]
 25pub fn path(input: TokenStream) -> TokenStream {
 26    let path = parse_macro_input!(input as LitStr);
 27    let mut path = path.value();
 28
 29    #[cfg(target_os = "windows")]
 30    {
 31        path = path.replace("/", "\\");
 32        if path.starts_with("\\") {
 33            path = format!("C:{}", path);
 34        }
 35    }
 36
 37    TokenStream::from(quote! {
 38        #path
 39    })
 40}
 41
 42/// This macro replaces the path prefix `file:///` with `file:///C:/` for Windows.
 43/// But if the target OS is not Windows, the URI is returned as is.
 44///
 45/// # Example
 46/// ```rust
 47/// use util_macros::uri;
 48///
 49/// let uri = uri!("file:///path/to/file");
 50/// #[cfg(target_os = "windows")]
 51/// assert_eq!(uri, "file:///C:/path/to/file");
 52/// #[cfg(not(target_os = "windows"))]
 53/// assert_eq!(uri, "file:///path/to/file");
 54/// ```
 55#[proc_macro]
 56pub fn uri(input: TokenStream) -> TokenStream {
 57    let uri = parse_macro_input!(input as LitStr);
 58    let uri = uri.value();
 59
 60    #[cfg(target_os = "windows")]
 61    let uri = uri.replace("file:///", "file:///C:/");
 62
 63    TokenStream::from(quote! {
 64        #uri
 65    })
 66}
 67
 68/// This macro replaces the line endings `\n` with `\r\n` for Windows.
 69/// But if the target OS is not Windows, the line endings are returned as is.
 70///
 71/// # Example
 72/// ```rust
 73/// use util_macros::line_endings;
 74///
 75/// let text = line_endings!("Hello\nWorld");
 76/// #[cfg(target_os = "windows")]
 77/// assert_eq!(text, "Hello\r\nWorld");
 78/// #[cfg(not(target_os = "windows"))]
 79/// assert_eq!(text, "Hello\nWorld");
 80/// ```
 81#[proc_macro]
 82pub fn line_endings(input: TokenStream) -> TokenStream {
 83    let text = parse_macro_input!(input as LitStr);
 84    let text = text.value();
 85
 86    #[cfg(target_os = "windows")]
 87    let text = text.replace("\n", "\r\n");
 88
 89    TokenStream::from(quote! {
 90        #text
 91    })
 92}
 93
 94#[proc_macro_derive(FieldAccessByEnum, attributes(field_access_by_enum))]
 95pub fn derive_field_access_by_enum(input: TokenStream) -> TokenStream {
 96    let input = parse_macro_input!(input as DeriveInput);
 97
 98    let struct_name = &input.ident;
 99
100    let mut enum_name = None;
101    let mut enum_attrs: Vec<TokenStream2> = Vec::new();
102
103    for attr in &input.attrs {
104        if attr.path().is_ident("field_access_by_enum") {
105            let name_values: Punctuated<MetaNameValue, Token![,]> =
106                attr.parse_args_with(Punctuated::parse_terminated).unwrap();
107            for name_value in name_values {
108                if name_value.path.is_ident("enum_name") {
109                    let value = name_value.value;
110                    match value {
111                        Expr::Lit(ExprLit {
112                            lit: Lit::Str(name),
113                            ..
114                        }) => enum_name = Some(name.value()),
115                        _ => panic!("Expected string literal in enum_name attribute"),
116                    }
117                } else if name_value.path.is_ident("enum_attrs") {
118                    let value = name_value.value;
119                    match value {
120                        Expr::Array(ExprArray { elems, .. }) => {
121                            for elem in elems {
122                                enum_attrs.push(quote!(#[#elem]));
123                            }
124                        }
125                        _ => panic!("Expected array literal in enum_attr attribute"),
126                    }
127                } else {
128                    if let Some(ident) = name_value.path.get_ident() {
129                        panic!("Unrecognized argument name {}", ident);
130                    } else {
131                        panic!("Unrecognized argument {:?}", name_value.path);
132                    }
133                }
134            }
135        }
136    }
137    let Some(enum_name) = enum_name else {
138        panic!("#[field_access_by_enum(enum_name = \"...\")] attribute is required");
139    };
140    let enum_ident = format_ident!("{}", enum_name);
141
142    let fields = match input.data {
143        Data::Struct(data_struct) => match data_struct.fields {
144            Fields::Named(fields) => fields.named,
145            _ => panic!("FieldAccessByEnum can only be derived for structs with named fields"),
146        },
147        _ => panic!("FieldAccessByEnum can only be derived for structs"),
148    };
149
150    if fields.is_empty() {
151        panic!("FieldAccessByEnum cannot be derived for structs with no fields");
152    }
153
154    let mut enum_variants = Vec::new();
155    let mut get_match_arms = Vec::new();
156    let mut set_match_arms = Vec::new();
157    let mut field_types = Vec::new();
158
159    for field in fields.iter() {
160        let field_name = field.ident.as_ref().unwrap();
161        let variant_name = field_name.to_string().to_case(Case::Pascal);
162        let variant_ident = format_ident!("{}", variant_name);
163        let field_type = &field.ty;
164
165        enum_variants.push(variant_ident.clone());
166        field_types.push(field_type);
167
168        get_match_arms.push(quote! {
169            #enum_ident::#variant_ident => &self.#field_name,
170        });
171
172        set_match_arms.push(quote! {
173            #enum_ident::#variant_ident => self.#field_name = value,
174        });
175    }
176
177    let first_type = &field_types[0];
178    let all_same_type = field_types
179        .iter()
180        .all(|ty| quote!(#ty).to_string() == quote!(#first_type).to_string());
181    if !all_same_type {
182        panic!("Fields have different types.");
183    }
184    let field_value_type = quote! { #first_type };
185
186    let expanded = quote! {
187        #(#enum_attrs)*
188        pub enum #enum_ident {
189            #(#enum_variants),*
190        }
191
192        impl util::FieldAccessByEnum<#field_value_type> for #struct_name {
193            type Field = #enum_ident;
194
195            fn get_field_by_enum(&self, field: Self::Field) -> &#field_value_type {
196                match field {
197                    #(#get_match_arms)*
198                }
199            }
200
201            fn set_field_by_enum(&mut self, field: Self::Field, value: #field_value_type) {
202                match field {
203                    #(#set_match_arms)*
204                }
205            }
206        }
207    };
208
209    TokenStream::from(expanded)
210}