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