diff --git a/godot-core/src/builtin/arrays.rs b/godot-core/src/builtin/arrays.rs index e14c9bf85..383e90fb4 100644 --- a/godot-core/src/builtin/arrays.rs +++ b/godot-core/src/builtin/arrays.rs @@ -559,6 +559,7 @@ impl_builtin_traits! { for Array { Default => array_construct_default; Drop => array_destroy; + PartialEq => array_operator_equal; } } diff --git a/godot-core/src/builtin/dictionary.rs b/godot-core/src/builtin/dictionary.rs new file mode 100644 index 000000000..ddc05fba7 --- /dev/null +++ b/godot-core/src/builtin/dictionary.rs @@ -0,0 +1,355 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use godot_ffi as sys; + +use crate::builtin::{inner, FromVariant, ToVariant, Variant, VariantConversionError}; +use crate::obj::Share; +use std::collections::{HashMap, HashSet}; +use std::fmt; +use sys::types::*; +use sys::{ffi_methods, interface_fn, GodotFfi}; + +use super::Array; + +/// Godot's `Dictionary` type. +/// +/// The keys and values of the array are all `Variant`, so they can all be of different types. +/// Variants are designed to be generally cheap to clone. +/// +/// # Thread safety +/// +/// The same principles apply as for [`Array`]. Consult its documentation for details. +#[repr(C)] +pub struct Dictionary { + opaque: OpaqueDictionary, +} + +impl Dictionary { + fn from_opaque(opaque: OpaqueDictionary) -> Self { + Self { opaque } + } + + /// Constructs an empty `Dictionary`. + pub fn new() -> Self { + Self::default() + } + + /// Removes all key-value pairs from the dictionary. Equivalent to `clear` in Godot. + pub fn clear(&mut self) { + self.as_inner().clear() + } + + /// Returns a deep copy of the dictionary. All nested arrays and dictionaries are duplicated and + /// will not be shared with the original dictionary. Note that any `Object`-derived elements will + /// still be shallow copied. + /// + /// To create a shallow copy, use [`duplicate_shallow()`] instead. To create a new reference to + /// the same array data, use [`share()`]. + /// + /// Equivalent to `dictionary.duplicate(true)` in Godot. + pub fn duplicate_deep(&self) -> Self { + self.as_inner().duplicate(true) + } + + /// Returns a shallow copy of the dictionary. All dictionary keys and values are copied, but + /// any reference types (such as `Array`, `Dictionary` and `Object`) will still refer to the + /// same value. + /// + /// To create a deep copy, use [`duplicate_deep()`] instead. To create a new reference to the + /// same dictionary data, use [`share()`]. + /// + /// Equivalent to `dictionary.duplicate(false)` in Godot. + pub fn duplicate_shallow(&self) -> Self { + self.as_inner().duplicate(false) + } + + /// Removes a key from the map, and returns the value associated with + /// the key if the key was in the dictionary. + pub fn remove(&mut self, key: K) -> Option { + let key = key.to_variant(); + let old_value = self.get(key.clone()); + self.as_inner().erase(key); + old_value + } + + /// Returns the first key whose associated value is `value`, if one exists. + /// + /// Unlike in Godot, this will return `None` if the key does not exist + /// and `Some(nil)` the key is `null`. + pub fn find_key_by_value(&self, value: V) -> Option { + let key = self.as_inner().find_key(value.to_variant()); + + if !key.is_nil() || self.contains_key(key.clone()) { + Some(key) + } else { + None + } + } + + /// Returns the value at the key in the dictionary, if there is + /// one. + /// + /// Unlike `get` in Godot, this will return `None` if there is + /// no value with the given key. + pub fn get(&self, key: K) -> Option { + let key = key.to_variant(); + if !self.contains_key(key.clone()) { + return None; + } + + Some(self.get_or_nil(key)) + } + + /// Returns the value at the key in the dictionary, or nil otherwise. This + /// method does not let you differentiate `NIL` values stored as values from + /// absent keys. If you need that, use `get()`. + /// + /// This is equivalent to `get` in Godot. + pub fn get_or_nil(&self, key: K) -> Variant { + self.as_inner().get(key.to_variant(), Variant::nil()) + } + + /// Returns `true` if the dictionary contains the given key. + /// + /// This is equivalent to `has` in Godot. + pub fn contains_key(&self, key: K) -> bool { + let key = key.to_variant(); + self.as_inner().has(key) + } + + /// Returns `true` if the dictionary contains all the given keys. + /// + /// This is equivalent to `has_all` in Godot. + pub fn contains_all_keys(&self, keys: Array) -> bool { + self.as_inner().has_all(keys) + } + + /// Returns a 32-bit integer hash value representing the dictionary and its contents. + pub fn hash(&self) -> u32 { + self.as_inner().hash().try_into().unwrap() + } + + /// Creates a new `Array` containing all the keys currently in the dictionary. + pub fn keys(&self) -> Array { + self.as_inner().keys() + } + + /// Creates a new `Array` containing all the values currently in the dictionary. + pub fn values(&self) -> Array { + self.as_inner().values() + } + + /// Returns true if the dictionary is empty. + pub fn is_empty(&self) -> bool { + self.as_inner().is_empty() + } + + /// Copies all keys and values from other into self. + /// + /// If overwrite is true, it will overwrite pre-existing keys. Otherwise + /// it will not. + /// + /// This is equivalent to `merge` in Godot. + pub fn extend_dictionary(&mut self, other: Self, overwrite: bool) { + self.as_inner().merge(other, overwrite) + } + + /// Returns the number of entries in the dictionary. + /// + /// This is equivalent to `size` in Godot. + pub fn len(&self) -> usize { + self.as_inner().size().try_into().unwrap() + } + + /// Get the pointer corresponding to the given key in the dictionary, + /// if there exists no value at the given key then a new one is created + /// and initialized to a nil variant. + fn get_ptr_mut(&mut self, key: K) -> *mut Variant { + let key = key.to_variant(); + unsafe { + let ptr = + (interface_fn!(dictionary_operator_index))(self.sys_mut(), key.var_sys_const()); + assert!(!ptr.is_null()); + ptr as *mut Variant + } + } + + /// Insert a value at the given key, returning the value + /// that previously was at that key if there was one. + pub fn insert(&mut self, key: K, value: V) -> Option { + let key = key.to_variant(); + let old_value = self.get(key.clone()); + self.set(key, value); + old_value + } + + /// Set a key to a given value. + pub fn set(&mut self, key: K, value: V) { + let key = key.to_variant(); + unsafe { + *self.get_ptr_mut(key) = value.to_variant(); + } + } + + #[doc(hidden)] + pub fn as_inner(&self) -> inner::InnerDictionary { + inner::InnerDictionary::from_outer(self) + } +} + +/// Creates a Dictionary from the given iterator I over a (&K, &V) key-value pair. +/// Each key and value are converted to a Variant. +impl<'a, 'b, K, V, I> From for Dictionary +where + I: IntoIterator, + K: ToVariant + 'a, + V: ToVariant + 'b, +{ + fn from(iterable: I) -> Self { + iterable + .into_iter() + .map(|(key, value)| (key.to_variant(), value.to_variant())) + .collect() + } +} + +/// Convert this dictionary to a strongly typed rust `HashMap`. If the conversion +/// fails for any key or value, an error is returned. +/// +/// Will be replaced by a proper iteration implementation. +impl TryFrom<&Dictionary> for HashMap { + type Error = VariantConversionError; + + fn try_from(dictionary: &Dictionary) -> Result { + // TODO: try to panic or something if modified while iterating + // Though probably better to fix when implementing iteration proper + dictionary + .keys() + .iter_shared() + .zip(dictionary.values().iter_shared()) + .map(|(key, value)| Ok((K::try_from_variant(&key)?, V::try_from_variant(&value)?))) + .collect() + } +} + +/// Convert the keys of this dictionary to a strongly typed rust `HashSet`. If the +/// conversion fails for any key, an error is returned. +impl TryFrom<&Dictionary> for HashSet { + type Error = VariantConversionError; + + fn try_from(dictionary: &Dictionary) -> Result { + // TODO: try to panic or something if modified while iterating + // Though probably better to fix when implementing iteration proper + dictionary + .keys() + .iter_shared() + .map(|key| K::try_from_variant(&key)) + .collect() + } +} + +/// Inserts all key-values from the iterator into the dictionary, +/// replacing values with existing keys with new values returned +/// from the iterator. +impl Extend<(K, V)> for Dictionary { + fn extend>(&mut self, iter: I) { + for (k, v) in iter.into_iter() { + self.set(k.to_variant(), v.to_variant()) + } + } +} + +impl FromIterator<(K, V)> for Dictionary { + fn from_iter>(iter: I) -> Self { + let mut dict = Dictionary::new(); + dict.extend(iter); + dict + } +} + +impl_builtin_traits! { + for Dictionary { + Default => dictionary_construct_default; + Drop => dictionary_destroy; + PartialEq => dictionary_operator_equal; + } +} + +impl GodotFfi for Dictionary { + ffi_methods! { + type sys::GDExtensionTypePtr = *mut Opaque; + fn from_sys; + fn sys; + fn write_sys; + } + + unsafe fn from_sys_init(init_fn: impl FnOnce(sys::GDExtensionTypePtr)) -> Self { + // Can't use uninitialized pointer -- Dictionary CoW implementation in C++ expects that on + // assignment, the target CoW pointer is either initialized or nullptr + + let mut result = Self::default(); + init_fn(result.sys_mut()); + result + } +} + +impl fmt::Debug for Dictionary { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.to_variant().stringify()) + } +} + +/// Creates a new reference to the data in this dictionary. Changes to the original dictionary will be +/// reflected in the copy and vice versa. +/// +/// To create a (mostly) independent copy instead, see [`Dictionary::duplicate_shallow()`] and +/// [`Dictionary::duplicate_deep()`]. +impl Share for Dictionary { + fn share(&self) -> Self { + unsafe { + Self::from_sys_init(|self_ptr| { + let ctor = sys::builtin_fn!(dictionary_construct_copy); + let args = [self.sys_const()]; + ctor(self_ptr, args.as_ptr()); + }) + } + } +} + +/// Creates a new dictionary with the given keys and values, the syntax mirrors +/// Godot's dictionary creation syntax. +/// +/// Any value can be used as a key, but to use an expression you need to surround it +/// in `()` or `{}`. +/// +/// Example +/// ```no_run +/// use godot::builtin::dict; +/// +/// let key = "my_key"; +/// let d = dict! { +/// "key1": 10, +/// "another": Variant::nil(), +/// key: true, +/// (1 + 2): "final", +/// } +/// ``` +#[macro_export] +macro_rules! dict { + ($($key:tt: $value:expr),* $(,)?) => { + { + let mut d = $crate::builtin::Dictionary::new(); + $( + // `cargo check` complains that `(1 + 2): true` has unused parens, even though it's not + // possible to omit the parens. + #[allow(unused_parens)] + d.set($key, $value); + )* + d + } + }; +} diff --git a/godot-core/src/builtin/mod.rs b/godot-core/src/builtin/mod.rs index b1f6aa3d7..30d2a8044 100644 --- a/godot-core/src/builtin/mod.rs +++ b/godot-core/src/builtin/mod.rs @@ -37,6 +37,7 @@ mod vector_macros; mod arrays; mod color; +mod dictionary; mod node_path; mod others; mod packed_array; @@ -52,8 +53,11 @@ mod vector4i; pub mod meta; +pub use crate::dict; + pub use arrays::*; pub use color::*; +pub use dictionary::*; pub use node_path::*; pub use others::*; pub use packed_array::*; diff --git a/godot-core/src/builtin/others.rs b/godot-core/src/builtin/others.rs index 76e16b215..894a02273 100644 --- a/godot-core/src/builtin/others.rs +++ b/godot-core/src/builtin/others.rs @@ -25,7 +25,6 @@ impl_builtin_stub!(Projection, OpaqueProjection); impl_builtin_stub!(Rid, OpaqueRid); impl_builtin_stub!(Callable, OpaqueCallable); impl_builtin_stub!(Signal, OpaqueSignal); -impl_builtin_stub!(Dictionary, OpaqueDictionary); #[repr(C)] struct InnerRect { diff --git a/godot-core/src/builtin/string.rs b/godot-core/src/builtin/string.rs index 013446ab0..e74322904 100644 --- a/godot-core/src/builtin/string.rs +++ b/godot-core/src/builtin/string.rs @@ -10,6 +10,8 @@ use godot_ffi as sys; use sys::types::OpaqueString; use sys::{ffi_methods, interface_fn, GodotFfi}; +use super::{FromVariant, ToVariant, Variant, VariantConversionError}; + #[repr(C, align(8))] pub struct GodotString { opaque: OpaqueString, @@ -140,6 +142,24 @@ impl fmt::Debug for GodotString { } } +impl ToVariant for &str { + fn to_variant(&self) -> Variant { + GodotString::from(*self).to_variant() + } +} + +impl ToVariant for String { + fn to_variant(&self) -> Variant { + GodotString::from(self).to_variant() + } +} + +impl FromVariant for String { + fn try_from_variant(variant: &Variant) -> Result { + Ok(GodotString::try_from_variant(variant)?.to_string()) + } +} + // While this is a nice optimisation for ptrcalls, it's not easily possible // to pass in &GodotString when doing varcalls. /* diff --git a/godot-core/src/builtin/variant/impls.rs b/godot-core/src/builtin/variant/impls.rs index 1bb29a83f..b16fc892b 100644 --- a/godot-core/src/builtin/variant/impls.rs +++ b/godot-core/src/builtin/variant/impls.rs @@ -133,6 +133,7 @@ mod impls { use super::*; impl_variant_traits!(bool, bool_to_variant, bool_from_variant, Bool); + impl_variant_traits!(Dictionary, dictionary_to_variant, dictionary_from_variant, Dictionary); impl_variant_traits!(Vector2, vector2_to_variant, vector2_from_variant, Vector2); impl_variant_traits!(Vector3, vector3_to_variant, vector3_from_variant, Vector3); impl_variant_traits!(Vector4, vector4_to_variant, vector4_from_variant, Vector4); diff --git a/itest/rust/src/dictionary_test.rs b/itest/rust/src/dictionary_test.rs new file mode 100644 index 000000000..2ea9d85a5 --- /dev/null +++ b/itest/rust/src/dictionary_test.rs @@ -0,0 +1,402 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use std::collections::{HashMap, HashSet}; + +use crate::itest; +use godot::{ + builtin::{dict, Dictionary, FromVariant, ToVariant}, + prelude::{Share, Variant}, +}; + +pub fn run() -> bool { + let mut ok = true; + ok &= dictionary_default(); + ok &= dictionary_new(); + ok &= dictionary_from_iterator(); + ok &= dictionary_from(); + ok &= dictionary_macro(); + ok &= dictionary_try_to_hashmap(); + ok &= dictionary_try_to_hashset(); + ok &= dictionary_clone(); + ok &= dictionary_duplicate_deep(); + ok &= dictionary_hash(); + ok &= dictionary_get(); + ok &= dictionary_insert(); + ok &= dictionary_insert_multiple(); + ok &= dictionary_insert_long(); + ok &= dictionary_extend(); + ok &= dictionary_remove(); + ok &= dictionary_clear(); + ok &= dictionary_find_key(); + ok &= dictionary_contains_keys(); + ok &= dictionary_keys_values(); + ok &= dictionary_equal(); + ok +} + +#[itest] +fn dictionary_default() { + assert_eq!(Dictionary::default().len(), 0); +} + +#[itest] +fn dictionary_new() { + assert_eq!(Dictionary::new().len(), 0); +} + +#[itest] +fn dictionary_from_iterator() { + let dictionary = Dictionary::from_iter([("foo", 1), ("bar", 2)]); + + assert_eq!(dictionary.len(), 2); + assert_eq!(dictionary.get("foo"), Some(1.to_variant()), "key = \"foo\""); + assert_eq!(dictionary.get("bar"), Some(2.to_variant()), "key = \"bar\""); + + let dictionary = Dictionary::from_iter([(1, "foo"), (2, "bar")]); + + assert_eq!(dictionary.len(), 2); + assert_eq!(dictionary.get(1), Some("foo".to_variant()), "key = 1"); + assert_eq!(dictionary.get(2), Some("bar".to_variant()), "key = 2"); +} + +#[itest] +fn dictionary_from() { + let dictionary = Dictionary::from(&HashMap::from([("foo", 1), ("bar", 2)])); + + assert_eq!(dictionary.len(), 2); + assert_eq!(dictionary.get("foo"), Some(1.to_variant()), "key = \"foo\""); + assert_eq!(dictionary.get("bar"), Some(2.to_variant()), "key = \"bar\""); + + let dictionary = Dictionary::from(&HashMap::from([(1, "foo"), (2, "bar")])); + + assert_eq!(dictionary.len(), 2); + assert_eq!(dictionary.get(1), Some("foo".to_variant()), "key = \"foo\""); + assert_eq!(dictionary.get(2), Some("bar".to_variant()), "key = \"bar\""); +} + +#[itest] +fn dictionary_macro() { + let dictionary = dict! { + "foo": 0, + "bar": true, + "baz": "foobar" + }; + + assert_eq!(dictionary.len(), 3); + assert_eq!(dictionary.get("foo"), Some(0.to_variant()), "key = \"foo\""); + assert_eq!( + dictionary.get("bar"), + Some(true.to_variant()), + "key = \"bar\"" + ); + assert_eq!( + dictionary.get("baz"), + Some("foobar".to_variant()), + "key = \"baz\"" + ); + + let empty = dict!(); + assert!(empty.is_empty()); + + let foo = "foo"; + let dict_complex = dict! { + foo: 10, + "bar": true, + (1 + 2): Variant::nil(), + }; + assert_eq!(dict_complex.get("foo"), Some(10.to_variant())); + assert_eq!(dict_complex.get("bar"), Some(true.to_variant())); + assert_eq!(dict_complex.get(3), Some(Variant::nil())); +} + +#[itest] +fn dictionary_try_to_hashmap() { + let dictionary = dict! { + "foo": 0, + "bar": 1, + "baz": 2 + }; + + assert_eq!( + HashMap::::try_from(&dictionary), + Ok(HashMap::from([ + ("foo".into(), 0), + ("bar".into(), 1), + ("baz".into(), 2) + ])) + ); +} + +#[itest] +fn dictionary_try_to_hashset() { + let dictionary = dict! { + "foo": true, + "bar": true, + "baz": true + }; + + assert_eq!( + HashSet::::try_from(&dictionary), + Ok(HashSet::from(["foo".into(), "bar".into(), "baz".into()])) + ); +} + +#[itest] +fn dictionary_clone() { + let subdictionary = dict! { + "baz": true, + "foobar": false + }; + let dictionary = dict! { + "foo": 0, + "bar": subdictionary.share() + }; + #[allow(clippy::redundant_clone)] + let clone = dictionary.share(); + Dictionary::try_from_variant(&clone.get("bar").unwrap()) + .unwrap() + .insert("final", 4); + assert_eq!(subdictionary.get("final"), Some(4.to_variant())); +} + +#[itest] +fn dictionary_hash() { + let dictionary = dict! { + "foo": 0, + "bar": true, + "baz": "foobar" + }; + dictionary.hash(); +} + +#[itest] +fn dictionary_duplicate_deep() { + let subdictionary = dict! { + "baz": true, + "foobar": false + }; + let dictionary = dict! { + "foo": 0, + "bar": subdictionary.share() + }; + let clone = dictionary.duplicate_deep(); + Dictionary::try_from_variant(&clone.get("bar").unwrap()) + .unwrap() + .insert("baz", 4); + assert_eq!( + subdictionary.get("baz"), + Some(true.to_variant()), + "key = \"baz\"" + ); +} + +#[itest] +fn dictionary_duplicate_shallow() { + let subdictionary = dict! { + "baz": true, + "foobar": false + }; + let dictionary = dict! { + "foo": 0, + "bar": subdictionary.share() + }; + let mut clone = dictionary.duplicate_shallow(); + Dictionary::try_from_variant(&clone.get("bar").unwrap()) + .unwrap() + .insert("baz", 4); + assert_eq!( + subdictionary.get("baz"), + Some(4.to_variant()), + "key = \"baz\"" + ); + clone.insert("foo", false.to_variant()); + assert_eq!(dictionary.get("foo"), Some(0.to_variant())); + assert_eq!(clone.get("foo"), Some(false.to_variant())); +} + +#[itest] +fn dictionary_get() { + let mut dictionary = dict! { + "foo": 0, + "bar": true, + "baz": "foobar", + "nil": Variant::nil(), + }; + + dictionary.insert("baz", "foobar"); + + assert_eq!(dictionary.get("foo"), Some(0.to_variant()), "key = \"foo\""); + assert_eq!( + dictionary.get("bar"), + Some(true.to_variant()), + "key = \"bar\"" + ); + assert_eq!(dictionary.get("baz"), Some("foobar".to_variant())); + assert_eq!(dictionary.get("nil"), Some(Variant::nil()), "key = \"nil\""); + assert_eq!(dictionary.get("missing"), None, "key = \"missing\""); + assert_eq!( + dictionary.get_or_nil("nil"), + Variant::nil(), + "key = \"nil\"" + ); + assert_eq!( + dictionary.get_or_nil("missing"), + Variant::nil(), + "key = \"missing\"" + ); + assert_eq!(dictionary.get("foobar"), None, "key = \"foobar\""); +} + +#[itest] +fn dictionary_insert() { + let mut dictionary = dict! { + "foo": 0, + "bar": 1, + }; + + assert_eq!(dictionary.insert("bar", 2), Some(1.to_variant())); + assert_eq!( + HashMap::::try_from(&dictionary), + Ok(HashMap::from([("foo".into(), 0), ("bar".into(), 2)])) + ); + assert_eq!(dictionary.insert("baz", 3), None); + assert_eq!( + HashMap::::try_from(&dictionary), + Ok(HashMap::from([ + ("foo".into(), 0), + ("bar".into(), 2), + ("baz".into(), 3) + ])) + ); +} + +#[itest] +fn dictionary_insert_multiple() { + let mut dictionary = dict! {}; + assert!(dictionary.is_empty()); + + dictionary.insert(1, true); + assert_eq!(dictionary.get(1), Some(true.to_variant())); + + let mut other = dict! {}; + assert!(other.is_empty()); + + other.insert(1, 2); + assert_eq!(other.get(1), Some(2.to_variant())); +} +#[itest] +fn dictionary_insert_long() { + let mut dictionary = dict! {}; + let old = dictionary.insert("abcdefghijklmnopqrstuvwxyz", "zabcdefghijklmnopqrstuvwxy"); + assert_eq!(old, None); + assert_eq!( + dictionary.get("abcdefghijklmnopqrstuvwxyz"), + Some("zabcdefghijklmnopqrstuvwxy".to_variant()) + ); +} + +#[itest] +fn dictionary_extend() { + let mut dictionary = dict! { + "foo": 0, + "bar": true, + }; + assert_eq!(dictionary.get("foo"), Some(0.to_variant())); + let other = dict! { + "bar": "new", + "baz": Variant::nil(), + }; + dictionary.extend_dictionary(other, false); + assert_eq!(dictionary.get("bar"), Some(true.to_variant())); + assert_eq!(dictionary.get("baz"), Some(Variant::nil())); + + let mut dictionary = dict! { + "bar": true, + }; + let other = dict! { + "bar": "new", + }; + dictionary.extend_dictionary(other, true); + assert_eq!(dictionary.get("bar"), Some("new".to_variant())); +} + +#[itest] +fn dictionary_remove() { + let mut dictionary = dict! { + "foo": 0, + }; + assert_eq!(dictionary.remove("foo"), Some(0.to_variant())); + assert!(!dictionary.contains_key("foo")); + assert!(dictionary.is_empty()); +} + +#[itest] +fn dictionary_clear() { + let mut dictionary = dict! { + "foo": 0, + "bar": true, + "baz": "foobar" + }; + + assert!(!dictionary.is_empty()); + dictionary.clear(); + assert!(dictionary.is_empty()); +} + +#[itest] +fn dictionary_find_key() { + let dictionary = dict! { + "foo": 0, + "bar": true, + }; + + assert_eq!(dictionary.find_key_by_value(0), Some("foo".to_variant())); + assert_eq!(dictionary.find_key_by_value(true), Some("bar".to_variant())); +} + +#[itest] +fn dictionary_contains_keys() { + use godot::prelude::Array; + let dictionary = dict! { + "foo": 0, + "bar": true, + }; + + assert!(dictionary.contains_key("foo"), "key = \"foo\""); + assert!(dictionary.contains_key("bar"), "key = \"bar\""); + assert!( + dictionary.contains_all_keys(Array::from(&["foo", "bar"])), + "keys = [\"foo\", \"bar\"]" + ); + assert!(!dictionary.contains_key("missing"), "key = \"missing\""); + assert!( + !dictionary.contains_all_keys(Array::from(&["foo", "bar", "missing"])), + "keys = [\"foo\", \"bar\", \"missing\"]" + ); +} + +#[itest] +fn dictionary_keys_values() { + use godot::prelude::Array; + let dictionary = dict! { + "foo": 0, + "bar": true, + }; + + assert_eq!(dictionary.keys(), Array::from(&["foo", "bar"])); + assert_eq!( + dictionary.values(), + Array::from(&[0.to_variant(), true.to_variant()]) + ); +} + +#[itest] +fn dictionary_equal() { + assert_eq!(dict! {"foo": "bar"}, dict! {"foo": "bar"}); + assert_eq!(dict! {1: f32::NAN}, dict! {1: f32::NAN}); // yes apparently godot considers these equal + assert_ne!(dict! {"foo": "bar"}, dict! {"bar": "foo"}); +} diff --git a/itest/rust/src/lib.rs b/itest/rust/src/lib.rs index 027873b3d..7d6dfb1d7 100644 --- a/itest/rust/src/lib.rs +++ b/itest/rust/src/lib.rs @@ -12,6 +12,7 @@ use std::panic::UnwindSafe; mod array_test; mod base_test; mod builtin_test; +mod dictionary_test; mod enum_test; mod export_test; mod gdscript_ffi_test; @@ -37,6 +38,7 @@ fn run_tests() -> bool { ok &= string_test::run(); ok &= array_test::run(); ok &= packed_array_test::run(); + ok &= dictionary_test::run(); ok &= utilities_test::run(); ok &= variant_test::run(); ok &= virtual_methods_test::run(); diff --git a/itest/rust/src/variant_test.rs b/itest/rust/src/variant_test.rs index 75700aad3..debd8760b 100644 --- a/itest/rust/src/variant_test.rs +++ b/itest/rust/src/variant_test.rs @@ -39,7 +39,6 @@ fn variant_nil() { fn variant_conversions() { roundtrip(false); roundtrip(true); - roundtrip(gstr("some string")); roundtrip(InstanceId::from_nonzero(-9223372036854775808i64)); // roundtrip(Some(InstanceId::from_nonzero(9223372036854775807i64))); // roundtrip(Option::::None); @@ -60,6 +59,13 @@ fn variant_conversions() { roundtrip(2147483647i32); roundtrip(-2147483648i32); roundtrip(9223372036854775807i64); + + // string + roundtrip(gstr("some string")); + roundtrip(String::from("some other string")); + let str_val = "abcdefghijklmnop"; + let back = String::from_variant(&str_val.to_variant()); + assert_eq!(str_val, back.as_str()); } #[itest]