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 (Not implemented #376)
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. (Not implemented #376)
383    pub nullable: bool,
384    /// Default value of the property. (Not implemented #376)
385    pub default: Option<RString>,
386}
387
388impl<D> From<(String, PropertyFlags, D, DocComments)> for Property {
389    fn from(value: (String, PropertyFlags, D, DocComments)) -> Self {
390        let (name, flags, _default, docs) = value;
391        let static_ = flags.contains(PropertyFlags::Static);
392        let vis = Visibility::from(flags);
393        // TODO: Implement ty #376
394        let ty = Option::None;
395        // TODO: Implement default #376
396        let default = Option::<RString>::None;
397        // TODO: Implement nullable #376
398        let nullable = false;
399        let docs = docs.into();
400
401        Self {
402            name: name.into(),
403            docs,
404            ty,
405            vis,
406            static_,
407            nullable,
408            default,
409        }
410    }
411}
412
413/// Represents a method attached to an exported class.
414#[repr(C)]
415#[derive(Debug, PartialEq)]
416pub struct Method {
417    /// Name of the method.
418    pub name: RString,
419    /// Documentation comments for the method.
420    pub docs: DocBlock,
421    /// Type of the method.
422    pub ty: MethodType,
423    /// Parameters of the method.
424    pub params: Vec<Parameter>,
425    /// Return value of the method.
426    pub retval: Option<Retval>,
427    /// Whether the method is static.
428    pub r#static: bool,
429    /// Visibility of the method.
430    pub visibility: Visibility,
431    /// Not describe method body, if is abstract.
432    pub r#abstract: bool,
433}
434
435impl From<(FunctionBuilder<'_>, MethodFlags)> for Method {
436    fn from(val: (FunctionBuilder<'_>, MethodFlags)) -> Self {
437        let (builder, flags) = val;
438        let ret_allow_null = builder.ret_as_null;
439        Method {
440            name: builder.name.into(),
441            docs: DocBlock(
442                builder
443                    .docs
444                    .iter()
445                    .map(|d| (*d).into())
446                    .collect::<StdVec<_>>()
447                    .into(),
448            ),
449            retval: builder
450                .retval
451                .map(|r| Retval {
452                    ty: r,
453                    nullable: r != DataType::Mixed && ret_allow_null,
454                })
455                .into(),
456            params: builder
457                .args
458                .into_iter()
459                .map(Into::into)
460                .collect::<StdVec<_>>()
461                .into(),
462            ty: flags.into(),
463            r#static: flags.contains(MethodFlags::Static),
464            visibility: flags.into(),
465            r#abstract: flags.contains(MethodFlags::Abstract),
466        }
467    }
468}
469
470/// Represents a value returned from a function or method.
471#[repr(C)]
472#[derive(Debug, PartialEq)]
473pub struct Retval {
474    /// Type of the return value.
475    pub ty: DataType,
476    /// Whether the return value is nullable.
477    pub nullable: bool,
478}
479
480/// Enumerator used to differentiate between methods.
481#[repr(C)]
482#[derive(Clone, Copy, Debug, PartialEq)]
483pub enum MethodType {
484    /// A member method.
485    Member,
486    /// A static method.
487    Static,
488    /// A constructor.
489    Constructor,
490}
491
492impl From<MethodFlags> for MethodType {
493    fn from(value: MethodFlags) -> Self {
494        if value.contains(MethodFlags::IsConstructor) {
495            return Self::Constructor;
496        }
497        if value.contains(MethodFlags::Static) {
498            return Self::Static;
499        }
500
501        Self::Member
502    }
503}
504
505/// Enumerator used to differentiate between different method and property
506/// visibilties.
507#[repr(C)]
508#[derive(Clone, Copy, Debug, PartialEq)]
509pub enum Visibility {
510    /// Private visibility.
511    Private,
512    /// Protected visibility.
513    Protected,
514    /// Public visibility.
515    Public,
516}
517
518impl From<PropertyFlags> for Visibility {
519    fn from(value: PropertyFlags) -> Self {
520        if value.contains(PropertyFlags::Protected) {
521            return Self::Protected;
522        }
523        if value.contains(PropertyFlags::Private) {
524            return Self::Private;
525        }
526
527        Self::Public
528    }
529}
530
531impl From<MethodFlags> for Visibility {
532    fn from(value: MethodFlags) -> Self {
533        if value.contains(MethodFlags::Protected) {
534            return Self::Protected;
535        }
536
537        if value.contains(MethodFlags::Private) {
538            return Self::Private;
539        }
540
541        Self::Public
542    }
543}
544
545/// Represents an exported constant, stand alone or attached to a class.
546#[repr(C)]
547pub struct Constant {
548    /// Name of the constant.
549    pub name: RString,
550    /// Documentation comments for the constant.
551    pub docs: DocBlock,
552    /// Value of the constant.
553    pub value: Option<RString>,
554}
555
556impl From<(String, DocComments)> for Constant {
557    fn from(val: (String, DocComments)) -> Self {
558        let (name, docs) = val;
559        Constant {
560            name: name.into(),
561            value: Option::None,
562            docs: docs.into(),
563        }
564    }
565}
566
567impl From<(String, Box<dyn IntoConst + Send>, DocComments)> for Constant {
568    fn from(val: (String, Box<dyn IntoConst + Send + 'static>, DocComments)) -> Self {
569        let (name, value, docs) = val;
570        Constant {
571            name: name.into(),
572            value: Option::Some(value.stub_value().into()),
573            docs: docs.into(),
574        }
575    }
576}
577
578#[cfg(test)]
579mod tests {
580    #![cfg_attr(windows, feature(abi_vectorcall))]
581    use cfg_if::cfg_if;
582
583    use super::*;
584
585    use crate::{args::Arg, test::test_function};
586
587    #[test]
588    fn test_new_description() {
589        let module = Module {
590            name: "test".into(),
591            functions: vec![].into(),
592            classes: vec![].into(),
593            constants: vec![].into(),
594            #[cfg(feature = "enum")]
595            enums: vec![].into(),
596        };
597
598        let description = Description::new(module);
599        assert_eq!(description.version, crate::VERSION);
600        assert_eq!(description.module.name, "test".into());
601    }
602
603    #[test]
604    fn test_doc_block_from() {
605        let docs: &'static [&'static str] = &["doc1", "doc2"];
606        let docs: DocBlock = docs.into();
607        assert_eq!(docs.0.len(), 2);
608        assert_eq!(docs.0[0], "doc1".into());
609        assert_eq!(docs.0[1], "doc2".into());
610    }
611
612    #[test]
613    fn test_module_from() {
614        let builder = ModuleBuilder::new("test", "test_version")
615            .function(FunctionBuilder::new("test_function", test_function));
616        let module: Module = builder.into();
617        assert_eq!(module.name, "test".into());
618        assert_eq!(module.functions.len(), 1);
619        cfg_if! {
620            if #[cfg(feature = "closure")] {
621                assert_eq!(module.classes.len(), 1);
622            } else {
623                assert_eq!(module.classes.len(), 0);
624            }
625        }
626        assert_eq!(module.constants.len(), 0);
627    }
628
629    #[test]
630    fn test_function_from() {
631        let builder = FunctionBuilder::new("test_function", test_function)
632            .docs(&["doc1", "doc2"])
633            .arg(Arg::new("foo", DataType::Long))
634            .returns(DataType::Bool, true, true);
635        let function: Function = builder.into();
636        assert_eq!(function.name, "test_function".into());
637        assert_eq!(function.docs.0.len(), 2);
638        assert_eq!(
639            function.params,
640            vec![Parameter {
641                name: "foo".into(),
642                ty: Option::Some(DataType::Long),
643                nullable: false,
644                variadic: false,
645                default: Option::None,
646            }]
647            .into()
648        );
649        assert_eq!(
650            function.ret,
651            Option::Some(Retval {
652                ty: DataType::Bool,
653                nullable: true,
654            })
655        );
656    }
657
658    #[test]
659    fn test_class_from() {
660        let builder = ClassBuilder::new("TestClass")
661            .docs(&["doc1", "doc2"])
662            .extends((|| todo!(), "BaseClass"))
663            .implements((|| todo!(), "Interface1"))
664            .implements((|| todo!(), "Interface2"))
665            .property("prop1", PropertyFlags::Public, None, &["doc1"])
666            .method(
667                FunctionBuilder::new("test_function", test_function),
668                MethodFlags::Protected,
669            );
670        let class: Class = builder.into();
671
672        assert_eq!(class.name, "TestClass".into());
673        assert_eq!(class.docs.0.len(), 2);
674        assert_eq!(class.extends, Option::Some("BaseClass".into()));
675        assert_eq!(
676            class.implements,
677            vec!["Interface1".into(), "Interface2".into()].into()
678        );
679        assert_eq!(class.properties.len(), 1);
680        assert_eq!(
681            class.properties[0],
682            Property {
683                name: "prop1".into(),
684                docs: DocBlock(vec!["doc1".into()].into()),
685                ty: Option::None,
686                vis: Visibility::Public,
687                static_: false,
688                nullable: false,
689                default: Option::None,
690            }
691        );
692        assert_eq!(class.methods.len(), 1);
693        assert_eq!(
694            class.methods[0],
695            Method {
696                name: "test_function".into(),
697                docs: DocBlock(vec![].into()),
698                ty: MethodType::Member,
699                params: vec![].into(),
700                retval: Option::None,
701                r#static: false,
702                visibility: Visibility::Protected,
703                r#abstract: false
704            }
705        );
706    }
707
708    #[test]
709    fn test_property_from() {
710        let docs: &'static [&'static str] = &["doc1", "doc2"];
711        let property: Property = (
712            "test_property".to_string(),
713            PropertyFlags::Protected,
714            (),
715            docs,
716        )
717            .into();
718        assert_eq!(property.name, "test_property".into());
719        assert_eq!(property.docs.0.len(), 2);
720        assert_eq!(property.vis, Visibility::Protected);
721        assert!(!property.static_);
722        assert!(!property.nullable);
723    }
724
725    #[test]
726    fn test_method_from() {
727        let builder = FunctionBuilder::new("test_method", test_function)
728            .docs(&["doc1", "doc2"])
729            .arg(Arg::new("foo", DataType::Long))
730            .returns(DataType::Bool, true, true);
731        let method: Method = (builder, MethodFlags::Static | MethodFlags::Protected).into();
732        assert_eq!(method.name, "test_method".into());
733        assert_eq!(method.docs.0.len(), 2);
734        assert_eq!(
735            method.params,
736            vec![Parameter {
737                name: "foo".into(),
738                ty: Option::Some(DataType::Long),
739                nullable: false,
740                variadic: false,
741                default: Option::None,
742            }]
743            .into()
744        );
745        assert_eq!(
746            method.retval,
747            Option::Some(Retval {
748                ty: DataType::Bool,
749                nullable: true,
750            })
751        );
752        assert!(method.r#static);
753        assert_eq!(method.visibility, Visibility::Protected);
754        assert_eq!(method.ty, MethodType::Static);
755    }
756
757    #[test]
758    fn test_ty_from() {
759        let r#static: MethodType = MethodFlags::Static.into();
760        assert_eq!(r#static, MethodType::Static);
761
762        let constructor: MethodType = MethodFlags::IsConstructor.into();
763        assert_eq!(constructor, MethodType::Constructor);
764
765        let member: MethodType = MethodFlags::Public.into();
766        assert_eq!(member, MethodType::Member);
767
768        let mixed: MethodType = (MethodFlags::Protected | MethodFlags::Static).into();
769        assert_eq!(mixed, MethodType::Static);
770
771        let both: MethodType = (MethodFlags::Static | MethodFlags::IsConstructor).into();
772        assert_eq!(both, MethodType::Constructor);
773
774        let empty: MethodType = MethodFlags::empty().into();
775        assert_eq!(empty, MethodType::Member);
776    }
777
778    #[test]
779    fn test_prop_visibility_from() {
780        let private: Visibility = PropertyFlags::Private.into();
781        assert_eq!(private, Visibility::Private);
782
783        let protected: Visibility = PropertyFlags::Protected.into();
784        assert_eq!(protected, Visibility::Protected);
785
786        let public: Visibility = PropertyFlags::Public.into();
787        assert_eq!(public, Visibility::Public);
788
789        let mixed: Visibility = (PropertyFlags::Protected | PropertyFlags::Static).into();
790        assert_eq!(mixed, Visibility::Protected);
791
792        let empty: Visibility = PropertyFlags::empty().into();
793        assert_eq!(empty, Visibility::Public);
794    }
795
796    #[test]
797    fn test_method_visibility_from() {
798        let private: Visibility = MethodFlags::Private.into();
799        assert_eq!(private, Visibility::Private);
800
801        let protected: Visibility = MethodFlags::Protected.into();
802        assert_eq!(protected, Visibility::Protected);
803
804        let public: Visibility = MethodFlags::Public.into();
805        assert_eq!(public, Visibility::Public);
806
807        let mixed: Visibility = (MethodFlags::Protected | MethodFlags::Static).into();
808        assert_eq!(mixed, Visibility::Protected);
809
810        let empty: Visibility = MethodFlags::empty().into();
811        assert_eq!(empty, Visibility::Public);
812    }
813}