diff --git a/v3/engine/src/execute/ir/query_root/node_field.rs b/v3/engine/src/execute/ir/query_root/node_field.rs index ecfb92c0dfc..9e06afa91df 100644 --- a/v3/engine/src/execute/ir/query_root/node_field.rs +++ b/v3/engine/src/execute/ir/query_root/node_field.rs @@ -16,6 +16,7 @@ use crate::metadata::resolved; use crate::metadata::resolved::subgraph::Qualified; use crate::schema::types::{GlobalID, NamespaceAnnotation, NodeFieldTypeNameMapping}; use crate::schema::GDS; +use crate::utils::HashMapWithJsonKey; /// IR for the 'select_one' operation on a model #[derive(Serialize, Debug)] @@ -39,8 +40,10 @@ pub struct NodeSelect<'n, 's> { fn get_relay_node_namespace_typename_mappings<'s>( field_call: &normalized_ast::FieldCall<'s, GDS>, -) -> Result<&'s HashMap, resolved::model::FilterPermission>, error::Error> -{ +) -> Result< + &'s HashMapWithJsonKey, resolved::model::FilterPermission>, + error::Error, +> { field_call .info .namespaced @@ -88,7 +91,7 @@ pub(crate) fn relay_node_ir<'n, 's>( let typename_permissions: &'s HashMap< Qualified, resolved::model::FilterPermission, - > = get_relay_node_namespace_typename_mappings(field_call)?; + > = &get_relay_node_namespace_typename_mappings(field_call)?.0; let typename_mapping = typename_mappings.get(&global_id.typename).ok_or( error::InternalDeveloperError::GlobalIDTypenameMappingNotFound { type_name: global_id.typename.clone(), diff --git a/v3/engine/src/schema/query_root/node_field.rs b/v3/engine/src/schema/query_root/node_field.rs index 18a463503a7..6e2e29ae1e9 100644 --- a/v3/engine/src/schema/query_root/node_field.rs +++ b/v3/engine/src/schema/query_root/node_field.rs @@ -17,6 +17,7 @@ use crate::schema::types::{ Annotation, NodeFieldTypeNameMapping, OutputAnnotation, RootFieldAnnotation, }; use crate::schema::{Role, GDS}; +use crate::utils::HashMapWithJsonKey; pub(crate) struct RelayNodeFieldOutput { pub relay_node_gql_field: gql_schema::Field, @@ -111,7 +112,7 @@ pub(crate) fn relay_node_field( relay_node_field_permissions.insert( role.clone(), Some(types::NamespaceAnnotation::NodeFieldTypeMappings( - role_type_permission, + HashMapWithJsonKey(role_type_permission), )), ); } diff --git a/v3/engine/src/schema/types.rs b/v3/engine/src/schema/types.rs index 3728e204c13..82f7629387c 100644 --- a/v3/engine/src/schema/types.rs +++ b/v3/engine/src/schema/types.rs @@ -11,9 +11,12 @@ use std::{ use open_dds::{commands, models, types}; -use crate::metadata::resolved::{ - self, - subgraph::{Qualified, QualifiedTypeReference}, +use crate::{ + metadata::resolved::{ + self, + subgraph::{Qualified, QualifiedTypeReference}, + }, + utils::HashMapWithJsonKey, }; use strum_macros::Display; @@ -183,7 +186,7 @@ pub enum NamespaceAnnotation { /// decoding, a typename will be obtained. We need to use that typename to look up the /// Hashmap to get the appropriate `resolved::model::FilterPermission`. NodeFieldTypeMappings( - HashMap, resolved::model::FilterPermission>, + HashMapWithJsonKey, resolved::model::FilterPermission>, ), } diff --git a/v3/engine/src/utils.rs b/v3/engine/src/utils.rs index 56e7efb6e1e..a84a7f658d5 100644 --- a/v3/engine/src/utils.rs +++ b/v3/engine/src/utils.rs @@ -1 +1,92 @@ pub mod json_ext; +use serde::{de::DeserializeOwned, ser::SerializeMap, Deserialize, Serialize}; +use std::{collections::HashMap, hash::Hash}; + +/// HashMapWithJsonKey serializes the key as a String +/// during serialization and similarly while deserialization, the keys +/// are expected to be Stringified and it is then deserialized into K. +/// This type can be helpful when a HashMap needs to be serialized, +/// where K is a custom struct and which serializes into a JSON object +/// by default, serializing such a HashMap into JSON will throw an error +/// because JSON spec mandates that the keys in a JSON object be keys. +/// So, wrapping the HashMap with this type would serialize the Hashmap's +/// keys as strings and deserialize correspondingly. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HashMapWithJsonKey Deserialize<'a>, V>( + pub HashMap, +); + +impl Deserialize<'a> + Eq + Hash, V: Serialize> Serialize + for HashMapWithJsonKey +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut map = serializer.serialize_map(Some(self.0.len()))?; + for (k, v) in self.0.iter() { + let stringified_key = serde_json::to_string(k).map_err(serde::ser::Error::custom)?; + map.serialize_entry(&stringified_key, v)?; + } + map.end() + } +} + +impl<'de, K: DeserializeOwned + Hash + Eq + Serialize, V: Deserialize<'de>> Deserialize<'de> + for HashMapWithJsonKey +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let map: HashMap = Deserialize::deserialize(deserializer)?; + let mut result = HashMap::new(); + for (k, v) in map.into_iter() { + let k_str = serde_json::from_str(&k).map_err(serde::de::Error::custom)?; + result.insert(k_str, v); + } + Ok(HashMapWithJsonKey(result)) + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use serde::{Deserialize, Serialize}; + + use crate::utils::HashMapWithJsonKey; + + #[test] + fn test_hashmap_with_serializable_key() { + #[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Debug, Clone)] + struct Foo { + x: u32, + } + + let mut test_map: HashMap = HashMap::new(); + test_map.insert(Foo { x: 1 }, "hello".to_string()); + + // The `test_map` cannot be serialized as is because + // the key by default is serialized to a JSON object + // and the JSON spec mandates that in a JSON object, the + // key must be a String. + + assert!(serde_json::to_string(&test_map).is_err()); + + let test_map_with_serializable_key = HashMapWithJsonKey(test_map.clone()); + + let serialized_test_map_with_serializable_key = + serde_json::to_string(&test_map_with_serializable_key).unwrap(); + + assert_eq!( + serialized_test_map_with_serializable_key, + "{\"{\\\"x\\\":1}\":\"hello\"}" + ); + + let deserialized_test_map_with_serializable_key: HashMapWithJsonKey = + serde_json::from_str(&serialized_test_map_with_serializable_key).unwrap(); + + assert_eq!(test_map, deserialized_test_map_with_serializable_key.0); + } +} diff --git a/v3/engine/tests/common.rs b/v3/engine/tests/common.rs index 7ef0f2189a5..8201638ab83 100644 --- a/v3/engine/tests/common.rs +++ b/v3/engine/tests/common.rs @@ -65,6 +65,9 @@ pub fn test_execution_expectation_legacy(test_path_string: &str, common_metadata let gds = GDS::new(serde_json::from_value(metadata).unwrap()).unwrap(); let schema = GDS::build_schema(&gds).unwrap(); + // Ensure schema is serialized successfully. + serde_json::to_string(&schema).unwrap(); + let query = fs::read_to_string(request_path).unwrap(); let session = { @@ -129,6 +132,9 @@ pub fn test_execution_expectation(test_path_string: &str, common_metadata_paths: let gds = GDS::new(serde_json::from_value(metadata).unwrap()).unwrap(); let schema = GDS::build_schema(&gds).unwrap(); + // Ensure schema is serialized successfully. + serde_json::to_string(&schema).unwrap(); + let query = fs::read_to_string(request_path).unwrap(); let session_vars_path = &test_path.join("session_variables.json");