ext_php_rs/builders/
enum_builder.rs1use std::{ffi::CString, ptr};
2
3use crate::{
4    builders::FunctionBuilder,
5    convert::IntoZval,
6    describe::DocComments,
7    enum_::{Discriminant, EnumCase},
8    error::Result,
9    ffi::{zend_enum_add_case, zend_register_internal_enum},
10    flags::{DataType, MethodFlags},
11    types::{ZendStr, Zval},
12    zend::{ClassEntry, FunctionEntry},
13};
14
15#[must_use]
17pub struct EnumBuilder {
18    pub(crate) name: String,
19    pub(crate) methods: Vec<(FunctionBuilder<'static>, MethodFlags)>,
20    pub(crate) cases: Vec<&'static EnumCase>,
21    pub(crate) datatype: DataType,
22    register: Option<fn(&'static mut ClassEntry)>,
23    pub(crate) docs: DocComments,
24}
25
26impl EnumBuilder {
27    pub fn new<T: Into<String>>(name: T) -> Self {
29        Self {
30            name: name.into(),
31            methods: Vec::default(),
32            cases: Vec::default(),
33            datatype: DataType::Undef,
34            register: None,
35            docs: DocComments::default(),
36        }
37    }
38
39    pub fn case(mut self, case: &'static EnumCase) -> Self {
45        let data_type = case.data_type();
46        assert!(
47            data_type == self.datatype || self.cases.is_empty(),
48            "Cannot add case with data type {:?} to enum with data type {:?}",
49            data_type,
50            self.datatype
51        );
52
53        self.datatype = data_type;
54        self.cases.push(case);
55
56        self
57    }
58
59    pub fn method(mut self, method: FunctionBuilder<'static>, flags: MethodFlags) -> Self {
61        self.methods.push((method, flags));
62        self
63    }
64
65    pub fn registration(mut self, register: fn(&'static mut ClassEntry)) -> Self {
72        self.register = Some(register);
73        self
74    }
75
76    pub fn docs(mut self, docs: DocComments) -> Self {
78        self.docs = docs;
79        self
80    }
81
82    pub fn register(self) -> Result<()> {
94        let mut methods = self
95            .methods
96            .into_iter()
97            .map(|(method, flags)| {
98                method.build().map(|mut method| {
99                    method.flags |= flags.bits();
100                    method
101                })
102            })
103            .collect::<Result<Vec<_>>>()?;
104        methods.push(FunctionEntry::end());
105
106        let class = unsafe {
107            zend_register_internal_enum(
108                CString::new(self.name)?.as_ptr(),
109                self.datatype.as_u32().try_into()?,
110                methods.into_boxed_slice().as_ptr(),
111            )
112        };
113
114        for case in self.cases {
115            let name = ZendStr::new_interned(case.name, true);
116            let value = match &case.discriminant {
117                Some(value) => Self::create_enum_value(value)?,
118                None => ptr::null_mut(),
119            };
120            unsafe {
121                zend_enum_add_case(class, name.into_raw(), value);
122            }
123        }
124
125        if let Some(register) = self.register {
126            register(unsafe { &mut *class });
127        } else {
128            panic!("Enum was not registered with a registration function",);
129        }
130
131        Ok(())
132    }
133
134    fn create_enum_value(discriminant: &Discriminant) -> Result<*mut Zval> {
135        let value: Zval = match discriminant {
136            Discriminant::Int(i) => i.into_zval(false)?,
137            Discriminant::String(s) => s.into_zval(true)?,
138        };
139
140        let boxed_value = Box::new(value);
141        Ok(Box::into_raw(boxed_value).cast())
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use crate::enum_::Discriminant;
149
150    const case1: EnumCase = EnumCase {
151        name: "Variant1",
152        discriminant: None,
153        docs: &[],
154    };
155    const case2: EnumCase = EnumCase {
156        name: "Variant2",
157        discriminant: Some(Discriminant::Int(42)),
158        docs: &[],
159    };
160    const case3: EnumCase = EnumCase {
161        name: "Variant3",
162        discriminant: Some(Discriminant::String("foo")),
163        docs: &[],
164    };
165
166    #[test]
167    fn test_new_enum_builder() {
168        let builder = EnumBuilder::new("MyEnum");
169        assert_eq!(builder.name, "MyEnum");
170        assert!(builder.methods.is_empty());
171        assert!(builder.cases.is_empty());
172        assert_eq!(builder.datatype, DataType::Undef);
173        assert!(builder.register.is_none());
174    }
175
176    #[test]
177    fn test_enum_case() {
178        let builder = EnumBuilder::new("MyEnum").case(&case1);
179        assert_eq!(builder.cases.len(), 1);
180        assert_eq!(builder.cases[0].name, "Variant1");
181        assert_eq!(builder.datatype, DataType::Undef);
182
183        let builder = EnumBuilder::new("MyEnum").case(&case2);
184        assert_eq!(builder.cases.len(), 1);
185        assert_eq!(builder.cases[0].name, "Variant2");
186        assert_eq!(builder.cases[0].discriminant, Some(Discriminant::Int(42)));
187        assert_eq!(builder.datatype, DataType::Long);
188
189        let builder = EnumBuilder::new("MyEnum").case(&case3);
190        assert_eq!(builder.cases.len(), 1);
191        assert_eq!(builder.cases[0].name, "Variant3");
192        assert_eq!(
193            builder.cases[0].discriminant,
194            Some(Discriminant::String("foo"))
195        );
196        assert_eq!(builder.datatype, DataType::String);
197    }
198
199    #[test]
200    #[should_panic(expected = "Cannot add case with data type Long to enum with data type Undef")]
201    fn test_enum_case_mismatch() {
202        #[allow(unused_must_use)]
203        EnumBuilder::new("MyEnum").case(&case1).case(&case2); }
205
206    const docs: DocComments = &["This is a test enum"];
207    #[test]
208    fn test_docs() {
209        let builder = EnumBuilder::new("MyEnum").docs(docs);
210        assert_eq!(builder.docs.len(), 1);
211        assert_eq!(builder.docs[0], "This is a test enum");
212    }
213}