use super::{ArrayKey, ZendHashTable};
use crate::error::Error;
use crate::{
convert::IntoZval,
error::Result,
ffi::{
zend_hash_index_find, zend_hash_index_update, zend_hash_str_find, zend_hash_str_update,
zend_ulong,
},
types::Zval,
};
use std::mem::ManuallyDrop;
pub enum Entry<'a, 'k> {
Occupied(OccupiedEntry<'a, 'k>),
Vacant(VacantEntry<'a, 'k>),
}
pub struct OccupiedEntry<'a, 'k> {
ht: &'a mut ZendHashTable,
key: ArrayKey<'k>,
}
pub struct VacantEntry<'a, 'k> {
ht: &'a mut ZendHashTable,
key: ArrayKey<'k>,
}
impl<'a, 'k> Entry<'a, 'k> {
pub fn or_insert<V: IntoZval>(self, default: V) -> Result<&'a mut Zval> {
match self {
Entry::Occupied(entry) => entry.into_mut().ok_or(Error::InvalidPointer),
Entry::Vacant(entry) => entry.insert(default),
}
}
pub fn or_insert_with<V: IntoZval, F: FnOnce() -> V>(self, default: F) -> Result<&'a mut Zval> {
match self {
Entry::Occupied(entry) => entry.into_mut().ok_or(Error::InvalidPointer),
Entry::Vacant(entry) => entry.insert(default()),
}
}
pub fn or_insert_with_key<V: IntoZval, F: FnOnce(&ArrayKey<'k>) -> V>(
self,
default: F,
) -> Result<&'a mut Zval> {
match self {
Entry::Occupied(entry) => entry.into_mut().ok_or(Error::InvalidPointer),
Entry::Vacant(entry) => {
let value = default(entry.key());
entry.insert(value)
}
}
}
#[must_use]
pub fn key(&self) -> &ArrayKey<'k> {
match self {
Entry::Occupied(entry) => entry.key(),
Entry::Vacant(entry) => entry.key(),
}
}
#[must_use]
pub fn and_modify<F: FnOnce(&mut Zval)>(self, f: F) -> Self {
match self {
Entry::Occupied(mut entry) => {
if let Some(value) = entry.get_mut() {
f(value);
}
Entry::Occupied(entry)
}
Entry::Vacant(entry) => Entry::Vacant(entry),
}
}
}
impl<'a, 'k> Entry<'a, 'k>
where
'k: 'a,
{
pub fn or_default(self) -> Result<&'a mut Zval> {
match self {
Entry::Occupied(entry) => entry.into_mut().ok_or(Error::InvalidPointer),
Entry::Vacant(entry) => entry.insert(Zval::new()),
}
}
}
impl<'a, 'k> OccupiedEntry<'a, 'k> {
pub(super) fn new(ht: &'a mut ZendHashTable, key: ArrayKey<'k>) -> Self {
Self { ht, key }
}
#[must_use]
pub fn key(&self) -> &ArrayKey<'k> {
&self.key
}
#[must_use]
pub fn get(&self) -> Option<&Zval> {
match &self.key {
ArrayKey::Long(index) => unsafe {
#[allow(clippy::cast_sign_loss)]
zend_hash_index_find(self.ht, *index as zend_ulong).as_ref()
},
ArrayKey::String(key) => unsafe {
zend_hash_str_find(self.ht, key.as_ptr().cast(), key.len()).as_ref()
},
ArrayKey::Str(key) => unsafe {
zend_hash_str_find(self.ht, key.as_ptr().cast(), key.len()).as_ref()
},
}
}
pub fn get_mut(&mut self) -> Option<&mut Zval> {
get_value_mut(&self.key, self.ht)
}
#[must_use]
pub fn into_mut(self) -> Option<&'a mut Zval> {
get_value_mut(&self.key, self.ht)
}
pub fn insert<V: IntoZval>(&mut self, value: V) -> Result<Option<Zval>> {
let old = self.get().map(Zval::shallow_clone);
insert_value(&self.key, self.ht, value)?;
Ok(old)
}
pub fn remove_entry(self) -> (ArrayKey<'k>, Option<Zval>) {
let value = self.get().map(Zval::shallow_clone);
self.ht.remove(self.key.clone());
(self.key, value)
}
pub fn remove(self) -> Option<Zval> {
let value = self.get().map(Zval::shallow_clone);
self.ht.remove(self.key);
value
}
}
impl<'a, 'k> VacantEntry<'a, 'k> {
pub(super) fn new(ht: &'a mut ZendHashTable, key: ArrayKey<'k>) -> Self {
Self { ht, key }
}
#[must_use]
pub fn key(&self) -> &ArrayKey<'k> {
&self.key
}
#[must_use]
pub fn into_key(self) -> ArrayKey<'k> {
self.key
}
pub fn insert<V: IntoZval>(self, value: V) -> Result<&'a mut Zval> {
insert_value(&self.key, self.ht, value)
}
}
#[inline]
fn get_value_mut<'a>(key: &ArrayKey<'_>, ht: &'a mut ZendHashTable) -> Option<&'a mut Zval> {
match key {
ArrayKey::Long(index) => unsafe {
#[allow(clippy::cast_sign_loss)]
zend_hash_index_find(ht, *index as zend_ulong).as_mut()
},
ArrayKey::String(key) => unsafe {
zend_hash_str_find(ht, key.as_ptr().cast(), key.len()).as_mut()
},
ArrayKey::Str(key) => unsafe {
zend_hash_str_find(ht, key.as_ptr().cast(), key.len()).as_mut()
},
}
}
fn insert_value<'a, V: IntoZval>(
key: &ArrayKey<'_>,
ht: &'a mut ZendHashTable,
value: V,
) -> Result<&'a mut Zval> {
let mut val = ManuallyDrop::new(value.into_zval(false)?);
match key {
ArrayKey::Long(index) => unsafe {
#[allow(clippy::cast_sign_loss)]
zend_hash_index_update(ht, *index as zend_ulong, &raw mut *val)
.as_mut()
.ok_or(Error::InvalidPointer)
},
ArrayKey::String(key) => unsafe {
zend_hash_str_update(ht, key.as_ptr().cast(), key.len(), &raw mut *val)
.as_mut()
.ok_or(Error::InvalidPointer)
},
ArrayKey::Str(key) => unsafe {
zend_hash_str_update(ht, key.as_ptr().cast(), key.len(), &raw mut *val)
.as_mut()
.ok_or(Error::InvalidPointer)
},
}
}
#[cfg(test)]
#[cfg(feature = "embed")]
mod tests {
use super::*;
use crate::embed::Embed;
#[test]
fn test_entry_or_insert() {
Embed::run(|| {
let mut ht = ZendHashTable::new();
let result = ht.entry("key").or_insert("value");
assert!(result.is_ok());
assert_eq!(ht.get("key").and_then(|v| v.str()), Some("value"));
let result = ht.entry("key").or_insert("other");
assert!(result.is_ok());
assert_eq!(ht.get("key").and_then(|v| v.str()), Some("value"));
});
}
#[test]
fn test_entry_or_insert_with() {
Embed::run(|| {
let mut ht = ZendHashTable::new();
let result = ht.entry("key").or_insert_with(|| "computed");
assert!(result.is_ok());
assert_eq!(ht.get("key").and_then(|v| v.str()), Some("computed"));
});
}
#[test]
fn test_entry_and_modify() {
Embed::run(|| {
let mut ht = ZendHashTable::new();
let _ = ht.insert("counter", 5i64);
let result = ht
.entry("counter")
.and_modify(|v| {
if let Some(n) = v.long() {
v.set_long(n + 1);
}
})
.or_insert(0i64);
assert!(result.is_ok());
assert_eq!(ht.get("counter").and_then(Zval::long), Some(6));
});
}
#[test]
fn test_entry_and_modify_vacant() {
Embed::run(|| {
let mut ht = ZendHashTable::new();
let result = ht
.entry("key")
.and_modify(|v| {
v.set_long(100);
})
.or_insert(42i64);
assert!(result.is_ok());
assert_eq!(ht.get("key").and_then(Zval::long), Some(42));
});
}
#[test]
fn test_occupied_entry_insert() {
Embed::run(|| {
let mut ht = ZendHashTable::new();
let _ = ht.insert("key", "old");
if let Entry::Occupied(mut entry) = ht.entry("key") {
let old = entry.insert("new").expect("insert should succeed");
assert_eq!(old.as_ref().and_then(Zval::str), Some("old"));
}
assert_eq!(ht.get("key").and_then(|v| v.str()), Some("new"));
});
}
#[test]
fn test_occupied_entry_remove() {
Embed::run(|| {
let mut ht = ZendHashTable::new();
let _ = ht.insert("key", "value");
if let Entry::Occupied(entry) = ht.entry("key") {
let value = entry.remove().expect("remove should succeed");
assert_eq!(value.str(), Some("value"));
}
assert!(ht.get("key").is_none());
});
}
#[test]
fn test_entry_with_numeric_key() {
Embed::run(|| {
let mut ht = ZendHashTable::new();
let result = ht.entry(42i64).or_insert("value");
assert!(result.is_ok());
assert_eq!(ht.get_index(42).and_then(|v| v.str()), Some("value"));
});
}
#[test]
fn test_vacant_entry_into_key() {
Embed::run(|| {
let mut ht = ZendHashTable::new();
if let Entry::Vacant(entry) = ht.entry("my_key") {
let key = entry.into_key();
assert_eq!(key, ArrayKey::Str("my_key"));
}
});
}
#[test]
fn test_entry_with_binary_key() {
Embed::run(|| {
let mut ht = ZendHashTable::new();
let key = "\0MyClass\0myProp";
let result = ht.entry(key).or_insert("value");
assert!(result.is_ok());
});
}
}