async-graphql/src/validation/rules/known_argument_names.rs

290 lines
7.0 KiB
Rust

use async_graphql_value::Value;
use indexmap::map::IndexMap;
use crate::{
parser::types::{Directive, Field},
registry::MetaInputValue,
validation::{
suggestion::make_suggestion,
visitor::{Visitor, VisitorContext},
},
Name, Positioned,
};
enum ArgsType<'a> {
Directive(&'a str),
Field {
field_name: &'a str,
type_name: &'a str,
},
}
#[derive(Default)]
pub struct KnownArgumentNames<'a> {
current_args: Option<(&'a IndexMap<String, MetaInputValue>, ArgsType<'a>)>,
}
impl<'a> KnownArgumentNames<'a> {
fn get_suggestion(&self, name: &str) -> String {
make_suggestion(
" Did you mean",
self.current_args
.iter()
.map(|(args, _)| args.iter().map(|arg| arg.0.as_str()))
.flatten(),
name,
)
.unwrap_or_default()
}
}
impl<'a> Visitor<'a> for KnownArgumentNames<'a> {
fn enter_directive(
&mut self,
ctx: &mut VisitorContext<'a>,
directive: &'a Positioned<Directive>,
) {
self.current_args = ctx
.registry
.directives
.get(directive.node.name.node.as_str())
.map(|d| (&d.args, ArgsType::Directive(&directive.node.name.node)));
}
fn exit_directive(
&mut self,
_ctx: &mut VisitorContext<'a>,
_directive: &'a Positioned<Directive>,
) {
self.current_args = None;
}
fn enter_argument(
&mut self,
ctx: &mut VisitorContext<'a>,
name: &'a Positioned<Name>,
_value: &'a Positioned<Value>,
) {
if let Some((args, arg_type)) = &self.current_args {
if !args.contains_key(name.node.as_str()) {
match arg_type {
ArgsType::Field {
field_name,
type_name,
} => {
ctx.report_error(
vec![name.pos],
format!(
"Unknown argument \"{}\" on field \"{}\" of type \"{}\".{}",
name,
field_name,
type_name,
if ctx.registry.enable_suggestions {
self.get_suggestion(name.node.as_str())
} else {
String::new()
}
),
);
}
ArgsType::Directive(directive_name) => {
ctx.report_error(
vec![name.pos],
format!(
"Unknown argument \"{}\" on directive \"{}\".{}",
name,
directive_name,
self.get_suggestion(name.node.as_str())
),
);
}
}
}
}
}
fn enter_field(&mut self, ctx: &mut VisitorContext<'a>, field: &'a Positioned<Field>) {
if let Some(parent_type) = ctx.parent_type() {
if let Some(schema_field) = parent_type.field_by_name(&field.node.name.node) {
self.current_args = Some((
&schema_field.args,
ArgsType::Field {
field_name: &field.node.name.node,
type_name: ctx.parent_type().unwrap().name(),
},
));
}
}
}
fn exit_field(&mut self, _ctx: &mut VisitorContext<'a>, _field: &'a Positioned<Field>) {
self.current_args = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
pub fn factory<'a>() -> KnownArgumentNames<'a> {
KnownArgumentNames::default()
}
#[test]
fn single_arg_is_known() {
expect_passes_rule!(
factory,
r#"
fragment argOnRequiredArg on Dog {
doesKnowCommand(dogCommand: SIT)
}
{ __typename }
"#,
);
}
#[test]
fn multiple_args_are_known() {
expect_passes_rule!(
factory,
r#"
fragment multipleArgs on ComplicatedArgs {
multipleReqs(req1: 1, req2: 2)
}
{ __typename }
"#,
);
}
#[test]
fn ignores_args_of_unknown_fields() {
expect_passes_rule!(
factory,
r#"
fragment argOnUnknownField on Dog {
unknownField(unknownArg: SIT)
}
{ __typename }
"#,
);
}
#[test]
fn multiple_args_in_reverse_order_are_known() {
expect_passes_rule!(
factory,
r#"
fragment multipleArgsReverseOrder on ComplicatedArgs {
multipleReqs(req2: 2, req1: 1)
}
{ __typename }
"#,
);
}
#[test]
fn no_args_on_optional_arg() {
expect_passes_rule!(
factory,
r#"
fragment noArgOnOptionalArg on Dog {
isHousetrained
}
{ __typename }
"#,
);
}
#[test]
fn args_are_known_deeply() {
expect_passes_rule!(
factory,
r#"
{
dog {
doesKnowCommand(dogCommand: SIT)
}
human {
pet {
... on Dog {
doesKnowCommand(dogCommand: SIT)
}
}
}
}
"#,
);
}
#[test]
fn directive_args_are_known() {
expect_passes_rule!(
factory,
r#"
{
dog @skip(if: true)
}
"#,
);
}
#[test]
fn undirective_args_are_invalid() {
expect_fails_rule!(
factory,
r#"
{
dog @skip(unless: true)
}
"#,
);
}
#[test]
fn invalid_arg_name() {
expect_fails_rule!(
factory,
r#"
fragment invalidArgName on Dog {
doesKnowCommand(unknown: true)
}
{ __typename }
"#,
);
}
#[test]
fn unknown_args_amongst_known_args() {
expect_fails_rule!(
factory,
r#"
fragment oneGoodArgOneInvalidArg on Dog {
doesKnowCommand(whoknows: 1, dogCommand: SIT, unknown: true)
}
{ __typename }
"#,
);
}
#[test]
fn unknown_args_deeply() {
expect_fails_rule!(
factory,
r#"
{
dog {
doesKnowCommand(unknown: true)
}
human {
pet {
... on Dog {
doesKnowCommand(unknown: true)
}
}
}
}
"#,
);
}
}