graphql_starter/graphql/
map.rs

1//! Allows the usage of [HashMap] in GraphQL with [GraphQLMap] by serializing it as a regular list of typed entries,
2//! instead of the default opaque JSON.
3//!
4//! GraphQl [doesn't support](https://github.com/graphql/graphql-spec/issues/101) this.
5
6use std::{
7    borrow::Cow,
8    collections::HashMap,
9    fmt::{Debug, Display},
10};
11
12use async_graphql::{
13    parser::types::Field, registry::Registry, resolver_utils::resolve_list, ContextSelectionSet, InputType,
14    InputValueError, InputValueResult, OutputType, Positioned, ServerResult, Value,
15};
16
17use crate::error::{err, Error, GenericErrorCode, MapToErr};
18
19/// This type represents a [HashMap] in GraphQL
20#[derive(Debug, Clone, Default)]
21pub struct GraphQLMap<E: GraphQLMapEntry>(Vec<E>);
22
23/// Entry for a [GraphQLMap]
24pub trait GraphQLMapEntry {
25    type Key: Eq + std::hash::Hash;
26    type Item;
27
28    fn new(key: Self::Key, value: Self::Item) -> Self;
29    fn into_parts(self) -> (Self::Key, Self::Item);
30}
31
32/// Creates a new entry type implementing [GraphQLMapEntry] for the given type.
33///
34/// ## Examples
35///
36/// ```
37/// #[derive(Clone)]
38/// pub struct MyType;
39///
40/// graphql_starter::map_entry_for!(
41///     #[derive(Clone)]
42///     MyType
43/// );
44/// ```
45/// Expands to:
46/// ``` ignore
47/// #[derive(Clone)]
48/// pub struct MyTypeEntry {
49///     pub key: String,
50///     pub value: MyType,
51/// }
52/// ```
53///
54/// You can also configure the key type or the entry type name:
55/// ```
56/// # use graphql_starter::map_entry_for;
57/// # pub struct MyType {
58/// #    value: String,
59/// # }
60/// map_entry_for!(MyType { key = u32 });
61/// map_entry_for!(MyType { entry = MyCustomTypeEntry });
62/// map_entry_for!(MyType { key = &'static str, entry = MyEntry });
63/// ```
64#[macro_export]
65macro_rules! map_entry_for {
66    ($(#[$attr:meta])* $name:ident) => {
67        $crate::map_entry_for!{ $(#[$attr])* $name { key = String } }
68    };
69
70    ($(#[$attr:meta])* $name:ident { key = $key:ty }) => {
71        $crate::crates::paste::paste! {
72            $(#[$attr])*
73            pub struct [<$name Entry>] {
74                /// The entry key
75                pub key: $key,
76                /// The entry value
77                pub value: $name,
78            }
79            impl $crate::graphql::GraphQLMapEntry for [<$name Entry>] {
80                type Key = $key;
81                type Item = $name;
82
83                fn new(key: Self::Key, value: Self::Item) -> Self {
84                    [<$name Entry>] { key, value }
85                }
86
87                fn into_parts(self) -> (Self::Key, Self::Item) {
88                    (self.key, self.value)
89                }
90            }
91        }
92    };
93
94    ($(#[$attr:meta])* $name:ident { entry = $entry:ident }) => {
95        $crate::map_entry_for!{ $(#[$attr])* $name { entry = $entry, key = String } }
96    };
97
98    ($(#[$attr:meta])* $name:ident { key = $key:ty, entry = $entry:ident }) => {
99        $crate::map_entry_for!{ $(#[$attr])* $name { entry = $entry, key = $key } }
100    };
101
102    ($(#[$attr:meta])* $name:ident { entry = $entry:ident, key = $key:ty }) => {
103        $(#[$attr])*
104        pub struct $entry {
105            /// The entry key
106            pub key: $key,
107            /// The entry value
108            pub value: $name,
109        }
110        impl $crate::graphql::GraphQLMapEntry for $entry {
111            type Key = $key;
112            type Item = $name;
113
114            fn new(key: Self::Key, value: Self::Item) -> Self {
115                $entry { key, value }
116            }
117
118            fn into_parts(self) -> (Self::Key, Self::Item) {
119                (self.key, self.value)
120            }
121        }
122    };
123}
124
125impl<E, K, T> From<HashMap<K, T>> for GraphQLMap<E>
126where
127    E: GraphQLMapEntry,
128    K: Into<<E as GraphQLMapEntry>::Key>,
129    T: Into<<E as GraphQLMapEntry>::Item>,
130{
131    fn from(map: HashMap<K, T>) -> Self {
132        GraphQLMap(map.into_iter().map(|(k, v)| E::new(k.into(), v.into())).collect())
133    }
134}
135
136impl<E> GraphQLMap<E>
137where
138    E: GraphQLMapEntry,
139{
140    /// Try to build a new [GraphQLMap] from a [HashMap]
141    pub fn try_from<K, V>(map: HashMap<K, V>) -> Result<Self, Box<Error>>
142    where
143        K: Eq + std::hash::Hash + Display,
144        K: TryInto<<E as GraphQLMapEntry>::Key>,
145        <K as TryInto<<E as GraphQLMapEntry>::Key>>::Error: Display + Send + Sync + 'static,
146        V: TryInto<<E as GraphQLMapEntry>::Item>,
147        <V as TryInto<<E as GraphQLMapEntry>::Item>>::Error: Display + Send + Sync + 'static,
148    {
149        let mut vec = Vec::with_capacity(map.len());
150        for (key, value) in map.into_iter() {
151            let key = key.try_into().map_to_internal_err("Invalid map key")?;
152            let value = value.try_into().map_to_internal_err("Invalid map value")?;
153
154            vec.push(E::new(key, value));
155        }
156        Ok(GraphQLMap(vec))
157    }
158}
159
160impl<E, K, V> TryFrom<GraphQLMap<E>> for HashMap<K, V>
161where
162    E: GraphQLMapEntry,
163    K: Eq + std::hash::Hash + Display,
164    <E as GraphQLMapEntry>::Key: TryInto<K>,
165    <<E as GraphQLMapEntry>::Key as TryInto<K>>::Error: Display + Send + Sync + 'static,
166    <E as GraphQLMapEntry>::Item: TryInto<V>,
167    <<E as GraphQLMapEntry>::Item as TryInto<V>>::Error: Display + Send + Sync + 'static,
168{
169    type Error = Box<Error>;
170
171    fn try_from(value: GraphQLMap<E>) -> Result<Self, Self::Error> {
172        let mut map = HashMap::<K, V>::with_capacity(value.0.len());
173        for e in value.0.into_iter() {
174            let (key, value) = e.into_parts();
175
176            let key = key
177                .try_into()
178                .map_to_err_with(GenericErrorCode::BadRequest, "Invalid map key")?;
179            let value = value
180                .try_into()
181                .map_to_err_with(GenericErrorCode::BadRequest, "Invalid map value")?;
182
183            #[allow(clippy::map_entry)] // If we insert first, we no longer have the key to generate the error message
184            if map.contains_key(&key) {
185                return Err(err!(GenericErrorCode::BadRequest, "Duplicated key: {}", key));
186            } else {
187                map.insert(key, value);
188            }
189        }
190        Ok(map)
191    }
192}
193
194impl<T: OutputType + GraphQLMapEntry> OutputType for GraphQLMap<T> {
195    fn type_name() -> Cow<'static, str> {
196        Cow::Owned(format!("[{}]", T::qualified_type_name()))
197    }
198
199    fn qualified_type_name() -> String {
200        format!("[{}]!", T::qualified_type_name())
201    }
202
203    fn create_type_info(registry: &mut Registry) -> String {
204        T::create_type_info(registry);
205        Self::qualified_type_name()
206    }
207
208    async fn resolve(&self, ctx: &ContextSelectionSet<'_>, field: &Positioned<Field>) -> ServerResult<Value> {
209        resolve_list(ctx, field, &self.0, Some(self.0.len())).await
210    }
211}
212impl<T: InputType + GraphQLMapEntry> InputType for GraphQLMap<T> {
213    type RawValueType = Self;
214
215    fn type_name() -> Cow<'static, str> {
216        Cow::Owned(format!("[{}]", T::qualified_type_name()))
217    }
218
219    fn qualified_type_name() -> String {
220        format!("[{}]!", T::qualified_type_name())
221    }
222
223    fn create_type_info(registry: &mut Registry) -> String {
224        T::create_type_info(registry);
225        Self::qualified_type_name()
226    }
227
228    fn parse(value: Option<Value>) -> InputValueResult<Self> {
229        match value.unwrap_or_default() {
230            Value::List(values) => {
231                let list: Vec<_> = values
232                    .into_iter()
233                    .map(|value| InputType::parse(Some(value)))
234                    .collect::<Result<_, _>>()
235                    .map_err(InputValueError::propagate)?;
236
237                Ok(GraphQLMap(list))
238            }
239            value => {
240                let list = vec![InputType::parse(Some(value)).map_err(InputValueError::propagate)?];
241                Ok(GraphQLMap(list))
242            }
243        }
244    }
245
246    fn to_value(&self) -> Value {
247        Value::List(self.0.iter().map(InputType::to_value).collect())
248    }
249
250    fn as_raw_value(&self) -> Option<&Self::RawValueType> {
251        Some(self)
252    }
253}