If the field name or parameter name is wrong, give suggestion
This commit is contained in:
parent
0ddf5d913e
commit
1b05724390
|
@ -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 }
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
mod rules;
|
||||
mod suggestion;
|
||||
mod utils;
|
||||
mod visitor;
|
||||
mod visitors;
|
||||
|
|
|
@ -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)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
50
src/validation/suggestion.rs
Normal file
50
src/validation/suggestion.rs
Normal 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(", ")
|
||||
))
|
||||
}
|
|
@ -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()
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user