use std::{ffi::CString, mem::ManuallyDrop, ptr};
use crate::{
args::ArgInfoTables,
builders::{
FunctionBuilder,
class::{ensure_class_name_free, first_duplicate},
function::free_registered_entries,
},
convert::IntoZval,
describe::DocComments,
enum_::{Discriminant, EnumCase},
error::{Error, Result},
ffi::{zend_enum_add_case, zend_register_internal_enum},
flags::{DataType, DataTypeExt, MethodFlags},
types::{ZendStr, Zval},
zend::{ClassEntry, ExecutorGlobals, FunctionEntry},
};
#[must_use]
pub struct EnumBuilder {
pub(crate) name: String,
pub(crate) methods: Vec<(FunctionBuilder<'static>, MethodFlags)>,
pub(crate) cases: Vec<&'static EnumCase>,
pub(crate) datatype: DataType,
register: Option<fn(&'static mut ClassEntry, ArgInfoTables)>,
pub(crate) docs: DocComments,
}
impl EnumBuilder {
pub fn new<T: Into<String>>(name: T) -> Self {
Self {
name: name.into(),
methods: Vec::default(),
cases: Vec::default(),
datatype: DataType::Undef,
register: None,
docs: DocComments::default(),
}
}
pub fn case(mut self, case: &'static EnumCase) -> Self {
let data_type = case.data_type();
assert!(
data_type == self.datatype || self.cases.is_empty(),
"Cannot add case with data type {:?} to enum with data type {:?}",
data_type,
self.datatype
);
self.datatype = data_type;
self.cases.push(case);
self
}
pub fn method(mut self, method: FunctionBuilder<'static>, flags: MethodFlags) -> Self {
self.methods.push((method, flags));
self
}
pub fn registration(mut self, register: fn(&'static mut ClassEntry, ArgInfoTables)) -> Self {
self.register = Some(register);
self
}
pub fn docs(mut self, docs: DocComments) -> Self {
self.docs = docs;
self
}
fn validate(&self) -> Result<()> {
if let Some(name) = first_duplicate(self.methods.iter().map(|(m, _)| m.name.as_str()), true)
{
return Err(Error::DuplicateMethod {
method: format!("{}::{name}", self.name),
});
}
if let Some(name) = first_duplicate(self.cases.iter().map(|c| c.name), false) {
return Err(Error::DuplicateConstant {
constant: format!("{}::{name}", self.name),
});
}
Ok(())
}
pub fn register(self) -> Result<()> {
assert!(
!ExecutorGlobals::get().current_module.is_null(),
"Enums can only be registered from a module startup (MINIT) function: \
`do_register_internal_class` dereferences `EG(current_module)`."
);
self.validate()?;
ensure_class_name_free(&self.name)?;
let mut arg_info = Vec::with_capacity(self.methods.len());
let mut methods = Vec::with_capacity(self.methods.len() + 1);
for (method, flags) in self.methods {
let (mut entry, args) = method.build()?;
entry.flags |= flags.bits();
methods.push(entry);
arg_info.push(args);
}
methods.push(FunctionEntry::end());
let name = CString::new(self.name)?;
let backing_type = self.datatype.as_u32().try_into()?;
let entries = Box::into_raw(methods.into_boxed_slice());
let arg_info = ManuallyDrop::new(arg_info.into_boxed_slice());
let class = unsafe {
zend_register_internal_enum(
name.as_ptr(),
backing_type,
entries.cast::<FunctionEntry>(),
)
};
unsafe { free_registered_entries(entries) };
unsafe { (*class).info.internal.builtin_functions = ptr::null() };
for case in self.cases {
let name = ZendStr::new_interned(case.name, true);
let mut value = match &case.discriminant {
Some(value) => Some(ManuallyDrop::new(Self::create_enum_value(value)?)),
None => None,
};
let value = value
.as_mut()
.map_or(ptr::null_mut(), |value| &raw mut **value);
unsafe {
zend_enum_add_case(class, name.into_raw(), value);
}
}
if let Some(register) = self.register {
register(unsafe { &mut *class }, ManuallyDrop::into_inner(arg_info));
} else {
panic!("Enum was not registered with a registration function");
}
Ok(())
}
fn create_enum_value(discriminant: &Discriminant) -> Result<Zval> {
Ok(match discriminant {
Discriminant::Int(i) => i.into_zval(false)?,
Discriminant::String(s) => s.into_zval(true)?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::enum_::Discriminant;
#[test]
fn validate_rejects_a_repeated_case() {
const same: EnumCase = EnumCase {
name: "Variant1",
discriminant: None,
docs: &[],
};
let builder = EnumBuilder::new("Foo").case(&case1).case(&same);
assert!(matches!(
builder.validate(),
Err(Error::DuplicateConstant { constant }) if constant == "Foo::Variant1"
));
}
#[test]
fn validate_rejects_methods_that_differ_only_by_case() {
let builder = EnumBuilder::new("Foo")
.method(
FunctionBuilder::new("label", crate::test::test_function),
MethodFlags::Public,
)
.method(
FunctionBuilder::new("Label", crate::test::test_function),
MethodFlags::Public,
);
assert!(matches!(
builder.validate(),
Err(Error::DuplicateMethod { method }) if method == "Foo::Label"
));
}
const case1: EnumCase = EnumCase {
name: "Variant1",
discriminant: None,
docs: &[],
};
const case2: EnumCase = EnumCase {
name: "Variant2",
discriminant: Some(Discriminant::Int(42)),
docs: &[],
};
const case3: EnumCase = EnumCase {
name: "Variant3",
discriminant: Some(Discriminant::String("foo")),
docs: &[],
};
#[test]
fn test_new_enum_builder() {
let builder = EnumBuilder::new("MyEnum");
assert_eq!(builder.name, "MyEnum");
assert!(builder.methods.is_empty());
assert!(builder.cases.is_empty());
assert_eq!(builder.datatype, DataType::Undef);
assert!(builder.register.is_none());
}
#[test]
fn test_enum_case() {
let builder = EnumBuilder::new("MyEnum").case(&case1);
assert_eq!(builder.cases.len(), 1);
assert_eq!(builder.cases[0].name, "Variant1");
assert_eq!(builder.datatype, DataType::Undef);
let builder = EnumBuilder::new("MyEnum").case(&case2);
assert_eq!(builder.cases.len(), 1);
assert_eq!(builder.cases[0].name, "Variant2");
assert_eq!(builder.cases[0].discriminant, Some(Discriminant::Int(42)));
assert_eq!(builder.datatype, DataType::Long);
let builder = EnumBuilder::new("MyEnum").case(&case3);
assert_eq!(builder.cases.len(), 1);
assert_eq!(builder.cases[0].name, "Variant3");
assert_eq!(
builder.cases[0].discriminant,
Some(Discriminant::String("foo"))
);
assert_eq!(builder.datatype, DataType::String);
}
#[test]
#[should_panic(expected = "Cannot add case with data type Long to enum with data type Undef")]
fn test_enum_case_mismatch() {
#[allow(unused_must_use)]
EnumBuilder::new("MyEnum").case(&case1).case(&case2); }
const docs: DocComments = &["This is a test enum"];
#[test]
fn test_docs() {
let builder = EnumBuilder::new("MyEnum").docs(docs);
assert_eq!(builder.docs.len(), 1);
assert_eq!(builder.docs[0], "This is a test enum");
}
}