diff --git a/src/types/array/array_key.rs b/src/types/array/array_key.rs index c321f84ac..033fc3ea0 100644 --- a/src/types/array/array_key.rs +++ b/src/types/array/array_key.rs @@ -3,7 +3,7 @@ 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, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ArrayKey<'a> { /// A numerical key. /// In Zend API it's represented by `u64` (`zend_ulong`), so the value needs @@ -92,12 +92,38 @@ impl<'a> From<&'a str> for ArrayKey<'a> { } } +impl<'a> From for ArrayKey<'a> { + fn from(index: i32) -> ArrayKey<'a> { + ArrayKey::Long(i64::from(index)) + } +} + impl<'a> From for ArrayKey<'a> { fn from(index: i64) -> ArrayKey<'a> { ArrayKey::Long(index) } } +impl<'a> From for ArrayKey<'a> { + fn from(index: u64) -> ArrayKey<'a> { + if let Ok(index) = i64::try_from(index) { + ArrayKey::Long(index) + } else { + ArrayKey::String(index.to_string()) + } + } +} + +impl<'a> From for ArrayKey<'a> { + fn from(index: usize) -> ArrayKey<'a> { + if let Ok(index) = i64::try_from(index) { + ArrayKey::Long(index) + } else { + ArrayKey::String(index.to_string()) + } + } +} + impl<'a> FromZval<'a> for ArrayKey<'_> { const TYPE: DataType = DataType::String; diff --git a/src/types/array/conversions/btree_map.rs b/src/types/array/conversions/btree_map.rs index 0c1234d24..8abc6d1db 100644 --- a/src/types/array/conversions/btree_map.rs +++ b/src/types/array/conversions/btree_map.rs @@ -122,7 +122,7 @@ mod tests { use crate::types::{ArrayKey, ZendHashTable, Zval}; #[test] - fn test_hash_table_try_from_btree_mab() { + fn test_hash_table_try_from_btree_map() { Embed::run(|| { let mut map = BTreeMap::new(); map.insert("key1", "value1"); diff --git a/src/types/array/conversions/btree_set.rs b/src/types/array/conversions/btree_set.rs new file mode 100644 index 000000000..4c031ad54 --- /dev/null +++ b/src/types/array/conversions/btree_set.rs @@ -0,0 +1,114 @@ +use super::super::ZendHashTable; +use crate::{ + boxed::ZBox, + convert::{FromZval, IntoZval}, + error::{Error, Result}, + flags::DataType, + types::Zval, +}; +use std::collections::BTreeSet; +use std::convert::TryFrom; + +impl<'a, V> TryFrom<&'a ZendHashTable> for BTreeSet +where + V: FromZval<'a> + Ord, +{ + type Error = Error; + + fn try_from(value: &'a ZendHashTable) -> Result { + let mut set = Self::new(); + + for (_key, val) in value { + set.insert(V::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?); + } + + Ok(set) + } +} + +impl TryFrom> for ZBox +where + V: IntoZval, +{ + type Error = Error; + + fn try_from(value: BTreeSet) -> Result { + let mut set = ZendHashTable::with_capacity( + value.len().try_into().map_err(|_| Error::IntegerOverflow)?, + ); + + for (k, v) in value.into_iter().enumerate() { + set.insert(k, v)?; + } + + Ok(set) + } +} + +impl<'a, V> FromZval<'a> for BTreeSet +where + V: FromZval<'a> + Ord, +{ + const TYPE: DataType = DataType::Array; + + fn from_zval(zval: &'a Zval) -> Option { + zval.array().and_then(|arr| arr.try_into().ok()) + } +} + +impl IntoZval for BTreeSet +where + 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(()) + } +} + +#[cfg(test)] +#[cfg(feature = "embed")] +#[allow(clippy::unwrap_used)] +mod tests { + use std::collections::BTreeSet; + + use crate::boxed::ZBox; + use crate::convert::FromZval; + use crate::embed::Embed; + use crate::types::{ZendHashTable, Zval}; + + #[test] + fn test_hash_table_try_from_btree_set() { + Embed::run(|| { + let mut set = BTreeSet::new(); + set.insert("one"); + let ht: ZBox = set.try_into().unwrap(); + assert_eq!(ht.len(), 1); + assert!(ht.get(0).is_some()); + }); + } + + #[test] + fn test_btree_set_try_from_hash_table() { + Embed::run(|| { + let mut ht = ZendHashTable::new(); + ht.insert(0, "value1").unwrap(); + ht.insert(1, "value2").unwrap(); + ht.insert(2, "value3").unwrap(); + let mut zval = Zval::new(); + zval.set_hashtable(ht); + + let map = BTreeSet::::from_zval(&zval).unwrap(); + assert_eq!(map.len(), 3); + let mut it = map.iter(); + assert_eq!(it.next().unwrap(), "value1"); + assert_eq!(it.next().unwrap(), "value2"); + assert_eq!(it.next().unwrap(), "value3"); + assert_eq!(it.next(), None); + }); + } +} diff --git a/src/types/array/conversions/hash_map.rs b/src/types/array/conversions/hash_map.rs index bce907810..e4399c26c 100644 --- a/src/types/array/conversions/hash_map.rs +++ b/src/types/array/conversions/hash_map.rs @@ -32,11 +32,11 @@ where } } -impl TryFrom> for ZBox +impl<'a, K, V, H> TryFrom> for ZBox where - K: AsRef, + K: Into>, V: IntoZval, - H: BuildHasher, + H: BuildHasher + Default, { type Error = Error; @@ -46,33 +46,29 @@ where ); for (k, v) in value { - ht.insert(k.as_ref(), v)?; + ht.insert(k.into(), v)?; } Ok(ht) } } -impl IntoZval for HashMap +impl<'a, K, V, H> FromZval<'a> for HashMap where - K: AsRef, - V: IntoZval, - H: BuildHasher, + K: TryFrom, Error = Error> + Hash + Eq, + V: FromZval<'a>, + H: BuildHasher + Default, { 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(()) + fn from_zval(zval: &'a Zval) -> Option { + zval.array().and_then(|arr| arr.try_into().ok()) } } -impl<'a, V, H> FromZval<'a> for HashMap +impl<'a, V> FromZval<'a> for HashMap, V> where V: FromZval<'a>, - H: BuildHasher + Default, { const TYPE: DataType = DataType::Array; @@ -80,3 +76,234 @@ where zval.array().and_then(|arr| arr.try_into().ok()) } } + +impl<'a, V, H> TryFrom<&'a ZendHashTable> for HashMap, V, H> +where + V: FromZval<'a>, + H: BuildHasher + Default, +{ + type Error = Error; + + fn try_from(value: &'a ZendHashTable) -> Result { + let mut map = Self::default(); + + 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, H> IntoZval for HashMap +where + K: Into>, + V: IntoZval, + H: BuildHasher + Default, +{ + 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(()) + } +} + +#[cfg(test)] +#[cfg(feature = "embed")] +#[allow(clippy::unwrap_used)] +mod tests { + use std::collections::HashMap; + + 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_hashmap_map() { + Embed::run(|| { + let mut map = HashMap::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 = HashMap::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_hashmap_map_into_zval() { + Embed::run(|| { + let mut map = HashMap::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 = HashMap::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_hashmap_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 = HashMap::::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 = HashMap::::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 = HashMap::::from_zval(&zval_mixed); + assert!(map_mixed.is_some()); + }); + } + + #[test] + fn test_hashmap_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 = HashMap::::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_hashmap_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: HashMap = 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_hashmap_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: HashMap = 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_hashmap_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: HashMap = 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_set.rs b/src/types/array/conversions/hash_set.rs new file mode 100644 index 000000000..a1e30cf94 --- /dev/null +++ b/src/types/array/conversions/hash_set.rs @@ -0,0 +1,117 @@ +use super::super::ZendHashTable; +use crate::{ + boxed::ZBox, + convert::{FromZval, IntoZval}, + error::{Error, Result}, + flags::DataType, + types::Zval, +}; +use std::collections::HashSet; +use std::convert::TryFrom; +use std::hash::{BuildHasher, Hash}; + +impl<'a, V, H> TryFrom<&'a ZendHashTable> for HashSet +where + V: FromZval<'a> + Eq + Hash, + H: BuildHasher + Default, +{ + type Error = Error; + + fn try_from(value: &'a ZendHashTable) -> Result { + let mut set = Self::with_capacity_and_hasher(value.len(), H::default()); + + for (_key, val) in value { + set.insert(V::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?); + } + + Ok(set) + } +} + +impl TryFrom> for ZBox +where + V: IntoZval, + H: BuildHasher, +{ + type Error = Error; + + fn try_from(value: HashSet) -> Result { + let mut ht = ZendHashTable::with_capacity( + value.len().try_into().map_err(|_| Error::IntegerOverflow)?, + ); + + for (k, v) in value.into_iter().enumerate() { + ht.insert(k, v)?; + } + + Ok(ht) + } +} + +impl<'a, V, H> FromZval<'a> for HashSet +where + V: FromZval<'a> + Eq + Hash, + H: BuildHasher + Default, +{ + const TYPE: DataType = DataType::Array; + + fn from_zval(zval: &'a Zval) -> Option { + zval.array().and_then(|arr| arr.try_into().ok()) + } +} + +impl IntoZval for HashSet +where + V: IntoZval, + H: BuildHasher, +{ + 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(()) + } +} + +#[cfg(test)] +#[cfg(feature = "embed")] +#[allow(clippy::unwrap_used)] +mod tests { + use std::collections::HashSet; + + use crate::boxed::ZBox; + use crate::convert::FromZval; + use crate::embed::Embed; + use crate::types::{ZendHashTable, Zval}; + + #[test] + fn test_hash_table_try_from_hash_set() { + Embed::run(|| { + let mut set = HashSet::new(); + set.insert("one"); + let ht: ZBox = set.try_into().unwrap(); + assert_eq!(ht.len(), 1); + assert!(ht.get(0).is_some()); + }); + } + + #[test] + fn test_hash_set_try_from_hash_table() { + Embed::run(|| { + let mut ht = ZendHashTable::new(); + ht.insert(0, "value1").unwrap(); + ht.insert(1, "value2").unwrap(); + ht.insert(2, "value3").unwrap(); + let mut zval = Zval::new(); + zval.set_hashtable(ht); + + let map = HashSet::::from_zval(&zval).unwrap(); + assert_eq!(map.len(), 3); + assert!(map.contains("value1")); + assert!(map.contains("value2")); + assert!(map.contains("value3")); + }); + } +} diff --git a/src/types/array/conversions/mod.rs b/src/types/array/conversions/mod.rs index 10b9685b8..fbcc4c7f0 100644 --- a/src/types/array/conversions/mod.rs +++ b/src/types/array/conversions/mod.rs @@ -7,9 +7,13 @@ //! ## Supported Collections //! //! - `BTreeMap` ↔ `ZendHashTable` (via `btree_map` module) +//! - `BTreeSet` ↔ `ZendHashTable` (via `btree_set` module) //! - `HashMap` ↔ `ZendHashTable` (via `hash_map` module) +//! - `HashSet` ↔ `ZendHashTable` (via `hash_set` module) //! - `Vec` and `Vec<(K, V)>` ↔ `ZendHashTable` (via `vec` module) mod btree_map; +mod btree_set; mod hash_map; +mod hash_set; mod vec; diff --git a/src/types/long.rs b/src/types/long.rs index 6d11f7e03..6f50a1bec 100644 --- a/src/types/long.rs +++ b/src/types/long.rs @@ -7,7 +7,6 @@ use crate::{ error::{Error, Result}, ffi::zend_long, flags::DataType, - macros::{into_zval, try_from_zval}, types::Zval, }; diff --git a/src/types/mod.rs b/src/types/mod.rs index 3d209dc5a..ffbf52bf3 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -23,7 +23,7 @@ pub use object::{PropertyQuery, ZendObject}; pub use string::ZendStr; pub use zval::Zval; -use crate::{convert::FromZval, flags::DataType, macros::into_zval}; +use crate::{convert::FromZval, flags::DataType}; into_zval!(f32, set_double, Double); into_zval!(f64, set_double, Double); diff --git a/src/types/string.rs b/src/types/string.rs index 9d8bb0657..fd88ccd3c 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -21,7 +21,6 @@ use crate::{ zend_string_init_interned, }, flags::DataType, - macros::try_from_zval, types::Zval, };