• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

davidcole1340 / ext-php-rs / 18244744117

04 Oct 2025 01:08PM UTC coverage: 30.759% (+3.0%) from 27.728%
18244744117

Pull #533

github

web-flow
Merge 0936605ac into 113ef33a0
Pull Request #533: Feat/interface impl

68 of 152 new or added lines in 9 files covered. (44.74%)

3 existing lines in 2 files now uncovered.

1326 of 4311 relevant lines covered (30.76%)

7.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

70.49
/crates/macros/src/interface.rs
1
use std::collections::{HashMap, HashSet};
2

3
use crate::class::ClassEntryAttribute;
4
use crate::constant::PhpConstAttribute;
5
use crate::function::{Args, Function};
6
use crate::helpers::{get_docs, CleanPhpAttr};
7
use darling::util::Flag;
8
use darling::FromAttributes;
9
use proc_macro2::TokenStream;
10
use quote::{format_ident, quote, ToTokens};
11
use syn::{Expr, Ident, ItemTrait, Path, TraitItem, TraitItemConst, TraitItemFn};
12

13
use crate::impl_::{FnBuilder, MethodModifier};
14
use crate::parsing::{PhpRename, RenameRule, Visibility};
15
use crate::prelude::*;
16

17
const INTERNAL_INTERFACE_NAME_PREFIX: &str = "PhpInterface";
18

19
#[derive(FromAttributes, Debug, Default)]
20
#[darling(attributes(php), forward_attrs(doc), default)]
21
pub struct TraitAttributes {
22
    #[darling(flatten)]
23
    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>,
28
    #[darling(multiple)]
29
    extends: Vec<ClassEntryAttribute>,
30
    attrs: Vec<syn::Attribute>,
31
}
32

33
pub fn parser(mut input: ItemTrait) -> Result<TokenStream> {
2✔
34
    let interface_data: InterfaceData = input.parse()?;
6✔
35
    let interface_tokens = quote! { #interface_data };
36

37
    Ok(quote! {
38
        #input
39

40
        #interface_tokens
41
    })
42
}
43

44
trait Parse<'a, T> {
45
    fn parse(&'a mut self) -> Result<T>;
46
}
47

48
struct InterfaceData<'a> {
49
    ident: &'a Ident,
50
    name: String,
51
    path: Path,
52
    extends: Vec<ClassEntryAttribute>,
53
    constructor: Option<Function<'a>>,
54
    methods: Vec<FnBuilder>,
55
    constants: Vec<Constant<'a>>,
56
    docs: Vec<String>,
57
}
58

59
impl ToTokens for InterfaceData<'_> {
60
    #[allow(clippy::too_many_lines)]
61
    fn to_tokens(&self, tokens: &mut TokenStream) {
2✔
62
        let interface_name = format_ident!("{INTERNAL_INTERFACE_NAME_PREFIX}{}", self.ident);
6✔
63
        let name = &self.name;
4✔
64
        let implements = &self.extends;
4✔
65
        let methods_sig = &self.methods;
4✔
66
        let constants = &self.constants;
4✔
67
        let docs = &self.docs;
4✔
68

69
        let _constructor = self
4✔
70
            .constructor
2✔
71
            .as_ref()
72
            .map(|func| func.constructor_meta(&self.path, Some(&Visibility::Public)))
2✔
73
            .option_tokens();
74

75
        quote! {
2✔
76
            pub struct #interface_name;
77

78
            impl ::ext_php_rs::class::RegisteredClass for #interface_name {
79
                const CLASS_NAME: &'static str = #name;
80

81
                const BUILDER_MODIFIER: Option<
82
                fn(::ext_php_rs::builders::ClassBuilder) -> ::ext_php_rs::builders::ClassBuilder,
83
                > = None;
84

85
                const EXTENDS: Option<::ext_php_rs::class::ClassEntryInfo> = None;
86

87
                const FLAGS: ::ext_php_rs::flags::ClassFlags = ::ext_php_rs::flags::ClassFlags::Interface;
88

89
                const IMPLEMENTS: &'static [::ext_php_rs::class::ClassEntryInfo] = &[
90
                    #(#implements,)*
91
                ];
92

93
                const DOC_COMMENTS: &'static [&'static str] = &[
94
                    #(#docs,)*
95
                ];
96

97
                fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata<Self> {
98
                    static METADATA: ::ext_php_rs::class::ClassMetadata<#interface_name> =
99
                    ::ext_php_rs::class::ClassMetadata::new();
100

101
                    &METADATA
102
                }
103

104
                fn method_builders() -> Vec<(
105
                    ::ext_php_rs::builders::FunctionBuilder<'static>,
106
                    ::ext_php_rs::flags::MethodFlags,
107
                )> {
108
                    vec![#(#methods_sig),*]
109
                }
110

111
                fn constructor() -> Option<::ext_php_rs::class::ConstructorMeta<Self>> {
112
                    None
113
                }
114

115
                fn constants() -> &'static [(
116
                    &'static str,
117
                    &'static dyn ext_php_rs::convert::IntoZvalDyn,
118
                    ext_php_rs::describe::DocComments,
119
                )] {
120
                    &[#(#constants),*]
121
                }
122

123
                fn get_properties<'a>() -> ::std::collections::HashMap<&'static str, ::ext_php_rs::internal::property::PropertyInfo<'a, Self>> {
124
                    panic!("Not supported for Interface");
125
                }
126
            }
127

128
            impl<'a> ::ext_php_rs::convert::FromZendObject<'a> for &'a #interface_name {
129
                #[inline]
130
                fn from_zend_object(
131
                    obj: &'a ::ext_php_rs::types::ZendObject,
132
                ) -> ::ext_php_rs::error::Result<Self> {
133
                    let obj = ::ext_php_rs::types::ZendClassObject::<#interface_name>::from_zend_obj(obj)
134
                        .ok_or(::ext_php_rs::error::Error::InvalidScope)?;
135
                    Ok(&**obj)
136
                }
137
            }
138
            impl<'a> ::ext_php_rs::convert::FromZendObjectMut<'a> for &'a mut #interface_name {
139
                #[inline]
140
                fn from_zend_object_mut(
141
                    obj: &'a mut ::ext_php_rs::types::ZendObject,
142
                ) -> ::ext_php_rs::error::Result<Self> {
143
                    let obj = ::ext_php_rs::types::ZendClassObject::<#interface_name>::from_zend_obj_mut(obj)
144
                        .ok_or(::ext_php_rs::error::Error::InvalidScope)?;
145
                    Ok(&mut **obj)
146
                }
147
            }
148
            impl<'a> ::ext_php_rs::convert::FromZval<'a> for &'a #interface_name {
149
                const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object(Some(
150
                    <#interface_name as ::ext_php_rs::class::RegisteredClass>::CLASS_NAME,
151
                ));
152
                #[inline]
153
                fn from_zval(zval: &'a ::ext_php_rs::types::Zval) -> ::std::option::Option<Self> {
154
                    <Self as ::ext_php_rs::convert::FromZendObject>::from_zend_object(zval.object()?).ok()
155
                }
156
            }
157
            impl<'a> ::ext_php_rs::convert::FromZvalMut<'a> for &'a mut #interface_name {
158
                const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object(Some(
159
                    <#interface_name as ::ext_php_rs::class::RegisteredClass>::CLASS_NAME,
160
                ));
161
                #[inline]
162
                fn from_zval_mut(zval: &'a mut ::ext_php_rs::types::Zval) -> ::std::option::Option<Self> {
163
                    <Self as ::ext_php_rs::convert::FromZendObjectMut>::from_zend_object_mut(zval.object_mut()?)
164
                        .ok()
165
                }
166
            }
167
            impl ::ext_php_rs::convert::IntoZendObject for #interface_name {
168
                #[inline]
169
                fn into_zend_object(
170
                    self,
171
                ) -> ::ext_php_rs::error::Result<::ext_php_rs::boxed::ZBox<::ext_php_rs::types::ZendObject>>
172
                {
173
                    Ok(::ext_php_rs::types::ZendClassObject::new(self).into())
174
                }
175
            }
176
            impl ::ext_php_rs::convert::IntoZval for #interface_name {
177
                const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object(Some(
178
                    <#interface_name as ::ext_php_rs::class::RegisteredClass>::CLASS_NAME,
179
                ));
180
                const NULLABLE: bool = false;
181
                #[inline]
182
                fn set_zval(
183
                    self,
184
                    zv: &mut ::ext_php_rs::types::Zval,
185
                    persistent: bool,
186
                ) -> ::ext_php_rs::error::Result<()> {
187
                    use ::ext_php_rs::convert::IntoZendObject;
188
                    self.into_zend_object()?.set_zval(zv, persistent)
189
                }
190
            }
191
        }.to_tokens(tokens);
192
    }
193
}
194

195
impl<'a> Parse<'a, InterfaceData<'a>> for ItemTrait {
196
    fn parse(&'a mut self) -> Result<InterfaceData<'a>> {
2✔
197
        let attrs = TraitAttributes::from_attributes(&self.attrs)?;
6✔
NEW
198
        let ident = &self.ident;
×
NEW
199
        let name = attrs.rename.rename(ident.to_string(), RenameRule::Pascal);
×
200
        let docs = get_docs(&attrs.attrs)?;
2✔
NEW
201
        self.attrs.clean_php();
×
NEW
202
        let interface_name = format_ident!("{INTERNAL_INTERFACE_NAME_PREFIX}{ident}");
×
NEW
203
        let ts = quote! { #interface_name };
×
204
        let path: Path = syn::parse2(ts)?;
2✔
205
        let mut data = InterfaceData {
206
            ident,
207
            name,
208
            path,
NEW
209
            extends: attrs.extends,
×
210
            constructor: None,
NEW
211
            methods: Vec::default(),
×
NEW
212
            constants: Vec::default(),
×
213
            docs,
214
        };
215

216
        for item in &mut self.items {
14✔
217
            match item {
6✔
218
                TraitItem::Fn(f) => {
3✔
219
                    match parse_trait_item_fn(f, attrs.change_method_case)? {
9✔
220
                        MethodKind::Method(builder) => data.methods.push(builder),
3✔
NEW
221
                        MethodKind::Constructor(builder) => {
×
NEW
222
                            if data.constructor.replace(builder).is_some() {
×
NEW
223
                                bail!("Only one constructor can be provided per class.");
×
224
                            }
225
                        }
226
                    };
227
                }
228
                TraitItem::Const(c) => data
3✔
NEW
229
                    .constants
×
230
                    .push(parse_trait_item_const(c, attrs.change_constant_case)?),
3✔
NEW
231
                _ => {}
×
232
            }
233
        }
234

235
        Ok(data)
2✔
236
    }
237
}
238

239
#[derive(FromAttributes, Default, Debug)]
240
#[darling(default, attributes(php), forward_attrs(doc))]
241
pub struct PhpFunctionInterfaceAttribute {
242
    #[darling(flatten)]
243
    rename: PhpRename,
244
    defaults: HashMap<Ident, Expr>,
245
    optional: Option<Ident>,
246
    vis: Option<Visibility>,
247
    attrs: Vec<syn::Attribute>,
248
    getter: Flag,
249
    setter: Flag,
250
    constructor: Flag,
251
}
252

253
enum MethodKind<'a> {
254
    Method(FnBuilder),
255
    Constructor(Function<'a>),
256
}
257

258
fn parse_trait_item_fn(
3✔
259
    fn_item: &mut TraitItemFn,
260
    change_case: Option<RenameRule>,
261
) -> Result<MethodKind<'_>> {
262
    if fn_item.default.is_some() {
6✔
NEW
263
        bail!(fn_item => "Interface an not have default impl");
×
264
    }
265

266
    let php_attr = PhpFunctionInterfaceAttribute::from_attributes(&fn_item.attrs)?;
3✔
267
    fn_item.attrs.clean_php();
268

269
    let mut args = Args::parse_from_fnargs(fn_item.sig.inputs.iter(), php_attr.defaults)?;
3✔
270

271
    let docs = get_docs(&php_attr.attrs)?;
3✔
272

273
    let mut modifiers: HashSet<MethodModifier> = HashSet::new();
274
    modifiers.insert(MethodModifier::Abstract);
275

276
    if args.typed.first().is_some_and(|arg| arg.name == "self_") {
4✔
NEW
277
        args.typed.pop();
×
278
    } else if args.receiver.is_none() {
3✔
NEW
279
        modifiers.insert(MethodModifier::Static);
×
280
    }
281

282
    let f = Function::new(
283
        &fn_item.sig,
284
        php_attr.rename.rename(
285
            fn_item.sig.ident.to_string(),
286
            change_case.unwrap_or(RenameRule::Camel),
287
        ),
288
        args,
289
        php_attr.optional,
290
        docs,
291
    );
292

293
    if php_attr.constructor.is_present() {
NEW
294
        Ok(MethodKind::Constructor(f))
×
295
    } else {
296
        let builder = FnBuilder {
297
            builder: f.abstract_function_builder(),
298
            vis: php_attr.vis.unwrap_or(Visibility::Public),
299
            modifiers,
300
        };
301

302
        Ok(MethodKind::Method(builder))
303
    }
304
}
305

306
#[derive(Debug)]
307
struct Constant<'a> {
308
    name: String,
309
    expr: &'a Expr,
310
    docs: Vec<String>,
311
}
312

313
impl ToTokens for Constant<'_> {
314
    fn to_tokens(&self, tokens: &mut TokenStream) {
3✔
315
        let name = &self.name;
6✔
316
        let expr = &self.expr;
6✔
317
        let docs = &self.docs;
6✔
318
        quote! {
3✔
319
            (#name, &#expr, &[#(#docs),*])
320
        }
321
        .to_tokens(tokens);
6✔
322
    }
323
}
324

325
impl<'a> Constant<'a> {
326
    fn new(name: String, expr: &'a Expr, docs: Vec<String>) -> Self {
3✔
327
        Self { name, expr, docs }
328
    }
329
}
330

331
fn parse_trait_item_const(
3✔
332
    const_item: &mut TraitItemConst,
333
    change_case: Option<RenameRule>,
334
) -> Result<Constant<'_>> {
335
    if const_item.default.is_none() {
6✔
NEW
336
        bail!(const_item => "PHP Interface const can not be empty");
×
337
    }
338

339
    let attr = PhpConstAttribute::from_attributes(&const_item.attrs)?;
3✔
340
    let name = attr.rename.rename(
341
        const_item.ident.to_string(),
342
        change_case.unwrap_or(RenameRule::ScreamingSnake),
343
    );
344
    let docs = get_docs(&attr.attrs)?;
3✔
345
    const_item.attrs.clean_php();
346

347
    let (_, expr) = const_item.default.as_ref().unwrap();
348
    Ok(Constant::new(name, expr, docs))
349
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc