If the field name or parameter name is wrong, give suggestion

This commit is contained in:
sunli 2020-04-03 13:57:24 +08:00
parent 0ddf5d913e
commit 1b05724390
10 changed files with 102 additions and 16 deletions

View File

@ -35,6 +35,7 @@ parking_lot = "0.10.0"
chrono = "0.4.10"
slab = "0.4.2"
once_cell = "1.3.1"
itertools = "0.9.0"
regex = { version = "1.3.5", optional = true }
bson = { version = "0.14.1", optional = true }
uuid = { version = "0.8.1", optional = true }

View File

@ -3,6 +3,8 @@
mod graphiql_source;
mod playground_source;
use itertools::Itertools;
pub use graphiql_source::graphiql_source;
pub use playground_source::playground_source;
@ -136,7 +138,7 @@ impl<'a> Serialize for GQLError<'a> {
for error in errors {
seq.serialize_element(&serde_json::json!({
"message": error.message,
"locations": error.locations.iter().map(|pos| serde_json::json!({"line": pos.line, "column": pos.column})).collect::<Vec<_>>(),
"locations": error.locations.iter().map(|pos| serde_json::json!({"line": pos.line, "column": pos.column})).collect_vec(),
}))?;
}
seq.end()

View File

@ -1,6 +1,7 @@
use crate::model::{__InputValue, __Type};
use crate::registry;
use async_graphql_derive::Object;
use itertools::Itertools;
pub struct __Field<'a> {
pub registry: &'a registry::Registry,
@ -32,7 +33,7 @@ impl<'a> __Field<'a> {
registry: self.registry,
input_value,
})
.collect::<Vec<_>>();
.collect_vec();
args.sort_by(|a, b| a.input_value.name.cmp(b.input_value.name));
args
}

View File

@ -1,6 +1,7 @@
use crate::model::{__EnumValue, __Field, __InputValue, __TypeKind};
use crate::registry;
use async_graphql_derive::Object;
use itertools::Itertools;
enum TypeDetail<'a> {
Simple(&'a registry::Type),
@ -108,7 +109,7 @@ impl<'a> __Type<'a> {
registry: self.registry,
field,
})
.collect::<Vec<_>>();
.collect_vec();
fields.sort_by(|a, b| a.field.name.cmp(&b.field.name));
Some(fields)
})

View File

@ -17,6 +17,7 @@ use graphql_parser::parse_query;
use graphql_parser::query::{
Definition, Field, FragmentDefinition, OperationDefinition, Selection,
};
use itertools::Itertools;
use once_cell::sync::Lazy;
use slab::Slab;
use std::any::{Any, TypeId};
@ -217,7 +218,7 @@ where
.extensions
.iter()
.map(|factory| factory())
.collect::<Vec<_>>();
.collect_vec();
extensions.iter().for_each(|e| e.parse_start(source));
let document = parse_query(source).map_err(Into::<Error>::into)?;
extensions.iter().for_each(|e| e.parse_end());

View File

@ -6,6 +6,7 @@ use crate::{
};
use graphql_parser::query::Field;
use inflector::Inflector;
use itertools::Itertools;
use std::borrow::Cow;
use std::collections::HashMap;
@ -142,7 +143,7 @@ impl<T: OutputValueType + Send + Sync, E: ObjectType + Sync + Send> ObjectType
extra_type,
node,
})
.collect::<Vec<_>>();
.collect_vec();
return OutputValueType::resolve(&edges, &ctx_obj, field.position).await;
} else if field.name.as_str() == "totalCount" {
return Ok(self
@ -151,11 +152,7 @@ impl<T: OutputValueType + Send + Sync, E: ObjectType + Sync + Send> ObjectType
.unwrap_or_else(|| serde_json::Value::Null));
} else if field.name.as_str() == T::type_name().to_plural().to_camel_case() {
let ctx_obj = ctx.with_selection_set(&field.selection_set);
let items = self
.nodes
.iter()
.map(|(_, _, item)| item)
.collect::<Vec<_>>();
let items = self.nodes.iter().map(|(_, _, item)| item).collect_vec();
return OutputValueType::resolve(&items, &ctx_obj, field.position).await;
}

View File

@ -1,4 +1,5 @@
mod rules;
mod suggestion;
mod utils;
mod visitor;
mod visitors;

View File

@ -1,4 +1,5 @@
use crate::registry::InputValue;
use crate::validation::suggestion::make_suggestion;
use crate::validation::visitor::{Visitor, VisitorContext};
use crate::Value;
use graphql_parser::query::{Directive, Field};
@ -18,6 +19,20 @@ pub struct KnownArgumentNames<'a> {
current_args: Option<(&'a HashMap<&'static str, InputValue>, 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))
.flatten(),
name,
)
.unwrap_or_default()
}
}
impl<'a> Visitor<'a> for KnownArgumentNames<'a> {
fn enter_directive(&mut self, ctx: &mut VisitorContext<'a>, directive: &'a Directive) {
self.current_args = ctx
@ -48,8 +63,11 @@ impl<'a> Visitor<'a> for KnownArgumentNames<'a> {
ctx.report_error(
vec![pos],
format!(
"Unknown argument \"{}\" on field \"{}\" of type \"{}\"",
name, field_name, type_name,
"Unknown argument \"{}\" on field \"{}\" of type \"{}\".{}",
name,
field_name,
type_name,
self.get_suggestion(name)
),
);
}
@ -57,8 +75,10 @@ impl<'a> Visitor<'a> for KnownArgumentNames<'a> {
ctx.report_error(
vec![pos],
format!(
"Unknown argument \"{}\" on directive \"{}\"",
name, directive_name
"Unknown argument \"{}\" on directive \"{}\".{}",
name,
directive_name,
self.get_suggestion(name)
),
);
}

View File

@ -0,0 +1,50 @@
use itertools::Itertools;
use std::collections::HashMap;
fn levenshtein_distance(s1: &str, s2: &str) -> usize {
let mut column = (0..=s1.len()).collect_vec();
for (x, rx) in s2.bytes().enumerate() {
column[0] = x + 1;
let mut lastdiag = x;
for (y, ry) in s1.bytes().enumerate() {
let olddiag = column[y + 1];
if rx != ry {
lastdiag += 1;
}
column[y + 1] = (column[y + 1] + 1).min((column[y] + 1).min(lastdiag));
lastdiag = olddiag;
}
}
column[s1.len()]
}
pub fn make_suggestion<'a, I>(prefix: &str, options: I, input: &str) -> Option<String>
where
I: Iterator<Item = &'a str>,
{
let mut selected = Vec::new();
let mut distances = HashMap::new();
for opt in options {
let distance = levenshtein_distance(input, opt);
let threshold = (input.len() / 2).max((opt.len() / 2).max(1));
if distance < threshold {
selected.push(opt);
distances.insert(opt, distance);
}
}
if selected.is_empty() {
return None;
}
selected.sort_by(|a, b| distances[a].cmp(&distances[b]));
Some(format!(
"{} {}?",
prefix,
selected
.into_iter()
.map(|s| format!("\"{}\"", s))
.join(", ")
))
}

View File

@ -1,5 +1,6 @@
use crate::error::RuleError;
use crate::registry;
use crate::validation::suggestion::make_suggestion;
use graphql_parser::query::{
Definition, Directive, Document, Field, FragmentDefinition, FragmentSpread, InlineFragment,
Name, OperationDefinition, Selection, SelectionSet, TypeCondition, Value, VariableDefinition,
@ -494,9 +495,20 @@ fn visit_selection<'a, V: Visitor<'a>>(
ctx.report_error(
vec![field.position],
format!(
"Cannot query field \"{}\" on type \"{}\".",
"Unknown field \"{}\" on type \"{}\".{}",
field.name,
ctx.current_type().name()
ctx.current_type().name(),
make_suggestion(
" Did you mean",
ctx.current_type()
.fields()
.iter()
.map(|fields| fields.keys())
.flatten()
.map(|s| s.as_str()),
&field.name
)
.unwrap_or_default()
),
);
}