Skip to content
File

Blob: src/rust/jsg-macros/utils.rs

rust198 lines
1// Copyright (c) 2026 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5//! Shared utility helpers used across the jsg-macros crate.
6 
7use proc_macro::TokenStream;
8use quote::ToTokens;
9use syn::Data;
10use syn::DeriveInput;
11use syn::Fields;
12 
13/// Extracts named fields from a struct, returning an empty list for unit structs.
14/// Returns `Err` with a compile error for tuple structs or non-struct data.
15pub fn extract_named_fields(
16 input: &DeriveInput,
17 macro_name: &str,
18) -> Result<syn::punctuated::Punctuated<syn::Field, syn::token::Comma>, TokenStream> {
19 match &input.data {
20 Data::Struct(data) => match &data.fields {
21 Fields::Named(fields) => Ok(fields.named.clone()),
22 Fields::Unit => Ok(syn::punctuated::Punctuated::new()),
23 Fields::Unnamed(_) => Err(error(
24 input,
25 &format!("#[{macro_name}] does not support tuple structs"),
26 )),
27 },
28 _ => Err(error(
29 input,
30 &format!("#[{macro_name}] can only be applied to structs or impl blocks"),
31 )),
32 }
33}
34 
35/// Checks if an attribute matches a given name, handling both unqualified (`#[jsg_method]`)
36/// and qualified (`#[jsg_macros::jsg_method]`) paths.
37pub fn is_attr(attr: &syn::Attribute, name: &str) -> bool {
38 attr.path().is_ident(name) || attr.path().segments.last().is_some_and(|s| s.ident == name)
39}
40 
41/// Returns `true` if the `custom_trace` bare word is present in the attribute token stream.
42///
43/// Handles both bare `custom_trace` and combined forms like `name = "Foo", custom_trace`.
44/// When set, `#[jsg_resource]` on a struct suppresses the auto-generated `Traced`
45/// impl, letting the user write their own.
46pub fn has_custom_trace_flag(attr: &TokenStream) -> bool {
47 use syn::Meta;
48 use syn::punctuated::Punctuated;
49 
50 let Ok(parsed) = syn::parse::Parser::parse(
51 Punctuated::<Meta, syn::Token![,]>::parse_terminated,
52 attr.clone(),
53 ) else {
54 return false;
55 };
56 
57 parsed
58 .iter()
59 .any(|meta| matches!(meta, Meta::Path(path) if path.is_ident("custom_trace")))
60}
61 
62/// Emits a `compile_error!` token stream anchored to `tokens` with message `msg`.
63pub fn error(tokens: &impl ToTokens, msg: &str) -> TokenStream {
64 syn::Error::new_spanned(tokens, msg)
65 .to_compile_error()
66 .into()
67}
68 
69/// Extracts the `name = "..."` value from an attribute token stream.
70///
71/// Handles combined forms like `name = "Foo", custom_trace` by parsing the
72/// token stream as comma-separated `syn::Meta` items and finding the first
73/// `name = "..."` name-value pair.
74pub fn extract_name_attribute(tokens: TokenStream) -> Option<String> {
75 use syn::Meta;
76 use syn::punctuated::Punctuated;
77 
78 let parsed: Punctuated<Meta, syn::Token![,]> =
79 syn::parse::Parser::parse(Punctuated::parse_terminated, tokens).ok()?;
80 
81 for meta in &parsed {
82 if let Meta::NameValue(nv) = meta
83 && nv.path.is_ident("name")
84 && let syn::Expr::Lit(syn::ExprLit {
85 lit: syn::Lit::Str(s),
86 ..
87 }) = &nv.value
88 {
89 return Some(s.value());
90 }
91 }
92 
93 None
94}
95 
96/// Converts a `snake_case` identifier to `camelCase`.
97pub fn snake_to_camel(s: &str) -> String {
98 let mut result = String::new();
99 let mut cap_next = false;
100 for (i, c) in s.chars().enumerate() {
101 match c {
102 '_' => cap_next = true,
103 _ if i == 0 => result.push(c),
104 _ if cap_next => {
105 result.push(c.to_ascii_uppercase());
106 cap_next = false;
107 }
108 _ => result.push(c),
109 }
110 }
111 result
112}
113 
114/// Checks if a type is `Result<T, E>`.
115pub fn is_result_type(ty: &syn::Type) -> bool {
116 if let syn::Type::Path(type_path) = ty
117 && let Some(segment) = type_path.path.segments.last()
118 {
119 return segment.ident == "Result";
120 }
121 false
122}
123 
124/// Returns true if the type is `&mut Lock` or `&mut jsg::Lock`.
125///
126/// When a method's first typed parameter matches this pattern, the macro passes the
127/// callback's `lock` directly instead of extracting it from JavaScript arguments.
128pub fn is_lock_ref(ty: &syn::Type) -> bool {
129 let syn::Type::Reference(ref_type) = ty else {
130 return false;
131 };
132 if ref_type.mutability.is_none() {
133 return false;
134 }
135 let syn::Type::Path(type_path) = ref_type.elem.as_ref() else {
136 return false;
137 };
138 let segments: Vec<_> = type_path.path.segments.iter().collect();
139 match segments.len() {
140 // `&mut Lock` โ€” bare import (assumes `use jsg::Lock;`)
141 1 => segments[0].ident == "Lock",
142 // `&mut jsg::Lock` โ€” fully qualified path
143 2 => segments[0].ident == "jsg" && segments[1].ident == "Lock",
144 _ => false,
145 }
146}
147 
148#[cfg(test)]
149mod tests {
150 use syn::parse_quote;
151 
152 use super::*;
153 
154 #[test]
155 fn snake_to_camel_cases() {
156 // First char is never uppercased; each `_` capitalises the next letter.
157 assert_eq!(snake_to_camel(""), "");
158 assert_eq!(snake_to_camel("hello"), "hello");
159 assert_eq!(snake_to_camel("get_name"), "getName");
160 assert_eq!(snake_to_camel("parse_caa_record"), "parseCaaRecord");
161 assert_eq!(snake_to_camel("alreadyCamel"), "alreadyCamel");
162 // A leading `_` sets cap_next; the next char is capitalised.
163 assert_eq!(snake_to_camel("_private"), "Private");
164 // Consecutive underscores โ€” the second just re-sets cap_next.
165 assert_eq!(snake_to_camel("a__b"), "aB");
166 }
167 
168 #[test]
169 fn is_result_type_cases() {
170 assert!(is_result_type(&parse_quote!(Result<String, Error>)));
171 // Qualified path: last segment is still `Result`.
172 assert!(is_result_type(&parse_quote!(std::result::Result<(), ()>)));
173 assert!(!is_result_type(&parse_quote!(Option<String>)));
174 assert!(!is_result_type(&parse_quote!(String)));
175 }
176 
177 #[test]
178 fn is_lock_ref_cases() {
179 assert!(is_lock_ref(&parse_quote!(&mut Lock)));
180 assert!(is_lock_ref(&parse_quote!(&mut jsg::Lock)));
181 // Immutable ref, wrong type, or not a ref at all must all return false.
182 assert!(!is_lock_ref(&parse_quote!(&Lock)));
183 assert!(!is_lock_ref(&parse_quote!(&mut String)));
184 assert!(!is_lock_ref(&parse_quote!(Lock)));
185 }
186 
187 #[test]
188 fn is_attr_cases() {
189 let simple: syn::ItemFn = parse_quote! { #[jsg_method] fn foo() {} };
190 let qualified: syn::ItemFn = parse_quote! { #[jsg_macros::jsg_method] fn foo() {} };
191 
192 assert!(is_attr(&simple.attrs[0], "jsg_method"));
193 assert!(!is_attr(&simple.attrs[0], "jsg_resource"));
194 // Qualified path (`jsg_macros::jsg_method`) must also match by last segment.
195 assert!(is_attr(&qualified.attrs[0], "jsg_method"));
196 }
197}