diff --git a/src/types/array/array_key.rs b/src/types/array/array_key.rs index d229ce3e8..4bb161e85 100644 --- a/src/types/array/array_key.rs +++ b/src/types/array/array_key.rs @@ -1,9 +1,9 @@ -use std::{convert::TryFrom, fmt::Display}; - use crate::{convert::FromZval, error::Error, flags::DataType, types::Zval}; +use std::str::FromStr; +use std::{convert::TryFrom, fmt::Display}; /// Represents the key of a PHP array, which can be either a long or a string. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum ArrayKey<'a> { /// A numerical key. /// In Zend API it's represented by `u64` (`zend_ulong`), so the value needs @@ -17,18 +17,22 @@ pub enum ArrayKey<'a> { impl From for ArrayKey<'_> { fn from(value: String) -> Self { - Self::String(value) + if let Ok(index) = i64::from_str(value.as_str()) { + Self::Long(index) + } else { + Self::String(value) + } } } impl TryFrom> for String { type Error = Error; - fn try_from(value: ArrayKey<'_>) -> std::result::Result { + fn try_from(value: ArrayKey<'_>) -> Result { match value { ArrayKey::String(s) => Ok(s), ArrayKey::Str(s) => Ok(s.to_string()), - ArrayKey::Long(_) => Err(Error::InvalidProperty), + ArrayKey::Long(l) => Ok(l.to_string()), } } } @@ -36,7 +40,7 @@ impl TryFrom> for String { impl TryFrom> for i64 { type Error = Error; - fn try_from(value: ArrayKey<'_>) -> std::result::Result { + fn try_from(value: ArrayKey<'_>) -> Result { match value { ArrayKey::Long(i) => Ok(i), ArrayKey::String(s) => s.parse::().map_err(|_| Error::InvalidProperty), @@ -71,8 +75,12 @@ impl Display for ArrayKey<'_> { } impl<'a> From<&'a str> for ArrayKey<'a> { - fn from(key: &'a str) -> ArrayKey<'a> { - ArrayKey::Str(key) + fn from(value: &'a str) -> ArrayKey<'a> { + if let Ok(index) = i64::from_str(value) { + Self::Long(index) + } else { + ArrayKey::Str(value) + } } } @@ -117,8 +125,7 @@ mod tests { let key = ArrayKey::Long(42); let result: crate::error::Result = key.try_into(); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), Error::InvalidProperty)); + assert_eq!(result.unwrap(), "42".to_string()); let key = ArrayKey::String("42".to_string()); let result: crate::error::Result = key.try_into(); diff --git a/src/types/array/conversions/btree_map.rs b/src/types/array/conversions/btree_map.rs new file mode 100644 index 000000000..0c1234d24 --- /dev/null +++ b/src/types/array/conversions/btree_map.rs @@ -0,0 +1,304 @@ +use std::{collections::BTreeMap, convert::TryFrom}; + +use super::super::ZendHashTable; +use crate::types::ArrayKey; +use crate::{ + boxed::ZBox, + convert::{FromZval, IntoZval}, + error::{Error, Result}, + flags::DataType, + types::Zval, +}; + +impl<'a, K, V> TryFrom<&'a ZendHashTable> for BTreeMap +where + K: TryFrom, Error = Error> + Ord, + V: FromZval<'a>, +{ + type Error = Error; + + fn try_from(value: &'a ZendHashTable) -> Result { + let mut map = Self::new(); + + for (key, val) in value { + map.insert( + key.try_into()?, + V::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?, + ); + } + + Ok(map) + } +} + +impl<'a, V> TryFrom<&'a ZendHashTable> for BTreeMap, V> +where + V: FromZval<'a>, +{ + type Error = Error; + + fn try_from(value: &'a ZendHashTable) -> Result { + let mut map = Self::new(); + + for (key, val) in value { + map.insert( + key, + V::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?, + ); + } + + Ok(map) + } +} + +impl<'a, K, V> TryFrom> for ZBox +where + K: Into>, + V: IntoZval, +{ + type Error = Error; + + fn try_from(value: BTreeMap) -> Result { + let mut ht = ZendHashTable::with_capacity( + value.len().try_into().map_err(|_| Error::IntegerOverflow)?, + ); + + for (k, v) in value { + ht.insert(k, v)?; + } + + Ok(ht) + } +} + +impl<'a, K, V> IntoZval for BTreeMap +where + K: Into>, + V: IntoZval, +{ + const TYPE: DataType = DataType::Array; + const NULLABLE: bool = false; + + fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> { + let arr = self.try_into()?; + zv.set_hashtable(arr); + Ok(()) + } +} + +impl<'a, K, V> FromZval<'a> for BTreeMap +where + K: TryFrom, Error = Error> + Ord, + V: FromZval<'a>, +{ + const TYPE: DataType = DataType::Array; + + fn from_zval(zval: &'a Zval) -> Option { + zval.array().and_then(|arr| arr.try_into().ok()) + } +} + +impl<'a, V> FromZval<'a> for BTreeMap, V> +where + V: FromZval<'a>, +{ + const TYPE: DataType = DataType::Array; + + fn from_zval(zval: &'a Zval) -> Option { + zval.array().and_then(|arr| arr.try_into().ok()) + } +} + +#[cfg(test)] +#[cfg(feature = "embed")] +#[allow(clippy::unwrap_used)] +mod tests { + use std::collections::BTreeMap; + + use crate::boxed::ZBox; + use crate::convert::{FromZval, IntoZval}; + use crate::embed::Embed; + use crate::error::Error; + use crate::types::{ArrayKey, ZendHashTable, Zval}; + + #[test] + fn test_hash_table_try_from_btree_mab() { + Embed::run(|| { + let mut map = BTreeMap::new(); + map.insert("key1", "value1"); + map.insert("key2", "value2"); + map.insert("key3", "value3"); + + let ht: ZBox = map.try_into().unwrap(); + assert_eq!(ht.len(), 3); + assert_eq!(ht.get("key1").unwrap().string().unwrap(), "value1"); + assert_eq!(ht.get("key2").unwrap().string().unwrap(), "value2"); + assert_eq!(ht.get("key3").unwrap().string().unwrap(), "value3"); + + let mut map_i64 = BTreeMap::new(); + map_i64.insert(1, "value1"); + map_i64.insert(2, "value2"); + map_i64.insert(3, "value3"); + + let ht_i64: ZBox = map_i64.try_into().unwrap(); + assert_eq!(ht_i64.len(), 3); + assert_eq!(ht_i64.get(1).unwrap().string().unwrap(), "value1"); + assert_eq!(ht_i64.get(2).unwrap().string().unwrap(), "value2"); + assert_eq!(ht_i64.get(3).unwrap().string().unwrap(), "value3"); + }); + } + + #[test] + fn test_btree_map_into_zval() { + Embed::run(|| { + let mut map = BTreeMap::new(); + map.insert("key1", "value1"); + map.insert("key2", "value2"); + map.insert("key3", "value3"); + + let zval = map.into_zval(false).unwrap(); + assert!(zval.is_array()); + let ht: &ZendHashTable = zval.array().unwrap(); + assert_eq!(ht.len(), 3); + assert_eq!(ht.get("key1").unwrap().string().unwrap(), "value1"); + assert_eq!(ht.get("key2").unwrap().string().unwrap(), "value2"); + assert_eq!(ht.get("key3").unwrap().string().unwrap(), "value3"); + + let mut map_i64 = BTreeMap::new(); + map_i64.insert(1, "value1"); + map_i64.insert(2, "value2"); + map_i64.insert(3, "value3"); + let zval_i64 = map_i64.into_zval(false).unwrap(); + assert!(zval_i64.is_array()); + let ht_i64: &ZendHashTable = zval_i64.array().unwrap(); + assert_eq!(ht_i64.len(), 3); + assert_eq!(ht_i64.get(1).unwrap().string().unwrap(), "value1"); + assert_eq!(ht_i64.get(2).unwrap().string().unwrap(), "value2"); + assert_eq!(ht_i64.get(3).unwrap().string().unwrap(), "value3"); + }); + } + + #[test] + fn test_btree_map_from_zval() { + Embed::run(|| { + let mut ht = ZendHashTable::new(); + ht.insert("key1", "value1").unwrap(); + ht.insert("key2", "value2").unwrap(); + ht.insert("key3", "value3").unwrap(); + let mut zval = Zval::new(); + zval.set_hashtable(ht); + + let map = BTreeMap::::from_zval(&zval).unwrap(); + assert_eq!(map.len(), 3); + assert_eq!(map.get("key1").unwrap(), "value1"); + assert_eq!(map.get("key2").unwrap(), "value2"); + assert_eq!(map.get("key3").unwrap(), "value3"); + + let mut ht_i64 = ZendHashTable::new(); + ht_i64.insert(1, "value1").unwrap(); + ht_i64.insert("2", "value2").unwrap(); + ht_i64.insert(3, "value3").unwrap(); + let mut zval_i64 = Zval::new(); + zval_i64.set_hashtable(ht_i64); + + let map_i64 = BTreeMap::::from_zval(&zval_i64).unwrap(); + assert_eq!(map_i64.len(), 3); + assert_eq!(map_i64.get(&1).unwrap(), "value1"); + assert_eq!(map_i64.get(&2).unwrap(), "value2"); + assert_eq!(map_i64.get(&3).unwrap(), "value3"); + + let mut ht_mixed = ZendHashTable::new(); + ht_mixed.insert("key1", "value1").unwrap(); + ht_mixed.insert(2, "value2").unwrap(); + ht_mixed.insert("3", "value3").unwrap(); + let mut zval_mixed = Zval::new(); + zval_mixed.set_hashtable(ht_mixed); + + let map_mixed = BTreeMap::::from_zval(&zval_mixed); + assert!(map_mixed.is_some()); + }); + } + + #[test] + fn test_btree_map_array_key_from_zval() { + Embed::run(|| { + let mut ht = ZendHashTable::new(); + ht.insert("key1", "value1").unwrap(); + ht.insert(2, "value2").unwrap(); + ht.insert("3", "value3").unwrap(); + let mut zval = Zval::new(); + zval.set_hashtable(ht); + + let map = BTreeMap::::from_zval(&zval).unwrap(); + assert_eq!(map.len(), 3); + assert_eq!( + map.get(&ArrayKey::String("key1".to_string())).unwrap(), + "value1" + ); + assert_eq!(map.get(&ArrayKey::Long(2)).unwrap(), "value2"); + assert_eq!(map.get(&ArrayKey::Long(3)).unwrap(), "value3"); + }); + } + + #[test] + fn test_btree_map_i64_v_try_from_hash_table() { + Embed::run(|| { + let mut ht = ZendHashTable::new(); + ht.insert(1, "value1").unwrap(); + ht.insert("2", "value2").unwrap(); + + let map: BTreeMap = ht.as_ref().try_into().unwrap(); + assert_eq!(map.len(), 2); + assert_eq!(map.get(&1).unwrap(), "value1"); + assert_eq!(map.get(&2).unwrap(), "value2"); + + let mut ht2 = ZendHashTable::new(); + ht2.insert("key1", "value1").unwrap(); + ht2.insert("key2", "value2").unwrap(); + + let map_err: crate::error::Result> = ht2.as_ref().try_into(); + assert!(map_err.is_err()); + assert!(matches!(map_err.unwrap_err(), Error::InvalidProperty)); + }); + } + + #[test] + fn test_btree_map_string_v_try_from_hash_table() { + Embed::run(|| { + let mut ht = ZendHashTable::new(); + ht.insert("key1", "value1").unwrap(); + ht.insert("key2", "value2").unwrap(); + + let map: BTreeMap = ht.as_ref().try_into().unwrap(); + assert_eq!(map.len(), 2); + assert_eq!(map.get("key1").unwrap(), "value1"); + assert_eq!(map.get("key2").unwrap(), "value2"); + + let mut ht2 = ZendHashTable::new(); + ht2.insert(1, "value1").unwrap(); + ht2.insert(2, "value2").unwrap(); + + let map2: crate::error::Result> = ht2.as_ref().try_into(); + assert!(map2.is_ok()); + }); + } + + #[test] + fn test_btree_map_array_key_v_try_from_hash_table() { + Embed::run(|| { + let mut ht = ZendHashTable::new(); + ht.insert("key1", "value1").unwrap(); + ht.insert(2, "value2").unwrap(); + ht.insert("3", "value3").unwrap(); + + let map: BTreeMap = ht.as_ref().try_into().unwrap(); + assert_eq!(map.len(), 3); + assert_eq!( + map.get(&ArrayKey::String("key1".to_string())).unwrap(), + "value1" + ); + assert_eq!(map.get(&ArrayKey::Long(2)).unwrap(), "value2"); + assert_eq!(map.get(&ArrayKey::Long(3)).unwrap(), "value3"); + }); + } +} diff --git a/src/types/array/conversions/hash_map.rs b/src/types/array/conversions/hash_map.rs index 768c737a2..bce907810 100644 --- a/src/types/array/conversions/hash_map.rs +++ b/src/types/array/conversions/hash_map.rs @@ -1,5 +1,5 @@ -use std::{collections::HashMap, convert::TryFrom}; - +use super::super::ZendHashTable; +use crate::types::ArrayKey; use crate::{ boxed::ZBox, convert::{FromZval, IntoZval}, @@ -7,23 +7,23 @@ use crate::{ flags::DataType, types::Zval, }; +use std::hash::{BuildHasher, Hash}; +use std::{collections::HashMap, convert::TryFrom}; -use super::super::ZendHashTable; - -// TODO: Generalize hasher -#[allow(clippy::implicit_hasher)] -impl<'a, V> TryFrom<&'a ZendHashTable> for HashMap +impl<'a, K, V, H> TryFrom<&'a ZendHashTable> for HashMap where + K: TryFrom, Error = Error> + Eq + Hash, V: FromZval<'a>, + H: BuildHasher + Default, { type Error = Error; fn try_from(value: &'a ZendHashTable) -> Result { - let mut hm = HashMap::with_capacity(value.len()); + let mut hm = Self::with_capacity_and_hasher(value.len(), H::default()); for (key, val) in value { hm.insert( - key.to_string(), + key.try_into()?, V::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?, ); } @@ -32,14 +32,15 @@ where } } -impl TryFrom> for ZBox +impl TryFrom> for ZBox where K: AsRef, V: IntoZval, + H: BuildHasher, { type Error = Error; - fn try_from(value: HashMap) -> Result { + fn try_from(value: HashMap) -> Result { let mut ht = ZendHashTable::with_capacity( value.len().try_into().map_err(|_| Error::IntegerOverflow)?, ); @@ -52,12 +53,11 @@ where } } -// TODO: Generalize hasher -#[allow(clippy::implicit_hasher)] -impl IntoZval for HashMap +impl IntoZval for HashMap where K: AsRef, V: IntoZval, + H: BuildHasher, { const TYPE: DataType = DataType::Array; const NULLABLE: bool = false; @@ -69,11 +69,10 @@ where } } -// TODO: Generalize hasher -#[allow(clippy::implicit_hasher)] -impl<'a, T> FromZval<'a> for HashMap +impl<'a, V, H> FromZval<'a> for HashMap where - T: FromZval<'a>, + V: FromZval<'a>, + H: BuildHasher + Default, { const TYPE: DataType = DataType::Array; diff --git a/src/types/array/conversions/mod.rs b/src/types/array/conversions/mod.rs index 6748bb599..10b9685b8 100644 --- a/src/types/array/conversions/mod.rs +++ b/src/types/array/conversions/mod.rs @@ -6,8 +6,10 @@ //! //! ## Supported Collections //! +//! - `BTreeMap` ↔ `ZendHashTable` (via `btree_map` module) //! - `HashMap` ↔ `ZendHashTable` (via `hash_map` module) //! - `Vec` and `Vec<(K, V)>` ↔ `ZendHashTable` (via `vec` module) +mod btree_map; mod hash_map; mod vec; diff --git a/src/types/array/conversions/vec.rs b/src/types/array/conversions/vec.rs index 0b61b8cc9..3f1e26c87 100644 --- a/src/types/array/conversions/vec.rs +++ b/src/types/array/conversions/vec.rs @@ -277,7 +277,7 @@ mod tests { let vec_mixed: Option> = Vec::<(String, String)>::from_zval(&zval_mixed); - assert!(vec_mixed.is_none()); + assert!(vec_mixed.is_some()); }); } @@ -345,8 +345,7 @@ mod tests { ht2.insert(2, "value2").unwrap(); let vec2: crate::error::Result> = ht2.as_ref().try_into(); - assert!(vec2.is_err()); - assert!(matches!(vec2.unwrap_err(), Error::InvalidProperty)); + assert!(vec2.is_ok()); }); } diff --git a/src/types/array/mod.rs b/src/types/array/mod.rs index 05658e19a..88c6142c2 100644 --- a/src/types/array/mod.rs +++ b/src/types/array/mod.rs @@ -1,7 +1,7 @@ //! Represents an array in PHP. As all arrays in PHP are associative arrays, //! they are represented by hash tables. -use std::{convert::TryFrom, ffi::CString, fmt::Debug, ptr, str::FromStr}; +use std::{convert::TryFrom, ffi::CString, fmt::Debug, ptr}; use crate::{ boxed::{ZBox, ZBoxable}, @@ -196,36 +196,17 @@ impl ZendHashTable { #[allow(clippy::cast_sign_loss)] zend_hash_index_find(self, index as zend_ulong).as_ref() }, - ArrayKey::String(key) => { - if let Ok(index) = i64::from_str(key.as_str()) { - #[allow(clippy::cast_sign_loss)] - unsafe { - zend_hash_index_find(self, index as zend_ulong).as_ref() - } - } else { - unsafe { - zend_hash_str_find( - self, - CString::new(key.as_str()).ok()?.as_ptr(), - key.len() as _, - ) - .as_ref() - } - } - } - ArrayKey::Str(key) => { - if let Ok(index) = i64::from_str(key) { - #[allow(clippy::cast_sign_loss)] - unsafe { - zend_hash_index_find(self, index as zend_ulong).as_ref() - } - } else { - unsafe { - zend_hash_str_find(self, CString::new(key).ok()?.as_ptr(), key.len() as _) - .as_ref() - } - } - } + ArrayKey::String(key) => unsafe { + zend_hash_str_find( + self, + CString::new(key.as_str()).ok()?.as_ptr(), + key.len() as _, + ) + .as_ref() + }, + ArrayKey::Str(key) => unsafe { + zend_hash_str_find(self, CString::new(key).ok()?.as_ptr(), key.len() as _).as_ref() + }, } } @@ -264,36 +245,17 @@ impl ZendHashTable { #[allow(clippy::cast_sign_loss)] zend_hash_index_find(self, index as zend_ulong).as_mut() }, - ArrayKey::String(key) => { - if let Ok(index) = i64::from_str(key.as_str()) { - #[allow(clippy::cast_sign_loss)] - unsafe { - zend_hash_index_find(self, index as zend_ulong).as_mut() - } - } else { - unsafe { - zend_hash_str_find( - self, - CString::new(key.as_str()).ok()?.as_ptr(), - key.len() as _, - ) - .as_mut() - } - } - } - ArrayKey::Str(key) => { - if let Ok(index) = i64::from_str(key) { - #[allow(clippy::cast_sign_loss)] - unsafe { - zend_hash_index_find(self, index as zend_ulong).as_mut() - } - } else { - unsafe { - zend_hash_str_find(self, CString::new(key).ok()?.as_ptr(), key.len() as _) - .as_mut() - } - } - } + ArrayKey::String(key) => unsafe { + zend_hash_str_find( + self, + CString::new(key.as_str()).ok()?.as_ptr(), + key.len() as _, + ) + .as_mut() + }, + ArrayKey::Str(key) => unsafe { + zend_hash_str_find(self, CString::new(key).ok()?.as_ptr(), key.len() as _).as_mut() + }, } } @@ -393,34 +355,16 @@ impl ZendHashTable { #[allow(clippy::cast_sign_loss)] zend_hash_index_del(self, index as zend_ulong) }, - ArrayKey::String(key) => { - if let Ok(index) = i64::from_str(key.as_str()) { - #[allow(clippy::cast_sign_loss)] - unsafe { - zend_hash_index_del(self, index as zend_ulong) - } - } else { - unsafe { - zend_hash_str_del( - self, - CString::new(key.as_str()).ok()?.as_ptr(), - key.len() as _, - ) - } - } - } - ArrayKey::Str(key) => { - if let Ok(index) = i64::from_str(key) { - #[allow(clippy::cast_sign_loss)] - unsafe { - zend_hash_index_del(self, index as zend_ulong) - } - } else { - unsafe { - zend_hash_str_del(self, CString::new(key).ok()?.as_ptr(), key.len() as _) - } - } - } + ArrayKey::String(key) => unsafe { + zend_hash_str_del( + self, + CString::new(key.as_str()).ok()?.as_ptr(), + key.len() as _, + ) + }, + ArrayKey::Str(key) => unsafe { + zend_hash_str_del(self, CString::new(key).ok()?.as_ptr(), key.len() as _) + }, }; if result < 0 { @@ -510,38 +454,19 @@ impl ZendHashTable { }; } ArrayKey::String(key) => { - if let Ok(index) = i64::from_str(&key) { - unsafe { - #[allow(clippy::cast_sign_loss)] - zend_hash_index_update(self, index as zend_ulong, &raw mut val) - }; - } else { - unsafe { - zend_hash_str_update( - self, - CString::new(key.as_str())?.as_ptr(), - key.len(), - &raw mut val, - ) - }; - } + unsafe { + zend_hash_str_update( + self, + CString::new(key.as_str())?.as_ptr(), + key.len(), + &raw mut val, + ) + }; } ArrayKey::Str(key) => { - if let Ok(index) = i64::from_str(key) { - unsafe { - #[allow(clippy::cast_sign_loss)] - zend_hash_index_update(self, index as zend_ulong, &raw mut val) - }; - } else { - unsafe { - zend_hash_str_update( - self, - CString::new(key)?.as_ptr(), - key.len(), - &raw mut val, - ) - }; - } + unsafe { + zend_hash_str_update(self, CString::new(key)?.as_ptr(), key.len(), &raw mut val) + }; } } val.release(); diff --git a/tests/src/integration/array/array.php b/tests/src/integration/array/array.php index 23cc5b06c..f3e102cb6 100644 --- a/tests/src/integration/array/array.php +++ b/tests/src/integration/array/array.php @@ -42,6 +42,16 @@ 2 => '2', '3' => '3', ]); +$assoc_keys = test_btree_map([ + 'a' => '1', + 2 => '2', + '3' => '3', +]); +assert($assoc_keys === [ + 2 => '2', + '3' => '3', + 'a' => '1', +]); $assoc_keys = test_array_assoc_array_keys(['foo', 'bar', 'baz']); assert($assoc_keys === [ @@ -49,3 +59,8 @@ 1 => 'bar', 2 => 'baz', ]); +assert(test_btree_map(['foo', 'bar', 'baz']) === [ + 0 => 'foo', + 1 => 'bar', + 2 => 'baz', +]); diff --git a/tests/src/integration/array/mod.rs b/tests/src/integration/array/mod.rs index d95658eed..b8922a176 100644 --- a/tests/src/integration/array/mod.rs +++ b/tests/src/integration/array/mod.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use ext_php_rs::{ convert::IntoZval, @@ -24,6 +24,11 @@ pub fn test_array_assoc_array_keys(a: Vec<(ArrayKey, String)>) -> Vec<(ArrayKey, a } +#[php_function] +pub fn test_btree_map(a: BTreeMap) -> BTreeMap { + a +} + #[php_function] pub fn test_array_keys() -> Zval { let mut ht = HashTable::new(); @@ -41,6 +46,7 @@ pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { .function(wrap_function!(test_array)) .function(wrap_function!(test_array_assoc)) .function(wrap_function!(test_array_assoc_array_keys)) + .function(wrap_function!(test_btree_map)) .function(wrap_function!(test_array_keys)) }