diff --git a/ext-php-rs-derive/Cargo.toml b/ext-php-rs-derive/Cargo.toml index 7f06c17939..bbce18ac03 100644 --- a/ext-php-rs-derive/Cargo.toml +++ b/ext-php-rs-derive/Cargo.toml @@ -14,6 +14,7 @@ proc-macro = true [dependencies] syn = { version = "1.0.68", features = ["full", "extra-traits"] } darling = "0.12" +ident_case = "1.0.1" quote = "1.0.9" proc-macro2 = "1.0.26" lazy_static = "1.4.0" diff --git a/ext-php-rs-derive/src/impl_.rs b/ext-php-rs-derive/src/impl_.rs index b2afd5efa0..2223a9bcc7 100644 --- a/ext-php-rs-derive/src/impl_.rs +++ b/ext-php-rs-derive/src/impl_.rs @@ -4,7 +4,7 @@ use anyhow::{anyhow, bail, Result}; use darling::{FromMeta, ToTokens}; use proc_macro2::TokenStream; use quote::quote; -use syn::{Attribute, ItemImpl, Lit, Meta, NestedMeta}; +use syn::{Attribute, AttributeArgs, ItemImpl, Lit, Meta, NestedMeta}; use crate::{constant::Constant, method}; @@ -15,14 +15,77 @@ pub enum Visibility { Private, } +#[derive(Debug, Copy, Clone, FromMeta)] +pub enum RenameRule { + #[darling(rename = "none")] + None, + #[darling(rename = "camelCase")] + Camel, + #[darling(rename = "snake_case")] + Snake, +} + +impl Default for RenameRule { + fn default() -> Self { + RenameRule::Camel + } +} + +impl RenameRule { + /// Change case of an identifier. + /// + /// Magic methods are handled specially to make sure they're always cased + /// correctly. + pub fn rename(&self, name: impl AsRef) -> String { + let name = name.as_ref(); + match self { + RenameRule::None => name.to_string(), + rule => match name { + "__construct" => "__construct".to_string(), + "__destruct" => "__destruct".to_string(), + "__call" => "__call".to_string(), + "__call_static" => "__callStatic".to_string(), + "__get" => "__get".to_string(), + "__set" => "__set".to_string(), + "__isset" => "__isset".to_string(), + "__unset" => "__unset".to_string(), + "__sleep" => "__sleep".to_string(), + "__wakeup" => "__wakeup".to_string(), + "__serialize" => "__serialize".to_string(), + "__unserialize" => "__unserialize".to_string(), + "__to_string" => "__toString".to_string(), + "__invoke" => "__invoke".to_string(), + "__set_state" => "__set_state".to_string(), + "__clone" => "__clone".to_string(), + "__debug_info" => "__debugInfo".to_string(), + field => match rule { + Self::Camel => ident_case::RenameRule::CamelCase.apply_to_field(field), + Self::Snake => ident_case::RenameRule::SnakeCase.apply_to_field(field), + Self::None => unreachable!(), + }, + }, + } + } +} + #[derive(Debug)] pub enum ParsedAttribute { Default(HashMap), Optional(String), Visibility(Visibility), + Rename(String), +} + +#[derive(Default, Debug, FromMeta)] +#[darling(default)] +pub struct AttrArgs { + rename_methods: Option, } -pub fn parser(input: ItemImpl) -> Result { +pub fn parser(args: AttributeArgs, input: ItemImpl) -> Result { + let args = AttrArgs::from_list(&args) + .map_err(|e| anyhow!("Unable to parse attribute arguments: {:?}", e))?; + let ItemImpl { self_ty, items, .. } = input; let class_name = self_ty.to_token_stream().to_string(); @@ -61,7 +124,8 @@ pub fn parser(input: ItemImpl) -> Result { } } syn::ImplItem::Method(mut method) => { - let (sig, method) = method::parser(&mut method)?; + let (sig, method) = + method::parser(&mut method, args.rename_methods.unwrap_or_default())?; class.methods.push(method); sig } @@ -107,6 +171,61 @@ pub fn parse_attribute(attr: &Attribute) -> Result { "public" => ParsedAttribute::Visibility(Visibility::Public), "protected" => ParsedAttribute::Visibility(Visibility::Protected), "private" => ParsedAttribute::Visibility(Visibility::Private), + "rename" => { + let ident = if let Meta::List(list) = meta { + if let Some(NestedMeta::Lit(lit)) = list.nested.first() { + String::from_value(lit).ok() + } else { + None + } + } else { + None + } + .ok_or_else(|| anyhow!("Invalid argument given for `#[rename] macro."))?; + + ParsedAttribute::Rename(ident) + } attr => bail!("Invalid attribute `#[{}]`.", attr), }) } + +#[cfg(test)] +mod tests { + use super::RenameRule; + + #[test] + fn test_rename_magic() { + for &(magic, expected) in &[ + ("__construct", "__construct"), + ("__destruct", "__destruct"), + ("__call", "__call"), + ("__call_static", "__callStatic"), + ("__get", "__get"), + ("__set", "__set"), + ("__isset", "__isset"), + ("__unset", "__unset"), + ("__sleep", "__sleep"), + ("__wakeup", "__wakeup"), + ("__serialize", "__serialize"), + ("__unserialize", "__unserialize"), + ("__to_string", "__toString"), + ("__invoke", "__invoke"), + ("__set_state", "__set_state"), + ("__clone", "__clone"), + ("__debug_info", "__debugInfo"), + ] { + assert_eq!(magic, RenameRule::None.rename(magic)); + assert_eq!(expected, RenameRule::Camel.rename(magic)); + assert_eq!(expected, RenameRule::Snake.rename(magic)); + } + } + + #[test] + fn test_rename_php_methods() { + for &(original, camel, snake) in &[("get_name", "getName", "get_name")] { + assert_eq!(original, RenameRule::None.rename(original)); + assert_eq!(camel, RenameRule::Camel.rename(original)); + assert_eq!(snake, RenameRule::Snake.rename(original)); + } + } +} diff --git a/ext-php-rs-derive/src/lib.rs b/ext-php-rs-derive/src/lib.rs index 6378f5957d..f6e18a49e3 100644 --- a/ext-php-rs-derive/src/lib.rs +++ b/ext-php-rs-derive/src/lib.rs @@ -93,10 +93,11 @@ pub fn php_startup(_: TokenStream, input: TokenStream) -> TokenStream { } #[proc_macro_attribute] -pub fn php_impl(_: TokenStream, input: TokenStream) -> TokenStream { +pub fn php_impl(args: TokenStream, input: TokenStream) -> TokenStream { + let args = parse_macro_input!(args as AttributeArgs); let input = parse_macro_input!(input as ItemImpl); - match impl_::parser(input) { + match impl_::parser(args, input) { Ok(parsed) => parsed, Err(e) => syn::Error::new(Span::call_site(), e).to_compile_error(), } diff --git a/ext-php-rs-derive/src/method.rs b/ext-php-rs-derive/src/method.rs index b7f6e3acd1..d2e83d3448 100644 --- a/ext-php-rs-derive/src/method.rs +++ b/ext-php-rs-derive/src/method.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use crate::{ function, - impl_::{parse_attribute, ParsedAttribute, Visibility}, + impl_::{parse_attribute, ParsedAttribute, RenameRule, Visibility}, }; use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; @@ -34,16 +34,21 @@ pub struct Method { pub visibility: Visibility, } -pub fn parser(input: &mut ImplItemMethod) -> Result<(TokenStream, Method)> { +pub fn parser( + input: &mut ImplItemMethod, + rename_rule: RenameRule, +) -> Result<(TokenStream, Method)> { let mut defaults = HashMap::new(); let mut optional = None; let mut visibility = Visibility::Public; + let mut identifier = None; for attr in input.attrs.iter() { match parse_attribute(attr)? { ParsedAttribute::Default(list) => defaults = list, ParsedAttribute::Optional(name) => optional = Some(name), ParsedAttribute::Visibility(vis) => visibility = vis, + ParsedAttribute::Rename(ident) => identifier = Some(ident), } } @@ -92,8 +97,9 @@ pub fn parser(input: &mut ImplItemMethod) -> Result<(TokenStream, Method)> { } }; + let name = identifier.unwrap_or_else(|| rename_rule.rename(ident.to_string())); let method = Method { - name: ident.to_string(), + name, ident: internal_ident.to_string(), args, optional,