Skip to main content

ext_php_rs/describe/
mod.rs

1//! Types used to describe downstream extensions. Used by the `cargo-php`
2//! CLI application to generate PHP stub files used by IDEs.
3use std::vec::Vec as StdVec;
4
5#[cfg(feature = "enum")]
6use crate::builders::EnumBuilder;
7use crate::{
8    builders::{ClassBuilder, FunctionBuilder},
9    constant::IntoConst,
10    flags::{DataType, MethodFlags, PropertyFlags},
11    prelude::ModuleBuilder,
12};
13use abi::{Option, RString, Str, Vec};
14
15pub mod abi;
16mod stub;
17
18pub use stub::ToStub;
19
20/// A slice of strings containing documentation comments.
21pub type DocComments = &'static [&'static str];
22
23/// Representation of the extension used to generate PHP stubs.
24#[repr(C)]
25pub struct Description {
26    /// Extension description.
27    pub module: Module,
28    /// ext-php-rs version.
29    pub version: &'static str,
30}
31
32impl Description {
33    /// Creates a new description.
34    ///
35    /// # Parameters
36    ///
37    /// * `module` - The extension module representation.
38    #[must_use]
39    pub fn new(module: Module) -> Self {
40        Self {
41            module,
42            version: crate::VERSION,
43        }
44    }
45}
46
47/// Represents a set of comments on an export.
48#[repr(C)]
49#[derive(Debug, PartialEq)]
50pub struct DocBlock(pub Vec<Str>);
51
52impl From<&'static [&'static str]> for DocBlock {
53    fn from(val: &'static [&'static str]) -> Self {
54        Self(
55            val.iter()
56                .map(|s| (*s).into())
57                .collect::<StdVec<_>>()
58                .into(),
59        )
60    }
61}
62
63/// Represents an extension containing a set of exports.
64#[repr(C)]
65pub struct Module {
66    /// Name of the extension.
67    pub name: RString,
68    /// Functions exported by the extension.
69    pub functions: Vec<Function>,
70    /// Classes exported by the extension.
71    pub classes: Vec<Class>,
72    #[cfg(feature = "enum")]
73    /// Enums exported by the extension.
74    pub enums: Vec<Enum>,
75    /// Constants exported by the extension.
76    pub constants: Vec<Constant>,
77}
78
79/// Builds a [`Module`] from a [`ModuleBuilder`].
80/// This is used to generate the PHP stubs for the module.
81impl From<ModuleBuilder<'_>> for Module {
82    fn from(builder: ModuleBuilder) -> Self {
83        let functions = builder.functions;
84
85        // Include both classes and interfaces in the classes list.
86        // Interfaces are distinguished by ClassFlags::Interface.
87        #[allow(unused_mut)]
88        let mut classes = builder
89            .interfaces
90            .into_iter()
91            .chain(builder.classes)
92            .map(|c| c().into())
93            .collect::<StdVec<_>>();
94
95        #[cfg(feature = "closure")]
96        classes.push(Class::closure());
97
98        Self {
99            name: builder.name.into(),
100            functions: functions
101                .into_iter()
102                .map(Function::from)
103                .collect::<StdVec<_>>()
104                .into(),
105            classes: classes.into(),
106            constants: builder
107                .constants
108                .into_iter()
109                .map(Constant::from)
110                .collect::<StdVec<_>>()
111                .into(),
112            #[cfg(feature = "enum")]
113            enums: builder
114                .enums
115                .into_iter()
116                .map(|e| e().into())
117                .collect::<StdVec<_>>()
118                .into(),
119        }
120    }
121}
122
123/// Represents an exported function.
124#[repr(C)]
125pub struct Function {
126    /// Name of the function.
127    pub name: RString,
128    /// Documentation comments for the function.
129    pub docs: DocBlock,
130    /// Return value of the function.
131    pub ret: Option<Retval>,
132    /// Parameters of the function.
133    pub params: Vec<Parameter>,
134}
135
136impl From<FunctionBuilder<'_>> for Function {
137    fn from(val: FunctionBuilder<'_>) -> Self {
138        let ret_allow_null = val.ret_as_null;
139        Function {
140            name: val.name.into(),
141            docs: DocBlock(
142                val.docs
143                    .iter()
144                    .map(|d| (*d).into())
145                    .collect::<StdVec<_>>()
146                    .into(),
147            ),
148            ret: val
149                .retval
150                .map(|r| Retval {
151                    ty: r,
152                    nullable: r != DataType::Mixed && ret_allow_null,
153                })
154                .into(),
155            params: val
156                .args
157                .into_iter()
158                .map(Parameter::from)
159                .collect::<StdVec<_>>()
160                .into(),
161        }
162    }
163}
164
165/// Represents a parameter attached to an exported function or method.
166#[repr(C)]
167#[derive(Debug, PartialEq)]
168pub struct Parameter {
169    /// Name of the parameter.
170    pub name: RString,
171    /// Type of the parameter.
172    pub ty: Option<DataType>,
173    /// Whether the parameter is nullable.
174    pub nullable: bool,
175    /// Whether the parameter is variadic.
176    pub variadic: bool,
177    /// Default value of the parameter.
178    pub default: Option<RString>,
179}
180
181/// Represents an exported class.
182#[repr(C)]
183pub struct Class {
184    /// Name of the class.
185    pub name: RString,
186    /// Documentation comments for the class.
187    pub docs: DocBlock,
188    /// Name of the class the exported class extends. (Not implemented #326)
189    pub extends: Option<RString>,
190    /// Names of the interfaces the exported class implements. (Not implemented
191    /// #326)
192    pub implements: Vec<RString>,
193    /// Properties of the class.
194    pub properties: Vec<Property>,
195    /// Methods of the class.
196    pub methods: Vec<Method>,
197    /// Constants of the class.
198    pub constants: Vec<Constant>,
199    /// Class flags
200    pub flags: u32,
201}
202
203#[cfg(feature = "closure")]
204impl Class {
205    /// Creates a new class representing a Rust closure used for generating
206    /// the stubs if the `closure` feature is enabled.
207    #[must_use]
208    pub fn closure() -> Self {
209        Self {
210            name: "RustClosure".into(),
211            docs: DocBlock(StdVec::new().into()),
212            extends: Option::None,
213            implements: StdVec::new().into(),
214            properties: StdVec::new().into(),
215            methods: vec![Method {
216                name: "__invoke".into(),
217                docs: DocBlock(StdVec::new().into()),
218                ty: MethodType::Member,
219                params: vec![Parameter {
220                    name: "args".into(),
221                    ty: Option::Some(DataType::Mixed),
222                    nullable: false,
223                    variadic: true,
224                    default: Option::None,
225                }]
226                .into(),
227                retval: Option::Some(Retval {
228                    ty: DataType::Mixed,
229                    nullable: false,
230                }),
231                r#static: false,
232                visibility: Visibility::Public,
233                r#abstract: false,
234            }]
235            .into(),
236            constants: StdVec::new().into(),
237            flags: 0,
238        }
239    }
240}
241
242impl From<ClassBuilder> for Class {
243    fn from(val: ClassBuilder) -> Self {
244        let flags = val.get_flags();
245        Self {
246            name: val.name.into(),
247            docs: DocBlock(
248                val.docs
249                    .iter()
250                    .map(|doc| (*doc).into())
251                    .collect::<StdVec<_>>()
252                    .into(),
253            ),
254            extends: val.extends.map(|(_, stub)| stub.into()).into(),
255            implements: val
256                .interfaces
257                .into_iter()
258                .map(|(_, stub)| stub.into())
259                .collect::<StdVec<_>>()
260                .into(),
261            properties: val
262                .properties
263                .into_iter()
264                .map(Property::from)
265                .collect::<StdVec<_>>()
266                .into(),
267            methods: val
268                .methods
269                .into_iter()
270                .map(Method::from)
271                .collect::<StdVec<_>>()
272                .into(),
273            constants: val
274                .constants
275                .into_iter()
276                .map(|(name, _, docs, stub)| Constant {
277                    name: name.into(),
278                    value: Option::Some(stub.into()),
279                    docs: docs.into(),
280                })
281                .collect::<StdVec<_>>()
282                .into(),
283            flags,
284        }
285    }
286}
287
288#[cfg(feature = "enum")]
289/// Represents an exported enum.
290#[repr(C)]
291#[derive(Debug, PartialEq)]
292pub struct Enum {
293    /// Name of the enum.
294    pub name: RString,
295    /// Documentation comments for the enum.
296    pub docs: DocBlock,
297    /// Cases of the enum.
298    pub cases: Vec<EnumCase>,
299    /// Backing type of the enum.
300    pub backing_type: Option<RString>,
301}
302
303#[cfg(feature = "enum")]
304impl From<EnumBuilder> for Enum {
305    fn from(val: EnumBuilder) -> Self {
306        Self {
307            name: val.name.into(),
308            docs: DocBlock(
309                val.docs
310                    .iter()
311                    .map(|d| (*d).into())
312                    .collect::<StdVec<_>>()
313                    .into(),
314            ),
315            cases: val
316                .cases
317                .into_iter()
318                .map(EnumCase::from)
319                .collect::<StdVec<_>>()
320                .into(),
321            backing_type: match val.datatype {
322                DataType::Long => Some("int".into()),
323                DataType::String => Some("string".into()),
324                _ => None,
325            }
326            .into(),
327        }
328    }
329}
330
331#[cfg(feature = "enum")]
332/// Represents a case in an exported enum.
333#[repr(C)]
334#[derive(Debug, PartialEq)]
335pub struct EnumCase {
336    /// Name of the enum case.
337    pub name: RString,
338    /// Documentation comments for the enum case.
339    pub docs: DocBlock,
340    /// Value of the enum case.
341    pub value: Option<RString>,
342}
343
344#[cfg(feature = "enum")]
345impl From<&'static crate::enum_::EnumCase> for EnumCase {
346    fn from(val: &'static crate::enum_::EnumCase) -> Self {
347        Self {
348            name: val.name.into(),
349            docs: DocBlock(
350                val.docs
351                    .iter()
352                    .map(|d| (*d).into())
353                    .collect::<StdVec<_>>()
354                    .into(),
355            ),
356            value: val
357                .discriminant
358                .as_ref()
359                .map(|v| match v {
360                    crate::enum_::Discriminant::Int(i) => i.to_string().into(),
361                    crate::enum_::Discriminant::String(s) => format!("'{s}'").into(),
362                })
363                .into(),
364        }
365    }
366}
367
368/// Represents a property attached to an exported class.
369#[repr(C)]
370#[derive(Debug, PartialEq)]
371pub struct Property {
372    /// Name of the property.
373    pub name: RString,
374    /// Documentation comments for the property.
375    pub docs: DocBlock,
376    /// Type of the property.
377    pub ty: Option<DataType>,
378    /// Visibility of the property.
379    pub vis: Visibility,
380    /// Whether the property is static.
381    pub static_: bool,
382    /// Whether the property is nullable.
383    pub nullable: bool,
384    /// Whether the property is readonly.
385    pub readonly: bool,
386    /// Default value of the property as a PHP stub string.
387    pub default: Option<RString>,
388}
389
390impl From<crate::builders::ClassProperty> for Property {
391    fn from(val: crate::builders::ClassProperty) -> Self {
392        let static_ = val.flags.contains(PropertyFlags::Static);
393        let vis = Visibility::from(val.flags);
394        let docs = val.docs.into();
395
396        Self {
397            name: val.name.into(),
398            docs,
399            ty: val.ty.into(),
400            vis,
401            static_,
402            nullable: val.nullable,
403            readonly: val.readonly,
404            default: val.default_stub.map(RString::from).into(),
405        }
406    }
407}
408
409/// Represents a method attached to an exported class.
410#[repr(C)]
411#[derive(Debug, PartialEq)]
412pub struct Method {
413    /// Name of the method.
414    pub name: RString,
415    /// Documentation comments for the method.
416    pub docs: DocBlock,
417    /// Type of the method.
418    pub ty: MethodType,
419    /// Parameters of the method.
420    pub params: Vec<Parameter>,
421    /// Return value of the method.
422    pub retval: Option<Retval>,
423    /// Whether the method is static.
424    pub r#static: bool,
425    /// Visibility of the method.
426    pub visibility: Visibility,
427    /// Not describe method body, if is abstract.
428    pub r#abstract: bool,
429}
430
431impl From<(FunctionBuilder<'_>, MethodFlags)> for Method {
432    fn from(val: (FunctionBuilder<'_>, MethodFlags)) -> Self {
433        let (builder, flags) = val;
434        let ret_allow_null = builder.ret_as_null;
435        Method {
436            name: builder.name.into(),
437            docs: DocBlock(
438                builder
439                    .docs
440                    .iter()
441                    .map(|d| (*d).into())
442                    .collect::<StdVec<_>>()
443                    .into(),
444            ),
445            retval: builder
446                .retval
447                .map(|r| Retval {
448                    ty: r,
449                    nullable: r != DataType::Mixed && ret_allow_null,
450                })
451                .into(),
452            params: builder
453                .args
454                .into_iter()
455                .map(Into::into)
456                .collect::<StdVec<_>>()
457                .into(),
458            ty: flags.into(),
459            r#static: flags.contains(MethodFlags::Static),
460            visibility: flags.into(),
461            r#abstract: flags.contains(MethodFlags::Abstract),
462        }
463    }
464}
465
466/// Represents a value returned from a function or method.
467#[repr(C)]
468#[derive(Debug, PartialEq)]
469pub struct Retval {
470    /// Type of the return value.
471    pub ty: DataType,
472    /// Whether the return value is nullable.
473    pub nullable: bool,
474}
475
476/// Enumerator used to differentiate between methods.
477#[repr(C)]
478#[derive(Clone, Copy, Debug, PartialEq)]
479pub enum MethodType {
480    /// A member method.
481    Member,
482    /// A static method.
483    Static,
484    /// A constructor.
485    Constructor,
486}
487
488impl From<MethodFlags> for MethodType {
489    fn from(value: MethodFlags) -> Self {
490        if value.contains(MethodFlags::IsConstructor) {
491            return Self::Constructor;
492        }
493        if value.contains(MethodFlags::Static) {
494            return Self::Static;
495        }
496
497        Self::Member
498    }
499}
500
501/// Enumerator used to differentiate between different method and property
502/// visibilties.
503#[repr(C)]
504#[derive(Clone, Copy, Debug, PartialEq)]
505pub enum Visibility {
506    /// Private visibility.
507    Private,
508    /// Protected visibility.
509    Protected,
510    /// Public visibility.
511    Public,
512}
513
514impl From<PropertyFlags> for Visibility {
515    fn from(value: PropertyFlags) -> Self {
516        if value.contains(PropertyFlags::Protected) {
517            return Self::Protected;
518        }
519        if value.contains(PropertyFlags::Private) {
520            return Self::Private;
521        }
522
523        Self::Public
524    }
525}
526
527impl From<MethodFlags> for Visibility {
528    fn from(value: MethodFlags) -> Self {
529        if value.contains(MethodFlags::Protected) {
530            return Self::Protected;
531        }
532
533        if value.contains(MethodFlags::Private) {
534            return Self::Private;
535        }
536
537        Self::Public
538    }
539}
540
541/// Represents an exported constant, stand alone or attached to a class.
542#[repr(C)]
543pub struct Constant {
544    /// Name of the constant.
545    pub name: RString,
546    /// Documentation comments for the constant.
547    pub docs: DocBlock,
548    /// Value of the constant.
549    pub value: Option<RString>,
550}
551
552impl From<(String, DocComments)> for Constant {
553    fn from(val: (String, DocComments)) -> Self {
554        let (name, docs) = val;
555        Constant {
556            name: name.into(),
557            value: Option::None,
558            docs: docs.into(),
559        }
560    }
561}
562
563impl From<(String, Box<dyn IntoConst + Send>, DocComments)> for Constant {
564    fn from(val: (String, Box<dyn IntoConst + Send + 'static>, DocComments)) -> Self {
565        let (name, value, docs) = val;
566        Constant {
567            name: name.into(),
568            value: Option::Some(value.stub_value().into()),
569            docs: docs.into(),
570        }
571    }
572}
573
574#[cfg(test)]
575mod tests {
576    #![cfg_attr(windows, feature(abi_vectorcall))]
577    use cfg_if::cfg_if;
578
579    use super::*;
580
581    use crate::{args::Arg, test::test_function};
582
583    #[test]
584    fn test_new_description() {
585        let module = Module {
586            name: "test".into(),
587            functions: vec![].into(),
588            classes: vec![].into(),
589            constants: vec![].into(),
590            #[cfg(feature = "enum")]
591            enums: vec![].into(),
592        };
593
594        let description = Description::new(module);
595        assert_eq!(description.version, crate::VERSION);
596        assert_eq!(description.module.name, "test".into());
597    }
598
599    #[test]
600    fn test_doc_block_from() {
601        let docs: &'static [&'static str] = &["doc1", "doc2"];
602        let docs: DocBlock = docs.into();
603        assert_eq!(docs.0.len(), 2);
604        assert_eq!(docs.0[0], "doc1".into());
605        assert_eq!(docs.0[1], "doc2".into());
606    }
607
608    #[test]
609    fn test_module_from() {
610        let builder = ModuleBuilder::new("test", "test_version")
611            .function(FunctionBuilder::new("test_function", test_function));
612        let module: Module = builder.into();
613        assert_eq!(module.name, "test".into());
614        assert_eq!(module.functions.len(), 1);
615        cfg_if! {
616            if #[cfg(feature = "closure")] {
617                assert_eq!(module.classes.len(), 1);
618            } else {
619                assert_eq!(module.classes.len(), 0);
620            }
621        }
622        assert_eq!(module.constants.len(), 0);
623    }
624
625    #[test]
626    fn test_function_from() {
627        let builder = FunctionBuilder::new("test_function", test_function)
628            .docs(&["doc1", "doc2"])
629            .arg(Arg::new("foo", DataType::Long))
630            .returns(DataType::Bool, true, true);
631        let function: Function = builder.into();
632        assert_eq!(function.name, "test_function".into());
633        assert_eq!(function.docs.0.len(), 2);
634        assert_eq!(
635            function.params,
636            vec![Parameter {
637                name: "foo".into(),
638                ty: Option::Some(DataType::Long),
639                nullable: false,
640                variadic: false,
641                default: Option::None,
642            }]
643            .into()
644        );
645        assert_eq!(
646            function.ret,
647            Option::Some(Retval {
648                ty: DataType::Bool,
649                nullable: true,
650            })
651        );
652    }
653
654    #[test]
655    fn test_class_from() {
656        let builder = ClassBuilder::new("TestClass")
657            .docs(&["doc1", "doc2"])
658            .extends((|| todo!(), "BaseClass"))
659            .implements((|| todo!(), "Interface1"))
660            .implements((|| todo!(), "Interface2"))
661            .property(crate::builders::ClassProperty {
662                name: "prop1".into(),
663                flags: PropertyFlags::Public,
664                default: None,
665                docs: &["doc1"],
666                ty: None,
667                nullable: false,
668                readonly: false,
669                default_stub: None,
670            })
671            .method(
672                FunctionBuilder::new("test_function", test_function),
673                MethodFlags::Protected,
674            );
675        let class: Class = builder.into();
676
677        assert_eq!(class.name, "TestClass".into());
678        assert_eq!(class.docs.0.len(), 2);
679        assert_eq!(class.extends, Option::Some("BaseClass".into()));
680        assert_eq!(
681            class.implements,
682            vec!["Interface1".into(), "Interface2".into()].into()
683        );
684        assert_eq!(class.properties.len(), 1);
685        assert_eq!(
686            class.properties[0],
687            Property {
688                name: "prop1".into(),
689                docs: DocBlock(vec!["doc1".into()].into()),
690                ty: Option::None,
691                vis: Visibility::Public,
692                static_: false,
693                nullable: false,
694                readonly: false,
695                default: Option::None,
696            }
697        );
698        assert_eq!(class.methods.len(), 1);
699        assert_eq!(
700            class.methods[0],
701            Method {
702                name: "test_function".into(),
703                docs: DocBlock(vec![].into()),
704                ty: MethodType::Member,
705                params: vec![].into(),
706                retval: Option::None,
707                r#static: false,
708                visibility: Visibility::Protected,
709                r#abstract: false
710            }
711        );
712    }
713
714    #[test]
715    fn test_property_from() {
716        let property: Property = crate::builders::ClassProperty {
717            name: "test_property".into(),
718            flags: PropertyFlags::Protected,
719            default: None,
720            docs: &["doc1", "doc2"],
721            ty: Some(DataType::String),
722            nullable: true,
723            readonly: false,
724            default_stub: Some("null".into()),
725        }
726        .into();
727        assert_eq!(property.name, "test_property".into());
728        assert_eq!(property.docs.0.len(), 2);
729        assert_eq!(property.vis, Visibility::Protected);
730        assert!(!property.static_);
731        assert!(property.nullable);
732        assert_eq!(property.default, Option::Some("null".into()));
733        assert_eq!(property.ty, Option::Some(DataType::String));
734    }
735
736    #[test]
737    fn test_method_from() {
738        let builder = FunctionBuilder::new("test_method", test_function)
739            .docs(&["doc1", "doc2"])
740            .arg(Arg::new("foo", DataType::Long))
741            .returns(DataType::Bool, true, true);
742        let method: Method = (builder, MethodFlags::Static | MethodFlags::Protected).into();
743        assert_eq!(method.name, "test_method".into());
744        assert_eq!(method.docs.0.len(), 2);
745        assert_eq!(
746            method.params,
747            vec![Parameter {
748                name: "foo".into(),
749                ty: Option::Some(DataType::Long),
750                nullable: false,
751                variadic: false,
752                default: Option::None,
753            }]
754            .into()
755        );
756        assert_eq!(
757            method.retval,
758            Option::Some(Retval {
759                ty: DataType::Bool,
760                nullable: true,
761            })
762        );
763        assert!(method.r#static);
764        assert_eq!(method.visibility, Visibility::Protected);
765        assert_eq!(method.ty, MethodType::Static);
766    }
767
768    #[test]
769    fn test_ty_from() {
770        let r#static: MethodType = MethodFlags::Static.into();
771        assert_eq!(r#static, MethodType::Static);
772
773        let constructor: MethodType = MethodFlags::IsConstructor.into();
774        assert_eq!(constructor, MethodType::Constructor);
775
776        let member: MethodType = MethodFlags::Public.into();
777        assert_eq!(member, MethodType::Member);
778
779        let mixed: MethodType = (MethodFlags::Protected | MethodFlags::Static).into();
780        assert_eq!(mixed, MethodType::Static);
781
782        let both: MethodType = (MethodFlags::Static | MethodFlags::IsConstructor).into();
783        assert_eq!(both, MethodType::Constructor);
784
785        let empty: MethodType = MethodFlags::empty().into();
786        assert_eq!(empty, MethodType::Member);
787    }
788
789    #[test]
790    fn test_prop_visibility_from() {
791        let private: Visibility = PropertyFlags::Private.into();
792        assert_eq!(private, Visibility::Private);
793
794        let protected: Visibility = PropertyFlags::Protected.into();
795        assert_eq!(protected, Visibility::Protected);
796
797        let public: Visibility = PropertyFlags::Public.into();
798        assert_eq!(public, Visibility::Public);
799
800        let mixed: Visibility = (PropertyFlags::Protected | PropertyFlags::Static).into();
801        assert_eq!(mixed, Visibility::Protected);
802
803        let empty: Visibility = PropertyFlags::empty().into();
804        assert_eq!(empty, Visibility::Public);
805    }
806
807    #[test]
808    fn test_method_visibility_from() {
809        let private: Visibility = MethodFlags::Private.into();
810        assert_eq!(private, Visibility::Private);
811
812        let protected: Visibility = MethodFlags::Protected.into();
813        assert_eq!(protected, Visibility::Protected);
814
815        let public: Visibility = MethodFlags::Public.into();
816        assert_eq!(public, Visibility::Public);
817
818        let mixed: Visibility = (MethodFlags::Protected | MethodFlags::Static).into();
819        assert_eq!(mixed, Visibility::Protected);
820
821        let empty: Visibility = MethodFlags::empty().into();
822        assert_eq!(empty, Visibility::Public);
823    }
824}