diff --git a/specta-macros/src/lib.rs b/specta-macros/src/lib.rs index 4bc0a4b8..a75c23ba 100644 --- a/specta-macros/src/lib.rs +++ b/specta-macros/src/lib.rs @@ -63,8 +63,8 @@ pub fn derive_type(input: proc_macro::TokenStream) -> proc_macro::TokenStream { #[proc_macro_attribute] #[cfg(feature = "DO_NOT_USE_function")] pub fn specta( - _: proc_macro::TokenStream, + attr: proc_macro::TokenStream, item: proc_macro::TokenStream, ) -> proc_macro::TokenStream { - specta::attribute(item).unwrap_or_else(|err| err.into_compile_error().into()) + specta::attribute(attr, item).unwrap_or_else(|err| err.into_compile_error().into()) } diff --git a/specta-macros/src/specta.rs b/specta-macros/src/specta.rs index 7446813f..755cfb95 100644 --- a/specta-macros/src/specta.rs +++ b/specta-macros/src/specta.rs @@ -2,11 +2,12 @@ use std::str::FromStr; +use inflector::Inflector; use proc_macro2::TokenStream; use quote::{ToTokens, quote}; use syn::{FnArg, ItemFn, Pat, Visibility, parse}; -use crate::utils::{format_fn_wrapper, parse_attrs}; +use crate::utils::{AttrExtract, format_fn_wrapper, parse_attrs}; fn unraw(s: &str) -> &str { if s.starts_with("r#") { @@ -16,9 +17,89 @@ fn unraw(s: &str) -> &str { } } -pub fn attribute(item: proc_macro::TokenStream) -> syn::Result { +#[derive(Clone, Copy)] +enum RenameAllRule { + Lowercase, + Uppercase, + PascalCase, + CamelCase, + SnakeCase, + ScreamingSnakeCase, + KebabCase, + ScreamingKebabCase, +} + +impl RenameAllRule { + fn parse(value: &str, span: proc_macro2::Span) -> syn::Result { + match value { + "lowercase" => Ok(Self::Lowercase), + "UPPERCASE" => Ok(Self::Uppercase), + "PascalCase" => Ok(Self::PascalCase), + "camelCase" => Ok(Self::CamelCase), + "snake_case" => Ok(Self::SnakeCase), + "SCREAMING_SNAKE_CASE" => Ok(Self::ScreamingSnakeCase), + "kebab-case" => Ok(Self::KebabCase), + "SCREAMING-KEBAB-CASE" => Ok(Self::ScreamingKebabCase), + _ => Err(syn::Error::new( + span, + "specta: unsupported rename rule. Expected one of lowercase, UPPERCASE, PascalCase, camelCase, snake_case, SCREAMING_SNAKE_CASE, kebab-case, SCREAMING-KEBAB-CASE", + )), + } + } + + fn apply(self, input: &str) -> String { + match self { + Self::Lowercase => input.to_lowercase(), + Self::Uppercase => input.to_uppercase(), + Self::PascalCase => input.to_pascal_case(), + Self::CamelCase => input.to_camel_case(), + Self::SnakeCase => input.to_snake_case(), + Self::ScreamingSnakeCase => input.to_screaming_snake_case(), + Self::KebabCase => input.to_kebab_case(), + Self::ScreamingKebabCase => input.to_kebab_case().to_uppercase(), + } + } +} + +struct FunctionNameAttrs { + rename: Option, + rename_all: Option, +} + +fn parse_name_attrs( + attr: proc_macro::TokenStream, + function: &ItemFn, +) -> syn::Result { + let mut attrs = function.attrs.clone(); + let attr = proc_macro2::TokenStream::from(attr); + if !attr.is_empty() { + let specta_attr: syn::Attribute = syn::parse_quote!(#[specta(#attr)]); + attrs.push(specta_attr); + } + + let mut attrs = parse_attrs(&attrs)?; + + let rename = attrs + .extract("specta", "rename") + .map(|attr| attr.parse_string()) + .transpose()?; + + let rename_all = attrs + .extract("specta", "rename_all") + .or_else(|| attrs.extract("command", "rename_all")) + .map(|attr| RenameAllRule::parse(&attr.parse_string()?, attr.value_span())) + .transpose()?; + + Ok(FunctionNameAttrs { rename, rename_all }) +} + +pub fn attribute( + attr: proc_macro::TokenStream, + item: proc_macro::TokenStream, +) -> syn::Result { let crate_ref = quote!(specta); let function = parse::(item)?; + let name_attrs = parse_name_attrs(attr, &function)?; let wrapper = format_fn_wrapper(&function.sig.ident); // While using wasm_bindgen and Specta is rare, this should make the DX nicer. @@ -44,7 +125,13 @@ pub fn attribute(item: proc_macro::TokenStream) -> syn::Result syn::Result syn::Result Self { + pub(crate) fn forbidden_name_legacy(path: ExportPath, name: &'static str) -> Self { Self { kind: ErrorKind::ForbiddenNameLegacy(path, name), } diff --git a/tests/tests/functions.rs b/tests/tests/functions.rs index 0ff99f34..4086e563 100644 --- a/tests/tests/functions.rs +++ b/tests/tests/functions.rs @@ -93,6 +93,15 @@ mod nested { #[specta] fn raw(r#type: i32) {} +// https://github.com/specta-rs/specta/issues/213 +#[allow(non_snake_case)] +#[specta(rename_all = "snake_case")] +fn rename_all_fn(myArg: i32, anotherValue: String) {} + +#[allow(non_snake_case)] +#[specta(rename = "totally_custom")] +fn renamed_fn(myArg: i32) {} + // TODO: Finish fixing these #[test] @@ -431,4 +440,19 @@ fn test_function_exporting() { let def: Function = fn_datatype![raw](&mut types); insta::assert_snapshot!(def.args()[0].0, @"type"); } + + { + let mut types = TypeCollection::default(); + let def: Function = fn_datatype![rename_all_fn](&mut types); + insta::assert_snapshot!(def.name(), @"rename_all_fn"); + insta::assert_snapshot!(def.args()[0].0, @"my_arg"); + insta::assert_snapshot!(def.args()[1].0, @"another_value"); + } + + { + let mut types = TypeCollection::default(); + let def: Function = fn_datatype![renamed_fn](&mut types); + insta::assert_snapshot!(def.name(), @"totally_custom"); + insta::assert_snapshot!(def.args()[0].0, @"myArg"); + } } diff --git a/tests/tests/macro/compile_error.rs b/tests/tests/macro/compile_error.rs index 1911a5a7..76edc9a8 100644 --- a/tests/tests/macro/compile_error.rs +++ b/tests/tests/macro/compile_error.rs @@ -159,4 +159,7 @@ use wasm_bindgen::prelude::wasm_bindgen; #[specta] pub fn testing() {} +#[specta(rename_all = "camelCase123")] +pub fn invalid_function_rename_all() {} + // TODO: https://docs.rs/trybuild/latest/trybuild/#what-to-test diff --git a/tests/tests/macro/compile_error.stderr b/tests/tests/macro/compile_error.stderr index 7d481060..2e5d2e31 100644 --- a/tests/tests/macro/compile_error.stderr +++ b/tests/tests/macro/compile_error.stderr @@ -78,6 +78,12 @@ error: specta: You must apply the #[specta] macro before the #[wasm_bindgen] mac | = note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info) +error: specta: unsupported rename rule. Expected one of lowercase, UPPERCASE, PascalCase, camelCase, snake_case, SCREAMING_SNAKE_CASE, kebab-case, SCREAMING-KEBAB-CASE + --> tests/macro/compile_error.rs:162:23 + | +162 | #[specta(rename_all = "camelCase123")] + | ^^^^^^^^^^^^^^ + error[E0255]: the name `__specta__fn__testing` is defined multiple times --> tests/macro/compile_error.rs:160:8 | @@ -186,10 +192,10 @@ error: cannot find attribute `serde` in this scope = note: `serde` is in scope, but it is a crate, not an attribute error[E0601]: `main` function not found in crate `$CRATE` - --> tests/macro/compile_error.rs:160:20 + --> tests/macro/compile_error.rs:163:40 | -160 | pub fn testing() {} - | ^ consider adding a `main` function to `$DIR/tests/macro/compile_error.rs` +163 | pub fn invalid_function_rename_all() {} + | ^ consider adding a `main` function to `$DIR/tests/macro/compile_error.rs` error[E0277]: the trait `specta::Type` is not implemented for `dyn std::error::Error + Send + Sync` --> tests/macro/compile_error.rs:15:23