add some validation rules
This commit is contained in:
parent
678733725f
commit
ecc861de3b
10
README.md
10
README.md
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
/// }
|
||||
|
|
|
@ -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(®istry.types[ty]),
|
||||
},
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
23
src/validation/rules/scalar_leafs.rs
Normal file
23
src/validation/rules/scalar_leafs.rs
Normal 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()
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
),
|
||||
)
|
||||
|
|
39
src/validation/rules/unique_operation_names.rs
Normal file
39
src/validation/rules/unique_operation_names.rs
Normal 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
src/validation/rules/unique_variable_names.rs
Normal file
35
src/validation/rules/unique_variable_names.rs
Normal 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
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
30
src/validation/rules/variables_are_input_types.rs
Normal file
30
src/validation/rules/variables_are_input_types.rs
Normal 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()
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
92
src/validation/rules/variables_in_allowed_position.rs
Normal file
92
src/validation/rules/variables_in_allowed_position.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
Loading…
Reference in New Issue
Block a user