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