Skip to main content

apollo_compiler/validation/
value.rs

1use crate::ast;
2use crate::coordinate::TypeAttributeCoordinate;
3use crate::schema;
4use crate::validation::diagnostics::DiagnosticData;
5use crate::validation::DiagnosticList;
6use crate::Node;
7
8fn unsupported_type(
9    diagnostics: &mut DiagnosticList,
10    value: &Node<ast::Value>,
11    declared_type: &Node<ast::Type>,
12) {
13    diagnostics.push(
14        value.location(),
15        DiagnosticData::UnsupportedValueType {
16            ty: declared_type.clone(),
17            value: value.clone(),
18            definition_location: declared_type.location(),
19        },
20    )
21}
22
23pub(crate) fn validate_values(
24    diagnostics: &mut DiagnosticList,
25    schema: &crate::Schema,
26    ty: &Node<ast::Type>,
27    argument: &Node<ast::Argument>,
28    var_defs: &[Node<ast::VariableDefinition>],
29) {
30    value_of_correct_type(diagnostics, schema, ty, &argument.value, var_defs);
31}
32
33pub(crate) fn value_of_correct_type(
34    diagnostics: &mut DiagnosticList,
35    schema: &crate::Schema,
36    ty: &Node<ast::Type>,
37    arg_value: &Node<ast::Value>,
38    var_defs: &[Node<ast::VariableDefinition>],
39) {
40    let Some(type_definition) = schema.types.get(ty.inner_named_type()) else {
41        return;
42    };
43
44    match &**arg_value {
45        // When expected as an input type, only integer input values are
46        // accepted. All other input values, including strings with numeric
47        // content, must raise a request error indicating an incorrect
48        // type. If the integer input value represents a value less than
49        // -2^31 or greater than or equal to 2^31, a request error should be
50        // raised.
51        // When expected as an input type, any string (such as "4") or
52        // integer (such as 4 or -4) input value should be coerced to ID
53        ast::Value::Int(int) => match &type_definition {
54            // Any value is valid for a custom scalar.
55            schema::ExtendedType::Scalar(scalar) if !scalar.is_built_in() => {}
56            schema::ExtendedType::Scalar(scalar) => match scalar.name.as_str() {
57                // Any integer sequence is valid for an ID.
58                "ID" => {}
59                "Int" => {
60                    if int.try_to_i32().is_err() {
61                        diagnostics.push(
62                            arg_value.location(),
63                            DiagnosticData::IntCoercionError {
64                                value: int.as_str().to_owned(),
65                            },
66                        )
67                    }
68                }
69                "Float" => {
70                    if int.try_to_f64().is_err() {
71                        diagnostics.push(
72                            arg_value.location(),
73                            DiagnosticData::FloatCoercionError {
74                                value: int.as_str().to_owned(),
75                            },
76                        )
77                    }
78                }
79                _ => unsupported_type(diagnostics, arg_value, ty),
80            },
81            _ => unsupported_type(diagnostics, arg_value, ty),
82        },
83        // When expected as an input type, both integer and float input
84        // values are accepted. All other input values, including strings
85        // with numeric content, must raise a request error indicating an
86        // incorrect type.
87        ast::Value::Float(float) => match &type_definition {
88            // Any value is valid for a custom scalar.
89            schema::ExtendedType::Scalar(scalar) if !scalar.is_built_in() => {}
90            schema::ExtendedType::Scalar(scalar) if scalar.name == "Float" => {
91                if float.try_to_f64().is_err() {
92                    diagnostics.push(
93                        arg_value.location(),
94                        DiagnosticData::FloatCoercionError {
95                            value: float.as_str().to_owned(),
96                        },
97                    )
98                }
99            }
100            _ => unsupported_type(diagnostics, arg_value, ty),
101        },
102        // When expected as an input type, only valid Unicode string input
103        // values are accepted. All other input values must raise a request
104        // error indicating an incorrect type.
105        // When expected as an input type, any string (such as "4") or
106        // integer (such as 4 or -4) input value should be coerced to ID
107        ast::Value::String(_) => match &type_definition {
108            schema::ExtendedType::Scalar(scalar) => {
109                // specifically return diagnostics for ints, floats, and
110                // booleans.
111                // string, ids and custom scalars are ok, and
112                // don't need a diagnostic.
113                if scalar.is_built_in() && !matches!(scalar.name.as_str(), "String" | "ID") {
114                    unsupported_type(diagnostics, arg_value, ty);
115                }
116            }
117            _ => unsupported_type(diagnostics, arg_value, ty),
118        },
119        // When expected as an input type, only boolean input values are
120        // accepted. All other input values must raise a request error
121        // indicating an incorrect type.
122        ast::Value::Boolean(_) => match &type_definition {
123            schema::ExtendedType::Scalar(scalar) => {
124                if scalar.is_built_in() && scalar.name.as_str() != "Boolean" {
125                    unsupported_type(diagnostics, arg_value, ty);
126                }
127            }
128            _ => unsupported_type(diagnostics, arg_value, ty),
129        },
130        ast::Value::Null => {
131            if ty.is_non_null() {
132                unsupported_type(diagnostics, arg_value, ty);
133            }
134        }
135        ast::Value::Variable(var_name) => {
136            if let Some(var_def) = var_defs.iter().find(|v| v.name == *var_name) {
137                match &type_definition {
138                    schema::ExtendedType::Scalar(_)
139                    | schema::ExtendedType::Enum(_)
140                    | schema::ExtendedType::InputObject(_) => {
141                        // we don't have the actual variable values here, so just
142                        // compare if two Types are the same
143                        // TODO(@goto-bus-stop) This should use the is_assignable_to check
144                        if var_def.ty.inner_named_type() != ty.inner_named_type() {
145                            unsupported_type(diagnostics, arg_value, ty);
146                        }
147                    }
148                    _ => unsupported_type(diagnostics, arg_value, ty),
149                }
150            } else {
151                diagnostics.push(
152                    arg_value.location(),
153                    DiagnosticData::UndefinedVariable {
154                        name: var_name.clone(),
155                    },
156                );
157            }
158        }
159        ast::Value::Enum(value) => match &type_definition {
160            schema::ExtendedType::Scalar(scalar) if !scalar.is_built_in() => {
161                // Accept enum values as input for custom scalars
162            }
163            schema::ExtendedType::Enum(enum_) => {
164                if !enum_.values.contains_key(value) {
165                    diagnostics.push(
166                        value.location(),
167                        DiagnosticData::UndefinedEnumValue {
168                            value: value.clone(),
169                            definition: enum_.name.clone(),
170                            definition_location: enum_.location(),
171                        },
172                    );
173                }
174            }
175            _ => unsupported_type(diagnostics, arg_value, ty),
176        },
177        // When expected as an input, list values are accepted only when
178        // each item in the list can be accepted by the list’s item type.
179        //
180        // If the value passed as an input to a list type is not a list and
181        // not the null value, then the result of input coercion is a list
182        // of size one, where the single item value is the result of input
183        // coercion for the list’s item type on the provided value (note
184        // this may apply recursively for nested lists).
185        ast::Value::List(li) => {
186            let accepts_list = ty.is_list()
187                // A named type can still accept a list if it is a custom scalar.
188                || matches!(type_definition, schema::ExtendedType::Scalar(scalar) if !scalar.is_built_in());
189            if !accepts_list {
190                unsupported_type(diagnostics, arg_value, ty)
191            } else {
192                let item_type = ty.same_location(ty.item_type().clone());
193                if type_definition.is_input_type() {
194                    for v in li {
195                        value_of_correct_type(diagnostics, schema, &item_type, v, var_defs);
196                    }
197                } else {
198                    unsupported_type(diagnostics, arg_value, &item_type);
199                }
200            }
201        }
202        ast::Value::Object(obj) => match &type_definition {
203            schema::ExtendedType::Scalar(scalar) if !scalar.is_built_in() => {}
204            schema::ExtendedType::InputObject(input_obj) => {
205                let undefined_field = obj
206                    .iter()
207                    .find(|(name, ..)| !input_obj.fields.contains_key(name));
208
209                // Add a diagnostic if a value does not exist on the input
210                // object type
211                if let Some((name, value)) = undefined_field {
212                    diagnostics.push(
213                        value.location(),
214                        DiagnosticData::UndefinedInputValue {
215                            value: name.clone(),
216                            definition: input_obj.name.clone(),
217                            definition_location: input_obj.location(),
218                        },
219                    );
220                }
221
222                input_obj.fields.iter().for_each(|(input_name, f)| {
223                    let ty = &f.ty;
224                    let is_missing = !obj.iter().any(|(value_name, ..)| input_name == value_name);
225                    let is_null = obj
226                        .iter()
227                        .any(|(name, value)| input_name == name && value.is_null());
228
229                    // If the input object field type is non_null, and no
230                    // default value is provided, or if the value provided
231                    // is null or missing entirely, an error should be
232                    // raised.
233                    if (ty.is_non_null() && f.default_value.is_none()) && (is_missing || is_null) {
234                        diagnostics.push(
235                            arg_value.location(),
236                            DiagnosticData::RequiredField {
237                                name: input_name.clone(),
238                                coordinate: TypeAttributeCoordinate {
239                                    ty: input_obj.name.clone(),
240                                    attribute: input_name.clone(),
241                                },
242                                expected_type: ty.clone(),
243                                definition_location: f.location(),
244                            },
245                        );
246                    }
247
248                    let used_val = obj.iter().find(|(obj_name, ..)| obj_name == input_name);
249
250                    if let Some((_, v)) = used_val {
251                        value_of_correct_type(diagnostics, schema, ty, v, var_defs);
252                    }
253                })
254            }
255            _ => unsupported_type(diagnostics, arg_value, ty),
256        },
257    }
258}