Skip to content

Commit

Permalink
Expose methods as camelCase and add attributes for controlling renaming
Browse files Browse the repository at this point in the history
Add an attribute to methods to control the name they're exported under
and an attribute to php_impl to override automatic case conversion
conventions.
  • Loading branch information
vodik committed Sep 13, 2021
1 parent 418265a commit 717663e
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 66 deletions.
111 changes: 108 additions & 3 deletions ext-php-rs-derive/src/impl_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -15,14 +15,64 @@ pub enum Visibility {
Private,
}

#[derive(Debug, Copy, Clone, FromMeta)]
pub enum RenameRule {
#[darling(rename = "camelCase")]
Camel,
#[darling(rename = "snake_case")]
Snake,
}

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<str>) -> String {
match name.as_ref() {
"__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 self {
Self::Camel => ident_case::RenameRule::CamelCase.apply_to_field(field),
Self::Snake => ident_case::RenameRule::SnakeCase.apply_to_field(field),
},
}
}
}

#[derive(Debug)]
pub enum ParsedAttribute {
Default(HashMap<String, Lit>),
Optional(String),
Visibility(Visibility),
Rename(String),
}

pub fn parser(input: ItemImpl) -> Result<TokenStream> {
#[derive(Default, Debug, FromMeta)]
#[darling(default)]
pub struct AttrArgs {
rename_methods: Option<RenameRule>,
}

pub fn parser(args: AttributeArgs, input: ItemImpl) -> Result<TokenStream> {
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();

Expand Down Expand Up @@ -61,7 +111,7 @@ pub fn parser(input: ItemImpl) -> Result<TokenStream> {
}
}
syn::ImplItem::Method(mut method) => {
let (sig, method) = method::parser(&mut method)?;
let (sig, method) = method::parser(&mut method, args.rename_methods.unwrap_or(RenameRule::Camel))?;
class.methods.push(method);
sig
}
Expand Down Expand Up @@ -107,6 +157,61 @@ pub fn parse_attribute(attr: &Attribute) -> Result<ParsedAttribute> {
"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!(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!(camel, RenameRule::Camel.rename(original));
assert_eq!(snake, RenameRule::Snake.rename(original));
}
}
}
5 changes: 3 additions & 2 deletions ext-php-rs-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
Expand Down
67 changes: 6 additions & 61 deletions ext-php-rs-derive/src/method.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
use anyhow::{anyhow, bail, Result};
use ident_case::RenameRule;
use quote::ToTokens;
use std::collections::HashMap;

use crate::{
function,
impl_::{parse_attribute, ParsedAttribute, Visibility},
};
use crate::{function, impl_::{ParsedAttribute, RenameRule, Visibility, parse_attribute}};
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
use syn::{punctuated::Punctuated, FnArg, ImplItemMethod, Lit, Pat, Signature, Token, Type};
Expand Down Expand Up @@ -35,39 +31,18 @@ pub struct Method {
pub visibility: Visibility,
}

fn camel_case_identifier(field: &str) -> String {
match field {
"__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 => RenameRule::CamelCase.apply_to_field(field),
}
}

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),
}
}

Expand Down Expand Up @@ -116,8 +91,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: camel_case_identifier(&ident.to_string()),
name,
ident: internal_ident.to_string(),
args,
optional,
Expand Down Expand Up @@ -271,34 +247,3 @@ impl Method {
.to_token_stream()
}
}

#[cfg(test)]
mod tests {
use super::camel_case_identifier;

#[test]
fn test_rename_php_methods() {
for &(original, 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"),
("get_name", "getName"),
] {
assert_eq!(camel_case_identifier(original), expected);
}
}
}

0 comments on commit 717663e

Please sign in to comment.