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}