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"
|
chrono = "0.4.10"
|
||||||
slab = "0.4.2"
|
slab = "0.4.2"
|
||||||
once_cell = "1.3.1"
|
once_cell = "1.3.1"
|
||||||
|
itertools = "0.9.0"
|
||||||
regex = { version = "1.3.5", optional = true }
|
regex = { version = "1.3.5", optional = true }
|
||||||
bson = { version = "0.14.1", optional = true }
|
bson = { version = "0.14.1", optional = true }
|
||||||
uuid = { version = "0.8.1", optional = true }
|
uuid = { version = "0.8.1", optional = true }
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
mod graphiql_source;
|
mod graphiql_source;
|
||||||
mod playground_source;
|
mod playground_source;
|
||||||
|
|
||||||
|
use itertools::Itertools;
|
||||||
|
|
||||||
pub use graphiql_source::graphiql_source;
|
pub use graphiql_source::graphiql_source;
|
||||||
pub use playground_source::playground_source;
|
pub use playground_source::playground_source;
|
||||||
|
|
||||||
|
@ -136,7 +138,7 @@ impl<'a> Serialize for GQLError<'a> {
|
||||||
for error in errors {
|
for error in errors {
|
||||||
seq.serialize_element(&serde_json::json!({
|
seq.serialize_element(&serde_json::json!({
|
||||||
"message": error.message,
|
"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()
|
seq.end()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::model::{__InputValue, __Type};
|
use crate::model::{__InputValue, __Type};
|
||||||
use crate::registry;
|
use crate::registry;
|
||||||
use async_graphql_derive::Object;
|
use async_graphql_derive::Object;
|
||||||
|
use itertools::Itertools;
|
||||||
|
|
||||||
pub struct __Field<'a> {
|
pub struct __Field<'a> {
|
||||||
pub registry: &'a registry::Registry,
|
pub registry: &'a registry::Registry,
|
||||||
|
@ -32,7 +33,7 @@ impl<'a> __Field<'a> {
|
||||||
registry: self.registry,
|
registry: self.registry,
|
||||||
input_value,
|
input_value,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect_vec();
|
||||||
args.sort_by(|a, b| a.input_value.name.cmp(b.input_value.name));
|
args.sort_by(|a, b| a.input_value.name.cmp(b.input_value.name));
|
||||||
args
|
args
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::model::{__EnumValue, __Field, __InputValue, __TypeKind};
|
use crate::model::{__EnumValue, __Field, __InputValue, __TypeKind};
|
||||||
use crate::registry;
|
use crate::registry;
|
||||||
use async_graphql_derive::Object;
|
use async_graphql_derive::Object;
|
||||||
|
use itertools::Itertools;
|
||||||
|
|
||||||
enum TypeDetail<'a> {
|
enum TypeDetail<'a> {
|
||||||
Simple(&'a registry::Type),
|
Simple(&'a registry::Type),
|
||||||
|
@ -108,7 +109,7 @@ impl<'a> __Type<'a> {
|
||||||
registry: self.registry,
|
registry: self.registry,
|
||||||
field,
|
field,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect_vec();
|
||||||
fields.sort_by(|a, b| a.field.name.cmp(&b.field.name));
|
fields.sort_by(|a, b| a.field.name.cmp(&b.field.name));
|
||||||
Some(fields)
|
Some(fields)
|
||||||
})
|
})
|
||||||
|
|
|
@ -17,6 +17,7 @@ use graphql_parser::parse_query;
|
||||||
use graphql_parser::query::{
|
use graphql_parser::query::{
|
||||||
Definition, Field, FragmentDefinition, OperationDefinition, Selection,
|
Definition, Field, FragmentDefinition, OperationDefinition, Selection,
|
||||||
};
|
};
|
||||||
|
use itertools::Itertools;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use slab::Slab;
|
use slab::Slab;
|
||||||
use std::any::{Any, TypeId};
|
use std::any::{Any, TypeId};
|
||||||
|
@ -217,7 +218,7 @@ where
|
||||||
.extensions
|
.extensions
|
||||||
.iter()
|
.iter()
|
||||||
.map(|factory| factory())
|
.map(|factory| factory())
|
||||||
.collect::<Vec<_>>();
|
.collect_vec();
|
||||||
extensions.iter().for_each(|e| e.parse_start(source));
|
extensions.iter().for_each(|e| e.parse_start(source));
|
||||||
let document = parse_query(source).map_err(Into::<Error>::into)?;
|
let document = parse_query(source).map_err(Into::<Error>::into)?;
|
||||||
extensions.iter().for_each(|e| e.parse_end());
|
extensions.iter().for_each(|e| e.parse_end());
|
||||||
|
|
|
@ -6,6 +6,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
use graphql_parser::query::Field;
|
use graphql_parser::query::Field;
|
||||||
use inflector::Inflector;
|
use inflector::Inflector;
|
||||||
|
use itertools::Itertools;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
@ -142,7 +143,7 @@ impl<T: OutputValueType + Send + Sync, E: ObjectType + Sync + Send> ObjectType
|
||||||
extra_type,
|
extra_type,
|
||||||
node,
|
node,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect_vec();
|
||||||
return OutputValueType::resolve(&edges, &ctx_obj, field.position).await;
|
return OutputValueType::resolve(&edges, &ctx_obj, field.position).await;
|
||||||
} else if field.name.as_str() == "totalCount" {
|
} else if field.name.as_str() == "totalCount" {
|
||||||
return Ok(self
|
return Ok(self
|
||||||
|
@ -151,11 +152,7 @@ impl<T: OutputValueType + Send + Sync, E: ObjectType + Sync + Send> ObjectType
|
||||||
.unwrap_or_else(|| serde_json::Value::Null));
|
.unwrap_or_else(|| serde_json::Value::Null));
|
||||||
} else if field.name.as_str() == T::type_name().to_plural().to_camel_case() {
|
} 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 ctx_obj = ctx.with_selection_set(&field.selection_set);
|
||||||
let items = self
|
let items = self.nodes.iter().map(|(_, _, item)| item).collect_vec();
|
||||||
.nodes
|
|
||||||
.iter()
|
|
||||||
.map(|(_, _, item)| item)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
return OutputValueType::resolve(&items, &ctx_obj, field.position).await;
|
return OutputValueType::resolve(&items, &ctx_obj, field.position).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
mod rules;
|
mod rules;
|
||||||
|
mod suggestion;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod visitor;
|
mod visitor;
|
||||||
mod visitors;
|
mod visitors;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::registry::InputValue;
|
use crate::registry::InputValue;
|
||||||
|
use crate::validation::suggestion::make_suggestion;
|
||||||
use crate::validation::visitor::{Visitor, VisitorContext};
|
use crate::validation::visitor::{Visitor, VisitorContext};
|
||||||
use crate::Value;
|
use crate::Value;
|
||||||
use graphql_parser::query::{Directive, Field};
|
use graphql_parser::query::{Directive, Field};
|
||||||
|
@ -18,6 +19,20 @@ pub struct KnownArgumentNames<'a> {
|
||||||
current_args: Option<(&'a HashMap<&'static str, InputValue>, ArgsType<'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> {
|
impl<'a> Visitor<'a> for KnownArgumentNames<'a> {
|
||||||
fn enter_directive(&mut self, ctx: &mut VisitorContext<'a>, directive: &'a Directive) {
|
fn enter_directive(&mut self, ctx: &mut VisitorContext<'a>, directive: &'a Directive) {
|
||||||
self.current_args = ctx
|
self.current_args = ctx
|
||||||
|
@ -48,8 +63,11 @@ impl<'a> Visitor<'a> for KnownArgumentNames<'a> {
|
||||||
ctx.report_error(
|
ctx.report_error(
|
||||||
vec![pos],
|
vec![pos],
|
||||||
format!(
|
format!(
|
||||||
"Unknown argument \"{}\" on field \"{}\" of type \"{}\"",
|
"Unknown argument \"{}\" on field \"{}\" of type \"{}\".{}",
|
||||||
name, field_name, type_name,
|
name,
|
||||||
|
field_name,
|
||||||
|
type_name,
|
||||||
|
self.get_suggestion(name)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -57,8 +75,10 @@ impl<'a> Visitor<'a> for KnownArgumentNames<'a> {
|
||||||
ctx.report_error(
|
ctx.report_error(
|
||||||
vec![pos],
|
vec![pos],
|
||||||
format!(
|
format!(
|
||||||
"Unknown argument \"{}\" on directive \"{}\"",
|
"Unknown argument \"{}\" on directive \"{}\".{}",
|
||||||
name, directive_name
|
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::error::RuleError;
|
||||||
use crate::registry;
|
use crate::registry;
|
||||||
|
use crate::validation::suggestion::make_suggestion;
|
||||||
use graphql_parser::query::{
|
use graphql_parser::query::{
|
||||||
Definition, Directive, Document, Field, FragmentDefinition, FragmentSpread, InlineFragment,
|
Definition, Directive, Document, Field, FragmentDefinition, FragmentSpread, InlineFragment,
|
||||||
Name, OperationDefinition, Selection, SelectionSet, TypeCondition, Value, VariableDefinition,
|
Name, OperationDefinition, Selection, SelectionSet, TypeCondition, Value, VariableDefinition,
|
||||||
|
@ -494,9 +495,20 @@ fn visit_selection<'a, V: Visitor<'a>>(
|
||||||
ctx.report_error(
|
ctx.report_error(
|
||||||
vec![field.position],
|
vec![field.position],
|
||||||
format!(
|
format!(
|
||||||
"Cannot query field \"{}\" on type \"{}\".",
|
"Unknown field \"{}\" on type \"{}\".{}",
|
||||||
field.name,
|
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