Skip to content

Commit 3634fb5

Browse files
XeniraNorbytus
authored andcommitted
feat(interface): add support for renaming all consts/methods
Refs: extphprs#533
1 parent cf3cf69 commit 3634fb5

File tree

10 files changed

+330
-134
lines changed

10 files changed

+330
-134
lines changed

crates/macros/src/class.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use darling::util::Flag;
22
use darling::{FromAttributes, FromMeta, ToTokens};
33
use proc_macro2::TokenStream;
4-
use quote::{quote, TokenStreamExt};
4+
use quote::{TokenStreamExt, quote};
55
use syn::{Attribute, Expr, Fields, ItemStruct};
66

77
use crate::helpers::get_docs;

crates/macros/src/interface.rs

Lines changed: 80 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,28 @@ use std::collections::{HashMap, HashSet};
33
use crate::class::ClassEntryAttribute;
44
use crate::constant::PhpConstAttribute;
55
use crate::function::{Args, Function};
6-
use crate::helpers::{get_docs, CleanPhpAttr};
7-
use darling::util::Flag;
6+
use crate::helpers::{CleanPhpAttr, get_docs};
87
use darling::FromAttributes;
8+
use darling::util::Flag;
99
use proc_macro2::TokenStream;
10-
use quote::{format_ident, quote, ToTokens};
10+
use quote::{ToTokens, format_ident, quote};
1111
use syn::{Expr, Ident, ItemTrait, Path, TraitItem, TraitItemConst, TraitItemFn};
1212

1313
use crate::impl_::{FnBuilder, MethodModifier};
1414
use crate::parsing::{PhpRename, RenameRule, Visibility};
1515
use crate::prelude::*;
1616

17+
const INTERNAL_INTERFACE_NAME_PREFIX: &str = "PhpInterface";
18+
1719
#[derive(FromAttributes, Debug, Default)]
1820
#[darling(attributes(php), forward_attrs(doc), default)]
19-
pub struct StructAttributes {
21+
pub struct TraitAttributes {
2022
#[darling(flatten)]
2123
rename: PhpRename,
24+
/// Rename methods to match the given rule.
25+
change_method_case: Option<RenameRule>,
26+
/// Rename constants to match the given rule.
27+
change_constant_case: Option<RenameRule>,
2228
#[darling(multiple)]
2329
extends: Vec<ClassEntryAttribute>,
2430
attrs: Vec<syn::Attribute>,
@@ -43,7 +49,7 @@ struct InterfaceData<'a> {
4349
ident: &'a Ident,
4450
name: String,
4551
path: Path,
46-
attrs: StructAttributes,
52+
extends: Vec<ClassEntryAttribute>,
4753
constructor: Option<Function<'a>>,
4854
methods: Vec<FnBuilder>,
4955
constants: Vec<Constant<'a>>,
@@ -53,9 +59,9 @@ struct InterfaceData<'a> {
5359
impl ToTokens for InterfaceData<'_> {
5460
#[allow(clippy::too_many_lines)]
5561
fn to_tokens(&self, tokens: &mut TokenStream) {
56-
let interface_name = format_ident!("PhpInterface{}", self.ident);
62+
let interface_name = format_ident!("{INTERNAL_INTERFACE_NAME_PREFIX}{}", self.ident);
5763
let name = &self.name;
58-
let implements = &self.attrs.extends;
64+
let implements = &self.extends;
5965
let methods_sig = &self.methods;
6066
let constants = &self.constants;
6167
let docs = &self.docs;
@@ -188,19 +194,19 @@ impl ToTokens for InterfaceData<'_> {
188194

189195
impl<'a> Parse<'a, InterfaceData<'a>> for ItemTrait {
190196
fn parse(&'a mut self) -> Result<InterfaceData<'a>> {
191-
let attrs = StructAttributes::from_attributes(&self.attrs)?;
197+
let attrs = TraitAttributes::from_attributes(&self.attrs)?;
192198
let ident = &self.ident;
193199
let name = attrs.rename.rename(ident.to_string(), RenameRule::Pascal);
194200
let docs = get_docs(&attrs.attrs)?;
195201
self.attrs.clean_php();
196-
let interface_name = format_ident!("PhpInterface{ident}");
202+
let interface_name = format_ident!("{INTERNAL_INTERFACE_NAME_PREFIX}{ident}");
197203
let ts = quote! { #interface_name };
198204
let path: Path = syn::parse2(ts)?;
199205
let mut data = InterfaceData {
200206
ident,
201207
name,
202208
path,
203-
attrs,
209+
extends: attrs.extends,
204210
constructor: None,
205211
methods: Vec::default(),
206212
constants: Vec::default(),
@@ -209,17 +215,17 @@ impl<'a> Parse<'a, InterfaceData<'a>> for ItemTrait {
209215

210216
for item in &mut self.items {
211217
match item {
212-
TraitItem::Fn(f) => {
213-
match f.parse()? {
214-
MethodKind::Method(builder) => data.methods.push(builder),
215-
MethodKind::Constructor(builder) => {
216-
if data.constructor.replace(builder).is_some() {
217-
bail!("Only one constructor can be provided per class.");
218-
}
218+
TraitItem::Fn(f) => match parse_trait_item_fn(f, attrs.change_method_case)? {
219+
MethodKind::Method(builder) => data.methods.push(builder),
220+
MethodKind::Constructor(builder) => {
221+
if data.constructor.replace(builder).is_some() {
222+
bail!("Only one constructor can be provided per class.");
219223
}
220-
};
221-
}
222-
TraitItem::Const(c) => data.constants.push(c.parse()?),
224+
}
225+
},
226+
TraitItem::Const(c) => data
227+
.constants
228+
.push(parse_trait_item_const(c, attrs.change_constant_case)?),
223229
_ => {}
224230
}
225231
}
@@ -247,63 +253,51 @@ enum MethodKind<'a> {
247253
Constructor(Function<'a>),
248254
}
249255

250-
impl<'a> Parse<'a, MethodKind<'a>> for TraitItemFn {
251-
fn parse(&'a mut self) -> Result<MethodKind<'a>> {
252-
if self.default.is_some() {
253-
bail!(self => "Interface could not have default impl");
254-
}
256+
fn parse_trait_item_fn(
257+
fn_item: &mut TraitItemFn,
258+
change_case: Option<RenameRule>,
259+
) -> Result<MethodKind<'_>> {
260+
if fn_item.default.is_some() {
261+
bail!(fn_item => "Interface an not have default impl");
262+
}
255263

256-
let php_attr = PhpFunctionInterfaceAttribute::from_attributes(&self.attrs)?;
257-
self.attrs.clean_php();
264+
let php_attr = PhpFunctionInterfaceAttribute::from_attributes(&fn_item.attrs)?;
265+
fn_item.attrs.clean_php();
258266

259-
let mut args = Args::parse_from_fnargs(self.sig.inputs.iter(), php_attr.defaults)?;
267+
let mut args = Args::parse_from_fnargs(fn_item.sig.inputs.iter(), php_attr.defaults)?;
260268

261-
let docs = get_docs(&php_attr.attrs)?;
269+
let docs = get_docs(&php_attr.attrs)?;
262270

263-
let mut modifiers: HashSet<MethodModifier> = HashSet::new();
264-
modifiers.insert(MethodModifier::Abstract);
271+
let mut modifiers: HashSet<MethodModifier> = HashSet::new();
272+
modifiers.insert(MethodModifier::Abstract);
265273

266-
if args.typed.first().is_some_and(|arg| arg.name == "self_") {
267-
args.typed.pop();
268-
} else if args.receiver.is_none() {
269-
modifiers.insert(MethodModifier::Static);
270-
}
271-
272-
let f = Function::new(
273-
&self.sig,
274-
php_attr
275-
.rename
276-
.rename(self.sig.ident.to_string(), RenameRule::Camel),
277-
args,
278-
php_attr.optional,
279-
docs,
280-
);
281-
282-
if php_attr.constructor.is_present() {
283-
Ok(MethodKind::Constructor(f))
284-
} else {
285-
let builder = FnBuilder {
286-
builder: f.abstract_function_builder(),
287-
vis: php_attr.vis.unwrap_or(Visibility::Public),
288-
modifiers,
289-
};
290-
291-
Ok(MethodKind::Method(builder))
292-
}
274+
if args.typed.first().is_some_and(|arg| arg.name == "self_") {
275+
args.typed.pop();
276+
} else if args.receiver.is_none() {
277+
modifiers.insert(MethodModifier::Static);
293278
}
294-
}
295279

296-
impl<'a> Parse<'a, Vec<MethodKind<'a>>> for ItemTrait {
297-
fn parse(&'a mut self) -> Result<Vec<MethodKind<'a>>> {
298-
Ok(self
299-
.items
300-
.iter_mut()
301-
.filter_map(|item| match item {
302-
TraitItem::Fn(f) => Some(f),
303-
_ => None,
304-
})
305-
.flat_map(Parse::parse)
306-
.collect())
280+
let f = Function::new(
281+
&fn_item.sig,
282+
php_attr.rename.rename(
283+
fn_item.sig.ident.to_string(),
284+
change_case.unwrap_or(RenameRule::Camel),
285+
),
286+
args,
287+
php_attr.optional,
288+
docs,
289+
);
290+
291+
if php_attr.constructor.is_present() {
292+
Ok(MethodKind::Constructor(f))
293+
} else {
294+
let builder = FnBuilder {
295+
builder: f.abstract_function_builder(),
296+
vis: php_attr.vis.unwrap_or(Visibility::Public),
297+
modifiers,
298+
};
299+
300+
Ok(MethodKind::Method(builder))
307301
}
308302
}
309303

@@ -332,32 +326,22 @@ impl<'a> Constant<'a> {
332326
}
333327
}
334328

335-
impl<'a> Parse<'a, Constant<'a>> for TraitItemConst {
336-
fn parse(&'a mut self) -> Result<Constant<'a>> {
337-
if self.default.is_none() {
338-
bail!(self => "Interface const could not be empty");
339-
}
340-
341-
let attr = PhpConstAttribute::from_attributes(&self.attrs)?;
342-
let name = self.ident.to_string();
343-
let docs = get_docs(&attr.attrs)?;
344-
self.attrs.clean_php();
345-
346-
let (_, expr) = self.default.as_ref().unwrap();
347-
Ok(Constant::new(name, expr, docs))
329+
fn parse_trait_item_const(
330+
const_item: &mut TraitItemConst,
331+
change_case: Option<RenameRule>,
332+
) -> Result<Constant<'_>> {
333+
if const_item.default.is_none() {
334+
bail!(const_item => "PHP Interface const can not be empty");
348335
}
349-
}
350336

351-
impl<'a> Parse<'a, Vec<Constant<'a>>> for ItemTrait {
352-
fn parse(&'a mut self) -> Result<Vec<Constant<'a>>> {
353-
Ok(self
354-
.items
355-
.iter_mut()
356-
.filter_map(|item| match item {
357-
TraitItem::Const(c) => Some(c),
358-
_ => None,
359-
})
360-
.flat_map(Parse::parse)
361-
.collect())
362-
}
337+
let attr = PhpConstAttribute::from_attributes(&const_item.attrs)?;
338+
let name = attr.rename.rename(
339+
const_item.ident.to_string(),
340+
change_case.unwrap_or(RenameRule::ScreamingSnake),
341+
);
342+
let docs = get_docs(&attr.attrs)?;
343+
const_item.attrs.clean_php();
344+
345+
let (_, expr) = const_item.default.as_ref().unwrap();
346+
Ok(Constant::new(name, expr, docs))
363347
}

crates/macros/src/lib.rs

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -332,44 +332,79 @@ fn php_enum_internal(_args: TokenStream2, input: TokenStream2) -> TokenStream2 {
332332
// BEGIN DOCS FROM interface.md
333333
/// # `#[php_interface]` Attribute
334334
///
335-
/// Traits can be exported to PHP as interface with the `#[php_interface]`
336-
/// attribute macro. This attribute generate empty struct and derives the
337-
/// `RegisteredClass`. To register the interface use the
338-
/// `interface::<PhpInterface{TraitName}>()` method on the `ModuleBuilder` in
339-
/// the `#[php_module]` macro.
335+
/// You can export a `Trait` block to PHP. This exports all methods as well as
336+
/// constants to PHP on the interface. Trait method SHOULD NOT contain default
337+
/// implementations, as these are not supported in PHP interfaces.
340338
///
341339
/// ## Options
342340
///
343-
/// The `#[php_interface]` attribute can be configured with the following
344-
/// options:
345-
/// - `#[php(name = "InterfaceName")]` or `#[php(change_case = snake_case)]`:
346-
/// Sets the name of the interface in PHP. The default is the `PascalCase`
347-
/// name of the interface.
348-
/// - `#[php(extends(ce = ce::throwable, stub = "\\Throwable"))]` to extends
349-
/// interface from other interface
341+
/// By default all constants are renamed to `UPPER_CASE` and all methods are
342+
/// renamed to `camelCase`. This can be changed by passing the
343+
/// `change_method_case` and `change_constant_case` as `#[php]` attributes on
344+
/// the `impl` block. The options are:
350345
///
351-
/// ### Example
346+
/// - `#[php(change_method_case = "snake_case")]` - Renames the method to snake
347+
/// case.
348+
/// - `#[php(change_constant_case = "snake_case")]` - Renames the constant to
349+
/// snake case.
350+
///
351+
/// See the [`name` and `change_case`](./php.md#name-and-change_case) section
352+
/// for a list of all available cases.
353+
///
354+
/// ## Methods
355+
///
356+
/// See the [`php_impl`](./impl.md#)
357+
///
358+
/// ## Constants
359+
///
360+
/// See the [`php_impl`](./impl.md#)
352361
///
353-
/// This example creates a PHP interface extend from php buildin Throwable.
362+
/// ## Example
363+
///
364+
/// Define an example trait with methods and constant:
354365
///
355366
/// ```rust,no_run,ignore
356367
/// # #![cfg_attr(windows, feature(abi_vectorcall))]
357368
/// # extern crate ext_php_rs;
358-
/// use ext_php_rs::prelude::*;
369+
/// use ext_php_rs::{prelude::*, types::ZendClassObject};
370+
///
359371
///
360372
/// #[php_interface]
361-
/// #[php(extends(ce = ce::throwable, stub = "\\Throwable"))]
362-
/// #[php(name = "LibName\\Exception\\MyCustomDomainException")]
363-
/// pub trait MyCustomDomainException {
364-
/// fn createWithMessage(message: String) -> Self;
373+
/// #[php(name = "Rust\\TestInterface")]
374+
/// trait Test {
375+
/// const TEST: &'static str = "TEST";
376+
///
377+
/// fn co();
378+
///
379+
/// #[php(defaults(value = 0))]
380+
/// fn set_value(&mut self, value: i32);
365381
/// }
366382
///
367383
/// #[php_module]
368-
/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
369-
/// module.interface::<PhpInterfaceMyCustomDomainException>()
384+
/// pub fn module(module: ModuleBuilder) -> ModuleBuilder {
385+
/// module
386+
/// .interface::<PhpInterfaceTest>()
370387
/// }
388+
///
371389
/// # fn main() {}
372390
/// ```
391+
///
392+
/// Using our newly created interface in PHP:
393+
///
394+
/// ```php
395+
/// <?php
396+
///
397+
/// assert(interface_exists("Rust\TestInterface"));
398+
///
399+
/// class B implements Rust\TestInterface {
400+
///
401+
/// public static function co() {}
402+
///
403+
/// public function setValue(?int $value = 0) {
404+
///
405+
/// }
406+
/// }
407+
/// ```
373408
// END DOCS FROM interface.md
374409
#[proc_macro_attribute]
375410
pub fn php_interface(args: TokenStream, input: TokenStream) -> TokenStream {
@@ -1286,7 +1321,6 @@ mod tests {
12861321
}
12871322

12881323
fn runtime_expand_attr(path: &PathBuf) {
1289-
dbg!(path);
12901324
let file = std::fs::File::open(path).expect("Failed to open expand test file");
12911325
runtime_macros::emulate_attributelike_macro_expansion(
12921326
file,

0 commit comments

Comments
 (0)