Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 18 additions & 11 deletions src/types/array/array_key.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -17,26 +17,30 @@ pub enum ArrayKey<'a> {

impl From<String> 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<ArrayKey<'_>> for String {
type Error = Error;

fn try_from(value: ArrayKey<'_>) -> std::result::Result<Self, Self::Error> {
fn try_from(value: ArrayKey<'_>) -> Result<Self, Self::Error> {
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()),
}
}
}

impl TryFrom<ArrayKey<'_>> for i64 {
type Error = Error;

fn try_from(value: ArrayKey<'_>) -> std::result::Result<Self, Self::Error> {
fn try_from(value: ArrayKey<'_>) -> Result<Self, Self::Error> {
match value {
ArrayKey::Long(i) => Ok(i),
ArrayKey::String(s) => s.parse::<i64>().map_err(|_| Error::InvalidProperty),
Expand Down Expand Up @@ -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)
}
}
}

Expand Down Expand Up @@ -117,8 +125,7 @@ mod tests {

let key = ArrayKey::Long(42);
let result: crate::error::Result<String, _> = 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<String, _> = key.try_into();
Expand Down
304 changes: 304 additions & 0 deletions src/types/array/conversions/btree_map.rs
Original file line number Diff line number Diff line change
@@ -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<K, V>
where
K: TryFrom<ArrayKey<'a>, Error = Error> + Ord,
V: FromZval<'a>,
{
type Error = Error;

fn try_from(value: &'a ZendHashTable) -> Result<Self> {
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<ArrayKey<'a>, V>
where
V: FromZval<'a>,
{
type Error = Error;

fn try_from(value: &'a ZendHashTable) -> Result<Self> {
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<BTreeMap<K, V>> for ZBox<ZendHashTable>
where
K: Into<ArrayKey<'a>>,
V: IntoZval,
{
type Error = Error;

fn try_from(value: BTreeMap<K, V>) -> Result<Self> {
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<K, V>
where
K: Into<ArrayKey<'a>>,
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<K, V>
where
K: TryFrom<ArrayKey<'a>, Error = Error> + Ord,
V: FromZval<'a>,
{
const TYPE: DataType = DataType::Array;

fn from_zval(zval: &'a Zval) -> Option<Self> {
zval.array().and_then(|arr| arr.try_into().ok())
}
}

impl<'a, V> FromZval<'a> for BTreeMap<ArrayKey<'a>, V>
where
V: FromZval<'a>,
{
const TYPE: DataType = DataType::Array;

fn from_zval(zval: &'a Zval) -> Option<Self> {
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<ZendHashTable> = 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<ZendHashTable> = 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::<String, String>::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::<i64, String>::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::<String, String>::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::<ArrayKey, String>::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<i64, String> = 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<BTreeMap<i64, String>> = 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<String, String> = 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<BTreeMap<String, String>> = 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<ArrayKey, String> = 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");
});
}
}
Loading
Loading