Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions specta-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
106 changes: 100 additions & 6 deletions specta-macros/src/specta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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#") {
Expand All @@ -16,9 +17,89 @@ fn unraw(s: &str) -> &str {
}
}

pub fn attribute(item: proc_macro::TokenStream) -> syn::Result<proc_macro::TokenStream> {
#[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<Self> {
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<String>,
rename_all: Option<RenameAllRule>,
}

fn parse_name_attrs(
attr: proc_macro::TokenStream,
function: &ItemFn,
) -> syn::Result<FunctionNameAttrs> {
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<proc_macro::TokenStream> {
let crate_ref = quote!(specta);
let function = parse::<ItemFn>(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.
Expand All @@ -44,7 +125,13 @@ pub fn attribute(item: proc_macro::TokenStream) -> syn::Result<proc_macro::Token
};

let function_name = &function.sig.ident;
let function_name_str = unraw(&function_name.to_string()).to_string();
let mut function_name_str = unraw(&function_name.to_string()).to_string();
if let Some(rule) = name_attrs.rename_all {
function_name_str = rule.apply(&function_name_str);
}
if let Some(rename) = name_attrs.rename {
function_name_str = rename;
}
let function_asyncness = function.sig.asyncness.is_some();

let mut arg_names = Vec::new();
Expand Down Expand Up @@ -79,9 +166,16 @@ pub fn attribute(item: proc_macro::TokenStream) -> syn::Result<proc_macro::Token
s
};

arg_names.push(TokenStream::from_str(&s).map_err(|err| {
let arg_name = TokenStream::from_str(&s).map_err(|err| {
syn::Error::new_spanned(input, format!("invalid token stream for argument: {err}"))
})?);
})?;

let mut arg_name_str = arg_name.to_string();
if let Some(rule) = name_attrs.rename_all {
arg_name_str = rule.apply(&arg_name_str);
}

arg_names.push(arg_name_str);
}

let arg_signatures = function.sig.inputs.iter().map(|_| quote!(_));
Expand Down Expand Up @@ -112,7 +206,7 @@ pub fn attribute(item: proc_macro::TokenStream) -> syn::Result<proc_macro::Token
#function_asyncness,
#function_name_str.into(),
types,
&[#(stringify!(#arg_names).into()),* ],
&[#(#arg_names.into()),* ],
std::borrow::Cow::Borrowed(#docs),
#deprecated,
#no_return_type,
Expand Down
5 changes: 1 addition & 4 deletions specta-typescript/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,7 @@ impl Error {
}
}

pub(crate) fn forbidden_name_legacy(
path: ExportPath,
name: &'static str,
) -> Self {
pub(crate) fn forbidden_name_legacy(path: ExportPath, name: &'static str) -> Self {
Self {
kind: ErrorKind::ForbiddenNameLegacy(path, name),
}
Expand Down
24 changes: 24 additions & 0 deletions tests/tests/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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");
}
}
3 changes: 3 additions & 0 deletions tests/tests/macro/compile_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 9 additions & 3 deletions tests/tests/macro/compile_error.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -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
|
Expand Down Expand Up @@ -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
Expand Down