add some validation rules

This commit is contained in:
sunli 2020-03-10 14:14:09 +08:00
parent 678733725f
commit ecc861de3b
16 changed files with 314 additions and 42 deletions

View File

@ -104,13 +104,13 @@ Open `http://localhost:8000` in browser
- [ ] OverlappingFieldsCanBeMerged
- [ ] PossibleFragmentSpreads
- [ ] ProvidedNonNullArguments
- [ ] ScalarLeafs
- [X] ScalarLeafs
- [X] UniqueArgumentNames
- [X] UniqueFragmentNames
- [ ] UniqueInputFieldNames
- [ ] UniqueOperationNames
- [ ] UniqueVariableNames
- [ ] VariableInAllowedPosition
- [X] UniqueOperationNames
- [X] UniqueVariableNames
- [X] VariablesAreInputTypes
- [X] VariableInAllowedPosition
- [ ] Integration examples
- [X] Actix-web
- [ ] Hyper

View File

@ -111,24 +111,43 @@ impl<'a, T> ContextBase<'a, T> {
.expect("The specified data type does not exist.")
}
fn resolve_input_value(&self, value: Value) -> Result<Value> {
if let Value::Variable(var_name) = value {
let def = self
.variable_definitions
.and_then(|defs| defs.iter().find(|def| def.name == var_name.as_str()));
if let Some(def) = def {
if let Some(var_value) = self.variables.map(|vars| vars.get(&def.name)).flatten() {
return Ok(var_value.clone());
} else if let Some(default) = &def.default_value {
return Ok(default.clone());
fn var_value(&self, name: &str) -> Result<Value> {
let def = self
.variable_definitions
.and_then(|defs| defs.iter().find(|def| def.name == name));
if let Some(def) = def {
if let Some(var_value) = self.variables.map(|vars| vars.get(&def.name)).flatten() {
return Ok(var_value.clone());
} else if let Some(default) = &def.default_value {
return Ok(default.clone());
}
}
return Err(QueryError::VarNotDefined {
var_name: name.to_string(),
}
.into());
}
fn resolve_input_value(&self, mut value: Value) -> Result<Value> {
match value {
Value::Variable(var_name) => self.var_value(&var_name),
Value::List(ref mut ls) => {
for value in ls {
if let Value::Variable(var_name) = value {
*value = self.var_value(&var_name)?;
}
}
Ok(value)
}
return Err(QueryError::VarNotDefined {
var_name: var_name.clone(),
Value::Object(ref mut obj) => {
for (_, value) in obj {
if let Value::Variable(var_name) = value {
*value = self.var_value(&var_name)?;
}
}
Ok(value)
}
.into());
} else {
Ok(value)
_ => Ok(value),
}
}

View File

@ -162,17 +162,17 @@ pub use types::{GQLEnum, GQLEnumItem};
/// self.value
/// }
///
/// #[field(name = "valueRef", desc = "reference value")]
/// #[field(desc = "reference value")]
/// async fn value_ref(&self) -> &i32 {
/// &self.value
/// }
///
/// #[field(name = "valueWithError", desc = "value with error")]
/// #[field(desc = "value with error")]
/// async fn value_with_error(&self) -> Result<i32> {
/// Ok(self.value)
/// }
///
/// #[field(name = "valueWithArg")]
/// #[field]
/// async fn value_with_arg(&self, #[arg(default = "1")] a: i32) -> i32 {
/// a
/// }

View File

@ -32,7 +32,7 @@ impl<'a> __Type<'a> {
registry,
detail: TypeDetail::List(ty.to_string()),
},
TypeName::Name(ty) => __Type {
TypeName::Named(ty) => __Type {
registry,
detail: TypeDetail::Simple(&registry.types[ty]),
},

View File

@ -20,7 +20,7 @@ fn parse_list(type_name: &str) -> Option<&str> {
pub enum TypeName<'a> {
List(&'a str),
NonNull(&'a str),
Name(&'a str),
Named(&'a str),
}
impl<'a> TypeName<'a> {
@ -30,7 +30,7 @@ impl<'a> TypeName<'a> {
} else if let Some(type_name) = parse_list(type_name) {
TypeName::List(type_name)
} else {
TypeName::Name(type_name)
TypeName::Named(type_name)
}
}
@ -38,7 +38,7 @@ impl<'a> TypeName<'a> {
match TypeName::create(type_name) {
TypeName::List(type_name) => Self::get_basic_typename(type_name),
TypeName::NonNull(type_name) => Self::get_basic_typename(type_name),
TypeName::Name(type_name) => type_name,
TypeName::Named(type_name) => type_name,
}
}
}
@ -138,6 +138,15 @@ impl Type {
_ => false,
}
}
pub fn is_input(&self) -> bool {
match self {
Type::Enum { .. } => true,
Type::Scalar { .. } => true,
Type::InputObject { .. } => true,
_ => false,
}
}
}
pub struct Directive {

View File

@ -26,7 +26,12 @@ pub fn check_rules(registry: &Registry, doc: &Document) -> Result<()> {
.with(rules::NoUnusedFragments::default())
.with(rules::NoUnusedVariables::default())
.with(rules::UniqueArgumentNames::default())
.with(rules::UniqueFragmentNames::default());
.with(rules::UniqueFragmentNames::default())
.with(rules::UniqueOperationNames::default())
.with(rules::UniqueVariableNames::default())
.with(rules::VariablesAreInputTypes)
.with(rules::VariableInAllowedPosition::default())
.with(rules::ScalarLeafs);
visit(&mut visitor, &mut ctx, doc);
if !ctx.errors.is_empty() {

View File

@ -12,6 +12,11 @@ mod no_unused_fragments;
mod no_unused_variables;
mod unique_argument_names;
mod unique_fragment_names;
mod unique_operation_names;
mod unique_variable_names;
mod variables_are_input_types;
mod variables_in_allowed_position;
mod scalar_leafs;
pub use arguments_of_correct_type::ArgumentsOfCorrectType;
pub use default_values_of_correct_type::DefaultValuesOfCorrectType;
@ -27,3 +32,8 @@ pub use no_unused_fragments::NoUnusedFragments;
pub use no_unused_variables::NoUnusedVariables;
pub use unique_argument_names::UniqueArgumentNames;
pub use unique_fragment_names::UniqueFragmentNames;
pub use unique_operation_names::UniqueOperationNames;
pub use unique_variable_names::UniqueVariableNames;
pub use variables_are_input_types::VariablesAreInputTypes;
pub use variables_in_allowed_position::VariableInAllowedPosition;
pub use scalar_leafs::ScalarLeafs;

View File

@ -11,6 +11,27 @@ pub struct NoUnusedVariables<'a> {
used_vars: HashSet<&'a str>,
}
impl<'a> NoUnusedVariables<'a> {
fn travel_value(&mut self, value: &'a Value) {
match value {
Value::Variable(var_name) => {
self.used_vars.insert(var_name.as_str());
}
Value::List(values) => {
for value in values {
self.travel_value(value);
}
}
Value::Object(obj) => {
for value in obj.values() {
self.travel_value(value);
}
}
_ => {}
}
}
}
impl<'a> Visitor<'a> for NoUnusedVariables<'a> {
fn enter_operation_definition(
&mut self,
@ -49,8 +70,6 @@ impl<'a> Visitor<'a> for NoUnusedVariables<'a> {
_name: &'a str,
value: &'a Value,
) {
if let Value::Variable(var) = value {
self.used_vars.insert(var);
}
self.travel_value(value);
}
}

View File

@ -0,0 +1,23 @@
use crate::validation::context::ValidatorContext;
use crate::validation::visitor::Visitor;
use graphql_parser::query::Field;
#[derive(Default)]
pub struct ScalarLeafs;
impl<'a> Visitor<'a> for ScalarLeafs {
fn enter_field(&mut self, ctx: &mut ValidatorContext<'a>, field: &'a Field) {
if let Some(ty) = ctx.parent_type() {
if let Some(schema_field) = ty.field_by_name(&field.name) {
if let Some(ty) = ctx.registry.get_basic_type(&schema_field.ty) {
if ty.is_leaf() && !field.selection_set.items.is_empty() {
ctx.report_error(vec![field.position], format!(
"Field \"{}\" must not have a selection since type \"{}\" has no subfields",
field.name, ty.name()
))
}
}
}
}
}
}

View File

@ -18,7 +18,7 @@ impl<'a> Visitor<'a> for UniqueFragmentNames<'a> {
ctx.report_error(
vec![fragment_definition.position],
format!(
"There can only be one fragment named {}",
"There can only be one fragment named \"{}\"",
fragment_definition.name
),
)

View File

@ -0,0 +1,39 @@
use crate::validation::context::ValidatorContext;
use crate::validation::visitor::Visitor;
use graphql_parser::query::{Mutation, OperationDefinition, Query, Subscription};
use std::collections::HashSet;
#[derive(Default)]
pub struct UniqueOperationNames<'a> {
names: HashSet<&'a str>,
}
impl<'a> Visitor<'a> for UniqueOperationNames<'a> {
fn enter_operation_definition(
&mut self,
ctx: &mut ValidatorContext<'a>,
operation_definition: &'a OperationDefinition,
) {
let name = match operation_definition {
OperationDefinition::Query(Query { name, position, .. }) => {
name.as_ref().map(|name| (name, position))
}
OperationDefinition::Mutation(Mutation { name, position, .. }) => {
name.as_ref().map(|name| (name, position))
}
OperationDefinition::Subscription(Subscription { name, position, .. }) => {
name.as_ref().map(|name| (name, position))
}
OperationDefinition::SelectionSet(_) => None,
};
if let Some((name, pos)) = name {
if !self.names.insert(name.as_str()) {
ctx.report_error(
vec![*pos],
format!("There can only be one operation named \"{}\"", name),
)
}
}
}
}

View File

@ -0,0 +1,35 @@
use crate::validation::context::ValidatorContext;
use crate::validation::visitor::Visitor;
use graphql_parser::query::{OperationDefinition, VariableDefinition};
use std::collections::HashSet;
#[derive(Default)]
pub struct UniqueVariableNames<'a> {
names: HashSet<&'a str>,
}
impl<'a> Visitor<'a> for UniqueVariableNames<'a> {
fn enter_operation_definition(
&mut self,
_ctx: &mut ValidatorContext<'a>,
_operation_definition: &'a OperationDefinition,
) {
self.names.clear();
}
fn enter_variable_definition(
&mut self,
ctx: &mut ValidatorContext<'a>,
variable_definition: &'a VariableDefinition,
) {
if !self.names.insert(variable_definition.name.as_str()) {
ctx.report_error(
vec![variable_definition.position],
format!(
"There can only be one variable named \"${}\"",
variable_definition.name
),
);
}
}
}

View File

@ -0,0 +1,30 @@
use crate::validation::context::ValidatorContext;
use crate::validation::visitor::Visitor;
use graphql_parser::query::VariableDefinition;
#[derive(Default)]
pub struct VariablesAreInputTypes;
impl<'a> Visitor<'a> for VariablesAreInputTypes {
fn enter_variable_definition(
&mut self,
ctx: &mut ValidatorContext<'a>,
variable_definition: &'a VariableDefinition,
) {
if let Some(ty) = ctx
.registry
.get_basic_type(&variable_definition.var_type.to_string())
{
if !ty.is_input() {
ctx.report_error(
vec![variable_definition.position],
format!(
"Variable \"{}\" cannot be of non-input type \"{}\"",
&variable_definition.name,
ty.name()
),
);
}
}
}
}

View File

@ -0,0 +1,92 @@
use crate::registry::TypeName;
use crate::validation::context::ValidatorContext;
use crate::validation::visitor::Visitor;
use crate::Value;
use graphql_parser::query::{Field, OperationDefinition, VariableDefinition};
use graphql_parser::schema::Directive;
use graphql_parser::Pos;
use std::collections::HashMap;
#[derive(Default)]
pub struct VariableInAllowedPosition<'a> {
var_types: HashMap<&'a str, String>,
}
impl<'a> VariableInAllowedPosition<'a> {
fn check_type(
&mut self,
ctx: &mut ValidatorContext<'a>,
pos: Pos,
except_type: &str,
value: &Value,
) {
let ty = TypeName::create(except_type);
match (ty, value) {
(_, Value::Variable(name)) => {
if let Some(var_type) = self.var_types.get(name.as_str()) {
if except_type != var_type {
ctx.report_error(
vec![pos],
format!(
"Variable \"{}\" of type \"{}\" used in position expecting type \"{}\"",
name, var_type, except_type
),
);
}
}
}
(TypeName::List(elem_type), Value::List(values)) => {
for value in values {
self.check_type(ctx, pos, elem_type, value);
}
}
(TypeName::NonNull(elem_type), value) => {
self.check_type(ctx, pos, elem_type, value);
}
_ => {}
}
}
}
impl<'a> Visitor<'a> for VariableInAllowedPosition<'a> {
fn enter_operation_definition(
&mut self,
_ctx: &mut ValidatorContext<'a>,
_operation_definition: &'a OperationDefinition,
) {
self.var_types.clear();
}
fn enter_variable_definition(
&mut self,
_ctx: &mut ValidatorContext<'a>,
variable_definition: &'a VariableDefinition,
) {
self.var_types.insert(
variable_definition.name.as_str(),
variable_definition.var_type.to_string(),
);
}
fn enter_directive(&mut self, ctx: &mut ValidatorContext<'a>, directive: &'a Directive) {
if let Some(schema_directive) = ctx.registry.directives.get(directive.name.as_str()) {
for (name, value) in &directive.arguments {
if let Some(input) = schema_directive.args.get(name.as_str()) {
self.check_type(ctx, directive.position, &input.ty, value);
}
}
}
}
fn enter_field(&mut self, ctx: &mut ValidatorContext<'a>, field: &'a Field) {
if let Some(parent_type) = ctx.parent_type() {
if let Some(schema_field) = parent_type.field_by_name(&field.name) {
for (name, value) in &field.arguments {
if let Some(arg) = schema_field.args.get(name.as_str()) {
self.check_type(ctx, field.position, &arg.ty, value);
}
}
}
}
}
}

View File

@ -17,7 +17,7 @@ pub fn is_valid_input_value(registry: &Registry, type_name: &str, value: &Value)
.all(|elem| is_valid_input_value(registry, type_name, elem)),
_ => false,
},
TypeName::Name(type_name) => {
TypeName::Named(type_name) => {
if let Value::Null = value {
return true;
}

View File

@ -376,15 +376,6 @@ fn visit_selection<'a, V: Visitor<'a>>(
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) => {