async_graphql/dynamic/
input_object.rs

1use indexmap::IndexMap;
2
3use super::{directive::to_meta_directive_invocation, Directive};
4use crate::{
5    dynamic::InputValue,
6    registry::{MetaInputValue, MetaType, Registry},
7};
8
9/// A GraphQL input object type
10///
11/// # Examples
12///
13/// ```
14/// use async_graphql::{dynamic::*, value, Value};
15///
16/// let my_input = InputObject::new("MyInput")
17///     .field(InputValue::new("a", TypeRef::named_nn(TypeRef::INT)))
18///     .field(InputValue::new("b", TypeRef::named_nn(TypeRef::INT)));
19///
20/// let query = Object::new("Query").field(
21///     Field::new("add", TypeRef::named_nn(TypeRef::INT), |ctx| {
22///         FieldFuture::new(async move {
23///             let input = ctx.args.try_get("input")?;
24///             let input = input.object()?;
25///             let a = input.try_get("a")?.i64()?;
26///             let b = input.try_get("b")?.i64()?;
27///             Ok(Some(Value::from(a + b)))
28///         })
29///     })
30///     .argument(InputValue::new("input", TypeRef::named_nn(my_input.type_name())))
31/// );
32///
33/// # tokio::runtime::Runtime::new().unwrap().block_on(async move {
34///
35/// let schema = Schema::build(query.type_name(), None, None)
36///     .register(my_input)
37///     .register(query)
38///     .finish()?;
39///
40/// assert_eq!(
41///    schema
42///        .execute("{ add(input: { a: 10, b: 20 }) }")
43///        .await
44///        .into_result()
45///        .unwrap()
46///        .data,
47///    value!({ "add": 30 })
48/// );
49///
50/// # Ok::<_, SchemaError>(())
51/// # }).unwrap();
52/// ```
53#[derive(Debug)]
54pub struct InputObject {
55    pub(crate) name: String,
56    pub(crate) description: Option<String>,
57    pub(crate) fields: IndexMap<String, InputValue>,
58    pub(crate) oneof: bool,
59    inaccessible: bool,
60    tags: Vec<String>,
61    directives: Vec<Directive>,
62}
63
64impl InputObject {
65    /// Create a GraphQL input object type
66    #[inline]
67    pub fn new(name: impl Into<String>) -> Self {
68        Self {
69            name: name.into(),
70            description: None,
71            fields: Default::default(),
72            oneof: false,
73            inaccessible: false,
74            tags: Vec::new(),
75            directives: Vec::new(),
76        }
77    }
78
79    impl_set_description!();
80    impl_set_inaccessible!();
81    impl_set_tags!();
82    impl_directive!();
83
84    /// Add a field
85    #[inline]
86    pub fn field(mut self, field: InputValue) -> Self {
87        assert!(
88            !self.fields.contains_key(&field.name),
89            "Field `{}` already exists",
90            field.name
91        );
92        self.fields.insert(field.name.clone(), field);
93        self
94    }
95
96    /// Indicates this Input Object is a OneOf Input Object
97    pub fn oneof(self) -> Self {
98        Self {
99            oneof: true,
100            ..self
101        }
102    }
103
104    /// Returns the type name
105    #[inline]
106    pub fn type_name(&self) -> &str {
107        &self.name
108    }
109
110    pub(crate) fn register(&self, registry: &mut Registry) -> Result<(), super::SchemaError> {
111        let mut input_fields = IndexMap::new();
112
113        for field in self.fields.values() {
114            input_fields.insert(
115                field.name.clone(),
116                MetaInputValue {
117                    name: field.name.clone(),
118                    description: field.description.clone(),
119                    ty: field.ty.to_string(),
120                    deprecation: field.deprecation.clone(),
121                    default_value: field.default_value.as_ref().map(ToString::to_string),
122                    visible: None,
123                    inaccessible: self.inaccessible,
124                    tags: self.tags.clone(),
125                    is_secret: false,
126                    directive_invocations: to_meta_directive_invocation(field.directives.clone()),
127                },
128            );
129        }
130
131        registry.types.insert(
132            self.name.clone(),
133            MetaType::InputObject {
134                name: self.name.clone(),
135                description: self.description.clone(),
136                input_fields,
137                visible: None,
138                inaccessible: self.inaccessible,
139                tags: self.tags.clone(),
140                rust_typename: None,
141                oneof: self.oneof,
142                directive_invocations: to_meta_directive_invocation(self.directives.clone()),
143            },
144        );
145
146        Ok(())
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use crate::{dynamic::*, value, Pos, ServerError, Value};
153
154    #[tokio::test]
155    async fn input_object() {
156        let myinput = InputObject::new("MyInput")
157            .field(InputValue::new("a", TypeRef::named_nn(TypeRef::INT)))
158            .field(InputValue::new("b", TypeRef::named_nn(TypeRef::INT)));
159        let query = Object::new("Query").field(
160            Field::new("add", TypeRef::named_nn(TypeRef::INT), |ctx| {
161                FieldFuture::new(async move {
162                    let input = ctx.args.try_get("input")?;
163                    let input = input.object()?;
164                    let a = input.try_get("a")?.i64()?;
165                    let b = input.try_get("b")?.i64()?;
166                    Ok(Some(Value::from(a + b)))
167                })
168            })
169            .argument(InputValue::new(
170                "input",
171                TypeRef::named_nn(myinput.type_name()),
172            )),
173        );
174
175        let schema = Schema::build(query.type_name(), None, None)
176            .register(query)
177            .register(myinput)
178            .finish()
179            .unwrap();
180
181        assert_eq!(
182            schema
183                .execute("{ add(input: {a: 10, b: 20}) }")
184                .await
185                .into_result()
186                .unwrap()
187                .data,
188            value!({
189                "add": 30
190            })
191        );
192    }
193
194    #[tokio::test]
195    async fn oneof_input_object() {
196        let myinput = InputObject::new("MyInput")
197            .oneof()
198            .field(InputValue::new("a", TypeRef::named(TypeRef::INT)))
199            .field(InputValue::new("b", TypeRef::named(TypeRef::INT)));
200
201        let query = Object::new("Query").field(
202            Field::new("add10", TypeRef::named_nn(TypeRef::INT), |ctx| {
203                FieldFuture::new(async move {
204                    let input = ctx.args.try_get("input")?;
205                    let input = input.object()?;
206                    Ok(Some(Value::from(if let Some(a) = input.get("a") {
207                        a.i64()? + 10
208                    } else if let Some(b) = input.get("b") {
209                        b.i64()? + 10
210                    } else {
211                        unreachable!()
212                    })))
213                })
214            })
215            .argument(InputValue::new(
216                "input",
217                TypeRef::named_nn(myinput.type_name()),
218            )),
219        );
220
221        let schema = Schema::build(query.type_name(), None, None)
222            .register(query)
223            .register(myinput)
224            .finish()
225            .unwrap();
226
227        assert_eq!(
228            schema
229                .execute("{ add10(input: {a: 10}) }")
230                .await
231                .into_result()
232                .unwrap()
233                .data,
234            value!({
235                "add10": 20
236            })
237        );
238
239        assert_eq!(
240            schema
241                .execute("{ add10(input: {b: 20}) }")
242                .await
243                .into_result()
244                .unwrap()
245                .data,
246            value!({
247                "add10": 30
248            })
249        );
250
251        assert_eq!(
252            schema
253                .execute("{ add10(input: {}) }")
254                .await
255                .into_result()
256                .unwrap_err(),
257            vec![ServerError {
258                message: "Invalid value for argument \"input\", Oneof input objects requires have exactly one field".to_owned(),
259                source: None,
260                locations: vec![Pos { column: 9, line: 1 }],
261                path: vec![],
262                extensions: None,
263            }]
264        );
265
266        assert_eq!(
267            schema
268                .execute("{ add10(input: { a: 10, b: 20 }) }")
269                .await
270                .into_result()
271                .unwrap_err(),
272            vec![ServerError {
273                message: "Invalid value for argument \"input\", Oneof input objects requires have exactly one field".to_owned(),
274                source: None,
275                locations: vec![Pos { column: 9, line: 1 }],
276                path: vec![],
277                extensions: None,
278            }]
279        );
280    }
281
282    #[tokio::test]
283    async fn invalid_oneof_input_object() {
284        let myinput = InputObject::new("MyInput")
285            .oneof()
286            .field(InputValue::new("a", TypeRef::named(TypeRef::INT)))
287            .field(InputValue::new("b", TypeRef::named_nn(TypeRef::INT)));
288
289        let query = Object::new("Query").field(
290            Field::new("value", TypeRef::named_nn(TypeRef::INT), |_| {
291                FieldFuture::new(async move { Ok(Some(Value::from(10))) })
292            })
293            .argument(InputValue::new(
294                "input",
295                TypeRef::named_nn(myinput.type_name()),
296            )),
297        );
298
299        let err = Schema::build(query.type_name(), None, None)
300            .register(query)
301            .register(myinput)
302            .finish()
303            .unwrap_err();
304        assert_eq!(err.0, "Field \"MyInput.b\" must be nullable".to_string());
305
306        let myinput = InputObject::new("MyInput")
307            .oneof()
308            .field(InputValue::new("a", TypeRef::named(TypeRef::INT)))
309            .field(InputValue::new("b", TypeRef::named(TypeRef::INT)).default_value(value!(10)));
310
311        let query = Object::new("Query").field(
312            Field::new("value", TypeRef::named_nn(TypeRef::INT), |_| {
313                FieldFuture::new(async move { Ok(Some(Value::from(10))) })
314            })
315            .argument(InputValue::new(
316                "input",
317                TypeRef::named_nn(myinput.type_name()),
318            )),
319        );
320
321        let err = Schema::build(query.type_name(), None, None)
322            .register(query)
323            .register(myinput)
324            .finish()
325            .unwrap_err();
326        assert_eq!(
327            err.0,
328            "Field \"MyInput.b\" must not have a default value".to_string()
329        );
330    }
331}