This commit is contained in:
sunli 2020-03-09 12:08:50 +08:00
parent 0169fd6122
commit 3de914df7a
17 changed files with 407 additions and 58 deletions

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql"
version = "0.10.1"
version = "0.10.2"
authors = ["sunli <scott_s829@163.com>"]
edition = "2018"
description = "The GraphQL server library implemented by rust"
@ -17,7 +17,7 @@ readme = "README.md"
default = ["chrono", "uuid"]
[dependencies]
async-graphql-derive = { path = "async-graphql-derive", version = "0.10.1" }
async-graphql-derive = { path = "async-graphql-derive", version = "0.10.2" }
graphql-parser = "0.2.3"
anyhow = "1.0.26"
thiserror = "1.0.11"

View File

@ -85,14 +85,14 @@
- [ ] Validation rules
- [X] ArgumentsOfCorrectType
- [X] DefaultValuesOfCorrectType
- [ ] FieldsOnCorrectType
- [ ] FragmentsOnCompositeTypes
- [ ] KnownArgumentNames
- [X] FieldsOnCorrectType
- [X] FragmentsOnCompositeTypes
- [X] KnownArgumentNames
- [ ] KnownDirectives
- [ ] KnownFragmentNames
- [X] KnownFragmentNames
- [ ] KnownTypeNames
- [ ] LoneAnonymousOperation
- [ ] NoFragmentCycles
- [X] NoFragmentCycles
- [ ] NoUndefinedVariables
- [ ] NoUnusedFragments
- [ ] NoUnusedVariables

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-derive"
version = "0.10.1"
version = "0.10.2"
authors = ["sunli <scott_s829@163.com>"]
edition = "2018"
description = "The GraphQL server library implemented by rust"

View File

@ -1,6 +1,6 @@
use crate::model::{__EnumValue, __Field, __InputValue, __TypeKind};
use crate::registry;
use crate::registry::{Type, TypeInfo};
use crate::registry::{Type, TypeName};
use async_graphql_derive::Object;
enum TypeDetail<'a> {
@ -23,16 +23,16 @@ impl<'a> __Type<'a> {
}
pub fn new(registry: &'a registry::Registry, type_name: &str) -> __Type<'a> {
match TypeInfo::create(type_name) {
TypeInfo::NonNull(ty) => __Type {
match TypeName::create(type_name) {
TypeName::NonNull(ty) => __Type {
registry,
detail: TypeDetail::NonNull(ty.to_string()),
},
TypeInfo::List(ty) => __Type {
TypeName::List(ty) => __Type {
registry,
detail: TypeDetail::List(ty.to_string()),
},
TypeInfo::Type(ty) => __Type {
TypeName::Name(ty) => __Type {
registry,
detail: TypeDetail::Simple(&registry.types[ty]),
},

View File

@ -17,20 +17,20 @@ fn parse_list(type_name: &str) -> Option<&str> {
}
}
pub enum TypeInfo<'a> {
pub enum TypeName<'a> {
List(&'a str),
NonNull(&'a str),
Type(&'a str),
Name(&'a str),
}
impl<'a> TypeInfo<'a> {
pub fn create(type_name: &str) -> TypeInfo {
impl<'a> TypeName<'a> {
pub fn create(type_name: &str) -> TypeName {
if let Some(type_name) = parse_non_null(type_name) {
TypeInfo::NonNull(type_name)
TypeName::NonNull(type_name)
} else if let Some(type_name) = parse_list(type_name) {
TypeInfo::List(type_name)
TypeName::List(type_name)
} else {
TypeInfo::Type(type_name)
TypeName::Name(type_name)
}
}
}
@ -113,6 +113,23 @@ impl Type {
Type::InputObject { name, .. } => name,
}
}
pub fn is_composite(&self) -> bool {
match self {
Type::Object { .. } => true,
Type::Interface { .. } => true,
Type::Union { .. } => true,
_ => false,
}
}
pub fn is_leaf(&self) -> bool {
match self {
Type::Enum { .. } => true,
Type::Scalar { .. } => true,
_ => false,
}
}
}
pub struct Directive {
@ -147,7 +164,7 @@ impl Registry {
fields.insert(
"__typename",
Field {
name: "",
name: "__typename",
description: None,
args: Default::default(),
ty: "String!".to_string(),
@ -179,10 +196,10 @@ impl Registry {
}
pub fn get_basic_type(&self, type_name: &str) -> Option<&Type> {
match TypeInfo::create(type_name) {
TypeInfo::Type(type_name) => self.types.get(type_name),
TypeInfo::List(type_name) => self.get_basic_type(type_name),
TypeInfo::NonNull(type_name) => self.get_basic_type(type_name),
match TypeName::create(type_name) {
TypeName::Name(type_name) => self.types.get(type_name),
TypeName::List(type_name) => self.get_basic_type(type_name),
TypeName::NonNull(type_name) => self.get_basic_type(type_name),
}
}
}

View File

@ -1,19 +1,30 @@
use crate::error::RuleError;
use crate::registry::{Registry, Type};
use graphql_parser::query::{Definition, Document};
use graphql_parser::Pos;
use std::collections::HashSet;
pub struct ValidatorContext<'a> {
pub registry: &'a Registry,
pub errors: Vec<RuleError>,
type_stack: Vec<&'a Type>,
fragments_names: HashSet<&'a str>,
}
impl<'a> ValidatorContext<'a> {
pub fn new(registry: &'a Registry) -> Self {
pub fn new(registry: &'a Registry, doc: &'a Document) -> Self {
Self {
registry,
errors: Default::default(),
type_stack: Default::default(),
fragments_names: doc
.definitions
.iter()
.filter_map(|d| match d {
Definition::Fragment(fragment) => Some(fragment.name.as_str()),
_ => None,
})
.collect(),
}
}
@ -24,13 +35,25 @@ impl<'a> ValidatorContext<'a> {
})
}
pub fn append_errors(&mut self, errors: Vec<RuleError>) {
self.errors.extend(errors);
}
pub fn with_type<F: FnMut(&mut ValidatorContext<'a>)>(&mut self, ty: &'a Type, mut f: F) {
self.type_stack.push(ty);
f(self);
self.type_stack.pop();
}
pub fn parent_type(&self) -> &'a Type {
pub fn parent_type(&self) -> Option<&'a Type> {
self.type_stack.get(self.type_stack.len() - 2).map(|t| *t)
}
pub fn current_type(&self) -> &'a Type {
self.type_stack.last().unwrap()
}
pub fn is_known_fragment(&self, name: &str) -> bool {
self.fragments_names.contains(name)
}
}

View File

@ -11,10 +11,15 @@ mod utils;
mod visitor;
pub fn check_rules(registry: &Registry, doc: &Document) -> Result<()> {
let mut ctx = ValidatorContext::new(registry);
let mut ctx = ValidatorContext::new(registry, doc);
let mut visitor = VisitorNil
.with(rules::ArgumentsOfCorrectType::default())
.with(rules::DefaultValuesOfCorrectType);
.with(rules::DefaultValuesOfCorrectType)
.with(rules::FieldsOnCorrectType)
.with(rules::FragmentsOnCompositeTypes)
.with(rules::KnownArgumentNames::default())
.with(rules::NoFragmentCycles::default())
.with(rules::KnownFragmentNames);
visit(&mut visitor, &mut ctx, doc);
if !ctx.errors.is_empty() {

View File

@ -45,7 +45,7 @@ impl<'a> Visitor<'a> for ArgumentsOfCorrectType<'a> {
fn enter_field(&mut self, ctx: &mut ValidatorContext<'a>, field: &'a Field) {
self.current_args = ctx
.parent_type()
.field_by_name(&field.name)
.and_then(|p| p.field_by_name(&field.name))
.map(|f| (&f.args, field.position));
}

View File

@ -3,7 +3,6 @@ use crate::validation::utils::is_valid_input_value;
use crate::validation::visitor::Visitor;
use graphql_parser::query::{Type, VariableDefinition};
#[derive(Default)]
pub struct DefaultValuesOfCorrectType;
impl<'a> Visitor<'a> for DefaultValuesOfCorrectType {
@ -15,7 +14,7 @@ impl<'a> Visitor<'a> for DefaultValuesOfCorrectType {
if let Some(value) = &variable_definition.default_value {
if let Type::NonNullType(_) = variable_definition.var_type {
ctx.report_error(vec![variable_definition.position],format!(
"Argument \"{}\" has type \"{}\" and is not nullable, so it't can't have a default value",
"Argument \"#{}\" has type \"{}\" and is not nullable, so it't can't have a default value",
variable_definition.name, variable_definition.var_type,
));
} else {

View File

@ -0,0 +1,33 @@
use crate::registry::Type;
use crate::validation::context::ValidatorContext;
use crate::validation::visitor::Visitor;
use graphql_parser::query::Field;
#[derive(Default)]
pub struct FieldsOnCorrectType;
impl<'a> Visitor<'a> for FieldsOnCorrectType {
fn enter_field(&mut self, ctx: &mut ValidatorContext<'a>, field: &'a Field) {
if ctx
.parent_type()
.unwrap()
.field_by_name(&field.name)
.is_none()
{
if let Some(Type::Union { .. }) = ctx.parent_type() {
if field.name == "__typename" {
return;
}
}
ctx.report_error(
vec![field.position],
format!(
"Unknown field \"{}\" on type \"{}\"",
field.name,
ctx.parent_type().unwrap().name()
),
);
}
}
}

View File

@ -0,0 +1,41 @@
use crate::validation::context::ValidatorContext;
use crate::validation::visitor::Visitor;
use graphql_parser::query::{FragmentDefinition, InlineFragment, TypeCondition};
#[derive(Default)]
pub struct FragmentsOnCompositeTypes;
impl<'a> Visitor<'a> for FragmentsOnCompositeTypes {
fn enter_fragment_definition(
&mut self,
ctx: &mut ValidatorContext<'a>,
fragment_definition: &'a FragmentDefinition,
) {
if !ctx.current_type().is_composite() {
let TypeCondition::On(name) = &fragment_definition.type_condition;
ctx.report_error(
vec![fragment_definition.position],
format!(
"Fragment \"{}\" cannot condition non composite type \"{}\"",
fragment_definition.name, name
),
);
}
}
fn enter_inline_fragment(
&mut self,
ctx: &mut ValidatorContext<'a>,
inline_fragment: &'a InlineFragment,
) {
if !ctx.current_type().is_composite() {
ctx.report_error(
vec![inline_fragment.position],
format!(
"Fragment cannot condition non composite type \"{}\"",
ctx.current_type().name()
),
);
}
}
}

View File

@ -0,0 +1,86 @@
use crate::registry::InputValue;
use crate::validation::context::ValidatorContext;
use crate::validation::visitor::Visitor;
use crate::Value;
use graphql_parser::query::{Directive, Field};
use graphql_parser::Pos;
use std::collections::HashMap;
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 HashMap<&'static str, InputValue>, ArgsType<'a>, Pos)>,
}
impl<'a> Visitor<'a> for KnownArgumentNames<'a> {
fn enter_directive(&mut self, ctx: &mut ValidatorContext<'a>, directive: &'a Directive) {
self.current_args = ctx.registry.directives.get(&directive.name).map(|d| {
(
&d.args,
ArgsType::Directive(&directive.name),
directive.position,
)
});
}
fn exit_directive(&mut self, _ctx: &mut ValidatorContext<'a>, _directive: &'a Directive) {
self.current_args = None;
}
fn enter_argument(&mut self, ctx: &mut ValidatorContext<'a>, name: &str, _value: &'a Value) {
if let Some((args, arg_type, pos)) = &self.current_args {
if !args.contains_key(name) {
match arg_type {
ArgsType::Field {
field_name,
type_name,
} => {
ctx.report_error(
vec![*pos],
format!(
"Unknown argument \"{}\" on field \"{}\" of type \"{}\"",
name, field_name, type_name,
),
);
}
ArgsType::Directive(directive_name) => {
ctx.report_error(
vec![*pos],
format!(
"Unknown argument \"{}\" on directive \"{}\"",
name, directive_name
),
);
}
}
}
}
}
fn enter_field(&mut self, ctx: &mut ValidatorContext<'a>, field: &'a Field) {
self.current_args = ctx
.parent_type()
.and_then(|p| p.field_by_name(&field.name))
.map(|f| {
(
&f.args,
ArgsType::Field {
field_name: &field.name,
type_name: ctx.parent_type().unwrap().name(),
},
field.position,
)
});
}
fn exit_field(&mut self, _ctx: &mut ValidatorContext<'a>, _field: &'a Field) {
self.current_args = None;
}
}

View File

@ -0,0 +1,21 @@
use crate::validation::context::ValidatorContext;
use crate::validation::visitor::Visitor;
use graphql_parser::query::FragmentSpread;
#[derive(Default)]
pub struct KnownFragmentNames;
impl<'a> Visitor<'a> for KnownFragmentNames {
fn enter_fragment_spread(
&mut self,
ctx: &mut ValidatorContext<'a>,
fragment_spread: &'a FragmentSpread,
) {
if !ctx.is_known_fragment(&fragment_spread.fragment_name) {
ctx.report_error(
vec![fragment_spread.position],
format!(r#"Unknown fragment: "{}""#, fragment_spread.fragment_name),
);
}
}
}

View File

@ -1,5 +1,15 @@
mod arguments_of_correct_type;
mod default_values_of_correct_type;
mod fields_on_correct_type;
mod fragments_on_composite_types;
mod known_argument_names;
mod known_fragment_names;
mod no_fragment_cycles;
pub use arguments_of_correct_type::ArgumentsOfCorrectType;
pub use default_values_of_correct_type::DefaultValuesOfCorrectType;
pub use fields_on_correct_type::FieldsOnCorrectType;
pub use fragments_on_composite_types::FragmentsOnCompositeTypes;
pub use known_argument_names::KnownArgumentNames;
pub use known_fragment_names::KnownFragmentNames;
pub use no_fragment_cycles::NoFragmentCycles;

View File

@ -0,0 +1,105 @@
use crate::error::RuleError;
use crate::validation::context::ValidatorContext;
use crate::validation::visitor::Visitor;
use graphql_parser::query::{Document, FragmentDefinition, FragmentSpread};
use graphql_parser::Pos;
use std::collections::{HashMap, HashSet};
struct CycleDetector<'a> {
visited: HashSet<&'a str>,
spreads: &'a HashMap<&'a str, Vec<(&'a str, Pos)>>,
path_indices: HashMap<&'a str, usize>,
errors: Vec<RuleError>,
}
impl<'a> CycleDetector<'a> {
fn detect_from(&mut self, from: &'a str, path: &mut Vec<(&'a str, Pos)>) {
self.visited.insert(from);
if !self.spreads.contains_key(from) {
return;
}
self.path_indices.insert(from, path.len());
for (name, pos) in &self.spreads[from] {
let index = self.path_indices.get(name).cloned();
if let Some(index) = index {
let err_pos = if index < path.len() {
path[index].1
} else {
*pos
};
self.errors.push(RuleError {
locations: vec![err_pos],
message: format!("Cannot spread fragment \"{}\"", name),
});
} else if !self.visited.contains(name) {
path.push((name, *pos));
self.detect_from(name, path);
path.pop();
}
}
self.path_indices.remove(from);
}
}
#[derive(Default)]
pub struct NoFragmentCycles<'a> {
current_fragment: Option<&'a str>,
spreads: HashMap<&'a str, Vec<(&'a str, Pos)>>,
fragment_order: Vec<&'a str>,
}
impl<'a> Visitor<'a> for NoFragmentCycles<'a> {
fn exit_document(&mut self, ctx: &mut ValidatorContext<'a>, _doc: &'a Document) {
let mut detector = CycleDetector {
visited: HashSet::new(),
spreads: &self.spreads,
path_indices: HashMap::new(),
errors: Vec::new(),
};
for frag in &self.fragment_order {
if !detector.visited.contains(frag) {
let mut path = Vec::new();
detector.detect_from(frag, &mut path);
}
}
ctx.append_errors(detector.errors);
}
fn enter_fragment_definition(
&mut self,
_ctx: &mut ValidatorContext<'a>,
fragment_definition: &'a FragmentDefinition,
) {
self.current_fragment = Some(&fragment_definition.name);
self.fragment_order.push(&fragment_definition.name);
}
fn exit_fragment_definition(
&mut self,
_ctx: &mut ValidatorContext<'a>,
_fragment_definition: &'a FragmentDefinition,
) {
self.current_fragment = None;
}
fn enter_fragment_spread(
&mut self,
_ctx: &mut ValidatorContext<'a>,
fragment_spread: &'a FragmentSpread,
) {
if let Some(current_fragment) = self.current_fragment {
self.spreads
.entry(current_fragment)
.or_insert_with(Vec::new)
.push((&fragment_spread.fragment_name, fragment_spread.position));
}
}
}

View File

@ -1,19 +1,19 @@
use crate::registry::{Registry, Type, TypeInfo};
use crate::registry::{Registry, Type, TypeName};
use crate::Value;
pub fn is_valid_input_value(registry: &Registry, type_name: &str, value: &Value) -> bool {
match TypeInfo::create(type_name) {
TypeInfo::NonNull(type_name) => match value {
match TypeName::create(type_name) {
TypeName::NonNull(type_name) => match value {
Value::Null => false,
_ => is_valid_input_value(registry, type_name, value),
},
TypeInfo::List(type_name) => match value {
TypeName::List(type_name) => match value {
Value::List(elems) => elems
.iter()
.all(|elem| is_valid_input_value(registry, type_name, elem)),
_ => false,
},
TypeInfo::Type(type_name) => {
TypeName::Name(type_name) => {
if let Value::Null = value {
return true;
}

View File

@ -341,12 +341,39 @@ fn visit_selection<'a, V: Visitor<'a>>(
) {
v.enter_selection(ctx, selection);
match selection {
Selection::Field(field) => visit_field(v, ctx, field),
Selection::Field(field) => {
if let Some(schema_field) = ctx.current_type().field_by_name(&field.name) {
ctx.with_type(
ctx.registry.get_basic_type(&schema_field.ty).unwrap(),
|ctx| {
visit_field(v, ctx, field);
},
);
} else {
ctx.report_error(
vec![field.position],
format!(
"Cannot query field \"{}\" on type \"{}\".",
field.name,
ctx.current_type().name()
),
);
}
}
Selection::FragmentSpread(fragment_spread) => {
visit_fragment_spread(v, ctx, fragment_spread)
}
Selection::InlineFragment(inline_fragment) => {
visit_inline_fragment(v, ctx, inline_fragment)
if let Some(TypeCondition::On(name)) = &inline_fragment.type_condition {
if let Some(ty) = ctx.registry.types.get(name) {
ctx.with_type(ty, |ctx| visit_inline_fragment(v, ctx, inline_fragment));
} else {
ctx.report_error(
vec![inline_fragment.position],
format!("Unknown type \"{}\".", name),
);
}
}
}
}
v.exit_selection(ctx, selection);
@ -356,25 +383,7 @@ fn visit_field<'a, V: Visitor<'a>>(v: &mut V, ctx: &mut ValidatorContext<'a>, fi
v.enter_field(ctx, field);
visit_arguments(v, ctx, &field.arguments);
visit_directives(v, ctx, &field.directives);
if let Some(schema_field) = ctx.parent_type().field_by_name(&field.name) {
ctx.with_type(
ctx.registry.get_basic_type(&schema_field.ty).unwrap(),
|ctx| {
visit_selection_set(v, ctx, &field.selection_set);
},
);
} else {
ctx.report_error(
vec![field.position],
format!(
"Cannot query field \"{}\" on type \"{}\".",
field.name,
ctx.parent_type().name()
),
);
}
visit_selection_set(v, ctx, &field.selection_set);
v.exit_field(ctx, field);
}