Skip to main content

ext_php_rs/builders/
enum_builder.rs

1use std::{ffi::CString, mem::ManuallyDrop, ptr};
2
3use crate::{
4    args::ArgInfoTables,
5    builders::{
6        FunctionBuilder,
7        class::{ensure_class_name_free, first_duplicate},
8        function::free_registered_entries,
9    },
10    convert::IntoZval,
11    describe::DocComments,
12    enum_::{Discriminant, EnumCase},
13    error::{Error, Result},
14    ffi::{zend_enum_add_case, zend_register_internal_enum},
15    flags::{DataType, DataTypeExt, MethodFlags},
16    types::{ZendStr, Zval},
17    zend::{ClassEntry, ExecutorGlobals, FunctionEntry},
18};
19
20/// A builder for PHP enums.
21#[must_use]
22pub struct EnumBuilder {
23    pub(crate) name: String,
24    pub(crate) methods: Vec<(FunctionBuilder<'static>, MethodFlags)>,
25    pub(crate) cases: Vec<&'static EnumCase>,
26    pub(crate) datatype: DataType,
27    register: Option<fn(&'static mut ClassEntry, ArgInfoTables)>,
28    pub(crate) docs: DocComments,
29}
30
31impl EnumBuilder {
32    /// Creates a new enum builder with the given name.
33    pub fn new<T: Into<String>>(name: T) -> Self {
34        Self {
35            name: name.into(),
36            methods: Vec::default(),
37            cases: Vec::default(),
38            datatype: DataType::Undef,
39            register: None,
40            docs: DocComments::default(),
41        }
42    }
43
44    /// Adds an enum case to the enum.
45    ///
46    /// # Panics
47    ///
48    /// If the case's data type does not match the enum's data type
49    pub fn case(mut self, case: &'static EnumCase) -> Self {
50        let data_type = case.data_type();
51        assert!(
52            data_type == self.datatype || self.cases.is_empty(),
53            "Cannot add case with data type {:?} to enum with data type {:?}",
54            data_type,
55            self.datatype
56        );
57
58        self.datatype = data_type;
59        self.cases.push(case);
60
61        self
62    }
63
64    /// Adds a method to the enum.
65    pub fn method(mut self, method: FunctionBuilder<'static>, flags: MethodFlags) -> Self {
66        self.methods.push((method, flags));
67        self
68    }
69
70    /// Function to register the enum with PHP, called once it is built.
71    ///
72    /// See [`ClassBuilder::registration`] for the ownership contract of the
73    /// argument info tables.
74    ///
75    /// # Parameters
76    ///
77    /// * `register` - The function to call to register the enum.
78    ///
79    /// [`ClassBuilder::registration`]: crate::builders::ClassBuilder::registration
80    pub fn registration(mut self, register: fn(&'static mut ClassEntry, ArgInfoTables)) -> Self {
81        self.register = Some(register);
82        self
83    }
84
85    /// Add documentation comments to the enum.
86    pub fn docs(mut self, docs: DocComments) -> Self {
87        self.docs = docs;
88        self
89    }
90
91    /// Checks that no two methods or cases of the enum share a PHP name.
92    /// Methods are compared ignoring ASCII case, like the engine.
93    fn validate(&self) -> Result<()> {
94        if let Some(name) = first_duplicate(self.methods.iter().map(|(m, _)| m.name.as_str()), true)
95        {
96            return Err(Error::DuplicateMethod {
97                method: format!("{}::{name}", self.name),
98            });
99        }
100        if let Some(name) = first_duplicate(self.cases.iter().map(|c| c.name), false) {
101            return Err(Error::DuplicateConstant {
102                constant: format!("{}::{name}", self.name),
103            });
104        }
105        Ok(())
106    }
107
108    /// Registers the enum with PHP.
109    ///
110    /// # Panics
111    ///
112    /// * If called outside a module startup (MINIT) function.
113    /// * If the registration function was not set prior to calling this
114    ///   method.
115    ///
116    /// # Errors
117    ///
118    /// * [`Error::DuplicateMethod`] or [`Error::DuplicateConstant`] - If two
119    ///   methods or cases share a PHP name.
120    /// * [`Error::DuplicateClass`] - If a class with this name, ignoring ASCII
121    ///   case, is already registered.
122    /// * If the enum could not be registered, e.g. due to an invalid name or
123    ///   data type.
124    pub fn register(self) -> Result<()> {
125        assert!(
126            !ExecutorGlobals::get().current_module.is_null(),
127            "Enums can only be registered from a module startup (MINIT) function: \
128             `do_register_internal_class` dereferences `EG(current_module)`."
129        );
130        self.validate()?;
131        ensure_class_name_free(&self.name)?;
132
133        let mut arg_info = Vec::with_capacity(self.methods.len());
134        let mut methods = Vec::with_capacity(self.methods.len() + 1);
135        for (method, flags) in self.methods {
136            let (mut entry, args) = method.build()?;
137            entry.flags |= flags.bits();
138            methods.push(entry);
139            arg_info.push(args);
140        }
141
142        methods.push(FunctionEntry::end());
143
144        let name = CString::new(self.name)?;
145        let backing_type = self.datatype.as_u32().try_into()?;
146        let entries = Box::into_raw(methods.into_boxed_slice());
147
148        // The engine keeps `zend_internal_function.arg_info` pointing into these
149        // for the life of the process, so `register` parks them. Wrapped only once
150        // the fallible steps are done, so an early return still drops them.
151        let arg_info = ManuallyDrop::new(arg_info.into_boxed_slice());
152
153        let class = unsafe {
154            zend_register_internal_enum(
155                name.as_ptr(),
156                backing_type,
157                entries.cast::<FunctionEntry>(),
158            )
159        };
160
161        // SAFETY: `zend_register_internal_enum` funnels through
162        // `do_register_internal_class`, which has interned every `fname` and read
163        // `builtin_functions` for the last time.
164        unsafe { free_registered_entries(entries) };
165        unsafe { (*class).info.internal.builtin_functions = ptr::null() };
166
167        for case in self.cases {
168            let name = ZendStr::new_interned(case.name, true);
169            // `zend_enum_add_case` interns a string discriminant in place and
170            // `create_enum_case_ast` takes the payload with `ZVAL_COPY_VALUE`, so the
171            // case AST owns it and the `Zval` must not run its destructor.
172            let mut value = match &case.discriminant {
173                Some(value) => Some(ManuallyDrop::new(Self::create_enum_value(value)?)),
174                None => None,
175            };
176            let value = value
177                .as_mut()
178                .map_or(ptr::null_mut(), |value| &raw mut **value);
179            unsafe {
180                zend_enum_add_case(class, name.into_raw(), value);
181            }
182        }
183
184        if let Some(register) = self.register {
185            register(unsafe { &mut *class }, ManuallyDrop::into_inner(arg_info));
186        } else {
187            panic!("Enum was not registered with a registration function");
188        }
189
190        Ok(())
191    }
192
193    fn create_enum_value(discriminant: &Discriminant) -> Result<Zval> {
194        Ok(match discriminant {
195            Discriminant::Int(i) => i.into_zval(false)?,
196            Discriminant::String(s) => s.into_zval(true)?,
197        })
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use crate::enum_::Discriminant;
205
206    #[test]
207    fn validate_rejects_a_repeated_case() {
208        const same: EnumCase = EnumCase {
209            name: "Variant1",
210            discriminant: None,
211            docs: &[],
212        };
213        let builder = EnumBuilder::new("Foo").case(&case1).case(&same);
214        assert!(matches!(
215            builder.validate(),
216            Err(Error::DuplicateConstant { constant }) if constant == "Foo::Variant1"
217        ));
218    }
219
220    #[test]
221    fn validate_rejects_methods_that_differ_only_by_case() {
222        let builder = EnumBuilder::new("Foo")
223            .method(
224                FunctionBuilder::new("label", crate::test::test_function),
225                MethodFlags::Public,
226            )
227            .method(
228                FunctionBuilder::new("Label", crate::test::test_function),
229                MethodFlags::Public,
230            );
231        assert!(matches!(
232            builder.validate(),
233            Err(Error::DuplicateMethod { method }) if method == "Foo::Label"
234        ));
235    }
236
237    const case1: EnumCase = EnumCase {
238        name: "Variant1",
239        discriminant: None,
240        docs: &[],
241    };
242    const case2: EnumCase = EnumCase {
243        name: "Variant2",
244        discriminant: Some(Discriminant::Int(42)),
245        docs: &[],
246    };
247    const case3: EnumCase = EnumCase {
248        name: "Variant3",
249        discriminant: Some(Discriminant::String("foo")),
250        docs: &[],
251    };
252
253    #[test]
254    fn test_new_enum_builder() {
255        let builder = EnumBuilder::new("MyEnum");
256        assert_eq!(builder.name, "MyEnum");
257        assert!(builder.methods.is_empty());
258        assert!(builder.cases.is_empty());
259        assert_eq!(builder.datatype, DataType::Undef);
260        assert!(builder.register.is_none());
261    }
262
263    #[test]
264    fn test_enum_case() {
265        let builder = EnumBuilder::new("MyEnum").case(&case1);
266        assert_eq!(builder.cases.len(), 1);
267        assert_eq!(builder.cases[0].name, "Variant1");
268        assert_eq!(builder.datatype, DataType::Undef);
269
270        let builder = EnumBuilder::new("MyEnum").case(&case2);
271        assert_eq!(builder.cases.len(), 1);
272        assert_eq!(builder.cases[0].name, "Variant2");
273        assert_eq!(builder.cases[0].discriminant, Some(Discriminant::Int(42)));
274        assert_eq!(builder.datatype, DataType::Long);
275
276        let builder = EnumBuilder::new("MyEnum").case(&case3);
277        assert_eq!(builder.cases.len(), 1);
278        assert_eq!(builder.cases[0].name, "Variant3");
279        assert_eq!(
280            builder.cases[0].discriminant,
281            Some(Discriminant::String("foo"))
282        );
283        assert_eq!(builder.datatype, DataType::String);
284    }
285
286    #[test]
287    #[should_panic(expected = "Cannot add case with data type Long to enum with data type Undef")]
288    fn test_enum_case_mismatch() {
289        #[allow(unused_must_use)]
290        EnumBuilder::new("MyEnum").case(&case1).case(&case2); // This should panic because case2 has a different data type
291    }
292
293    const docs: DocComments = &["This is a test enum"];
294    #[test]
295    fn test_docs() {
296        let builder = EnumBuilder::new("MyEnum").docs(docs);
297        assert_eq!(builder.docs.len(), 1);
298        assert_eq!(builder.docs[0], "This is a test enum");
299    }
300}