Validate arguments have unique names

This commit is contained in:
Eugen Wissner 2020-09-17 10:33:37 +02:00
parent 6e644c5b4b
commit 497b93c41b
6 changed files with 160 additions and 21 deletions

View File

@ -24,13 +24,14 @@ and this project adheres to
### Added ### Added
- `Validate.Validation.Rule`: `SelectionRule`, `FieldRule`, `FragmentRule`, - `Validate.Validation.Rule`: `SelectionRule`, `FieldRule`, `FragmentRule`,
`FragmentSpreadRule` constructors. `FragmentSpreadRule`, `ArgumentsRule` constructors.
- `Validate.Rules`: - `Validate.Rules`:
- `fragmentsOnCompositeTypesRule` - `fragmentsOnCompositeTypesRule`
- `fragmentSpreadTargetDefinedRule` - `fragmentSpreadTargetDefinedRule`
- `fragmentSpreadTypeExistenceRule` - `fragmentSpreadTypeExistenceRule`
- `noUnusedFragmentsRule` - `noUnusedFragmentsRule`
- `noFragmentCyclesRule` - `noFragmentCyclesRule`
- `uniqueArgumentNamesRule`
- `AST.Document.Field`. - `AST.Document.Field`.
- `AST.Document.FragmentSpread`. - `AST.Document.FragmentSpread`.
- `AST.Document.InlineFragment`. - `AST.Document.InlineFragment`.

View File

@ -3,6 +3,7 @@
obtain one at https://mozilla.org/MPL/2.0/. -} obtain one at https://mozilla.org/MPL/2.0/. -}
{-# LANGUAGE ExplicitForAll #-} {-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE LambdaCase #-}
-- | GraphQL validator. -- | GraphQL validator.
module Language.GraphQL.Validate module Language.GraphQL.Validate
@ -38,11 +39,47 @@ document schema' rules' document' =
join $ lift $ foldr (definition rule') Seq.empty document' join $ lift $ foldr (definition rule') Seq.empty document'
definition :: Rule m -> Definition -> Seq (RuleT m) -> Seq (RuleT m) definition :: Rule m -> Definition -> Seq (RuleT m) -> Seq (RuleT m)
definition (DefinitionRule rule) definition' acc = definition (DefinitionRule rule) definition' accumulator =
acc |> rule definition' accumulator |> rule definition'
definition rule (ExecutableDefinition executableDefinition') acc = definition rule (ExecutableDefinition executableDefinition') accumulator =
acc >< executableDefinition rule executableDefinition' accumulator >< executableDefinition rule executableDefinition'
definition _ _ acc = acc definition rule (TypeSystemDefinition typeSystemDefinition' _) accumulator =
accumulator >< typeSystemDefinition rule typeSystemDefinition'
definition rule (TypeSystemExtension extension _) accumulator =
accumulator >< typeSystemExtension rule extension
typeSystemExtension :: Rule m -> TypeSystemExtension -> Seq (RuleT m)
typeSystemExtension rule = \case
SchemaExtension extension -> schemaExtension rule extension
TypeExtension extension -> typeExtension rule extension
typeExtension :: Rule m -> TypeExtension -> Seq (RuleT m)
typeExtension rule = \case
ScalarTypeExtension _ directives' -> directives rule directives'
ObjectTypeFieldsDefinitionExtension _ _ directives' fields ->
directives rule directives' >< foldMap (fieldDefinition rule) fields
ObjectTypeDirectivesExtension _ _ directives' -> directives rule directives'
ObjectTypeImplementsInterfacesExtension _ _ -> mempty
InterfaceTypeFieldsDefinitionExtension _ directives' fields ->
directives rule directives' >< foldMap (fieldDefinition rule) fields
InterfaceTypeDirectivesExtension _ directives' ->
directives rule directives'
UnionTypeUnionMemberTypesExtension _ directives' _ ->
directives rule directives'
UnionTypeDirectivesExtension _ directives' -> directives rule directives'
EnumTypeEnumValuesDefinitionExtension _ directives' values ->
directives rule directives' >< foldMap (enumValueDefinition rule) values
EnumTypeDirectivesExtension _ directives' -> directives rule directives'
InputObjectTypeInputFieldsDefinitionExtension _ directives' fields
-> directives rule directives'
>< foldMap (inputValueDefinition rule) fields
InputObjectTypeDirectivesExtension _ directives' ->
directives rule directives'
schemaExtension :: Rule m -> SchemaExtension -> Seq (RuleT m)
schemaExtension rule = \case
SchemaOperationExtension directives' _ -> directives rule directives'
SchemaDirectivesExtension directives' -> directives rule directives'
executableDefinition :: Rule m -> ExecutableDefinition -> Seq (RuleT m) executableDefinition :: Rule m -> ExecutableDefinition -> Seq (RuleT m)
executableDefinition rule (DefinitionOperation operation) = executableDefinition rule (DefinitionOperation operation) =
@ -50,23 +87,60 @@ executableDefinition rule (DefinitionOperation operation) =
executableDefinition rule (DefinitionFragment fragment) = executableDefinition rule (DefinitionFragment fragment) =
fragmentDefinition rule fragment fragmentDefinition rule fragment
typeSystemDefinition :: Rule m -> TypeSystemDefinition -> Seq (RuleT m)
typeSystemDefinition rule = \case
SchemaDefinition directives' _ -> directives rule directives'
TypeDefinition typeDefinition' -> typeDefinition rule typeDefinition'
DirectiveDefinition _ _ arguments _ -> argumentsDefinition rule arguments
typeDefinition :: Rule m -> TypeDefinition -> Seq (RuleT m)
typeDefinition rule = \case
ScalarTypeDefinition _ _ directives' -> directives rule directives'
ObjectTypeDefinition _ _ _ directives' fields ->
directives rule directives' >< foldMap (fieldDefinition rule) fields
InterfaceTypeDefinition _ _ directives' fields ->
directives rule directives' >< foldMap (fieldDefinition rule) fields
UnionTypeDefinition _ _ directives' _ -> directives rule directives'
EnumTypeDefinition _ _ directives' values ->
directives rule directives' >< foldMap (enumValueDefinition rule) values
InputObjectTypeDefinition _ _ directives' fields
-> directives rule directives'
<> foldMap (inputValueDefinition rule) fields
enumValueDefinition :: Rule m -> EnumValueDefinition -> Seq (RuleT m)
enumValueDefinition rule (EnumValueDefinition _ _ directives') =
directives rule directives'
fieldDefinition :: Rule m -> FieldDefinition -> Seq (RuleT m)
fieldDefinition rule (FieldDefinition _ _ arguments _ directives') =
directives rule directives' >< argumentsDefinition rule arguments
argumentsDefinition :: Rule m -> ArgumentsDefinition -> Seq (RuleT m)
argumentsDefinition rule (ArgumentsDefinition definitions) =
foldMap (inputValueDefinition rule) definitions
inputValueDefinition :: Rule m -> InputValueDefinition -> Seq (RuleT m)
inputValueDefinition rule (InputValueDefinition _ _ _ _ directives') =
directives rule directives'
operationDefinition :: Rule m -> OperationDefinition -> Seq (RuleT m) operationDefinition :: Rule m -> OperationDefinition -> Seq (RuleT m)
operationDefinition (OperationDefinitionRule rule) operationDefinition' = operationDefinition (OperationDefinitionRule rule) operationDefinition' =
pure $ rule operationDefinition' pure $ rule operationDefinition'
operationDefinition rule (SelectionSet selections _) = operationDefinition rule (SelectionSet selections _) =
selectionSet rule selections selectionSet rule selections
operationDefinition rule (OperationDefinition _ _ _ _ selections _) = operationDefinition rule (OperationDefinition _ _ _ directives' selections _) =
selectionSet rule selections selectionSet rule selections >< directives rule directives'
fragmentDefinition :: Rule m -> FragmentDefinition -> Seq (RuleT m) fragmentDefinition :: Rule m -> FragmentDefinition -> Seq (RuleT m)
fragmentDefinition (FragmentDefinitionRule rule) fragmentDefinition' = fragmentDefinition (FragmentDefinitionRule rule) fragmentDefinition' =
pure $ rule fragmentDefinition' pure $ rule fragmentDefinition'
fragmentDefinition rule fragmentDefinition'@(FragmentDefinition _ _ _ selections _) fragmentDefinition rule fragmentDefinition'@(FragmentDefinition _ _ directives' selections _)
| FragmentRule definitionRule _ <- rule = | FragmentRule definitionRule _ <- rule =
applyToChildren |> definitionRule fragmentDefinition' applyToChildren |> definitionRule fragmentDefinition'
| otherwise = applyToChildren | otherwise = applyToChildren
where where
applyToChildren = selectionSet rule selections applyToChildren = selectionSet rule selections
>< directives rule directives'
selectionSet :: Traversable t => Rule m -> t Selection -> Seq (RuleT m) selectionSet :: Traversable t => Rule m -> t Selection -> Seq (RuleT m)
selectionSet = foldMap . selection selectionSet = foldMap . selection
@ -83,18 +157,36 @@ selection rule selection'
InlineFragmentSelection inlineFragment' -> InlineFragmentSelection inlineFragment' ->
inlineFragment rule inlineFragment' inlineFragment rule inlineFragment'
FragmentSpreadSelection fragmentSpread' -> FragmentSpreadSelection fragmentSpread' ->
pure $ fragmentSpread rule fragmentSpread' fragmentSpread rule fragmentSpread'
field :: Rule m -> Field -> Seq (RuleT m) field :: Rule m -> Field -> Seq (RuleT m)
field (FieldRule rule) field' = pure $ rule field' field rule field'@(Field _ _ _ directives' selections _)
field rule (Field _ _ _ _ selections _) = selectionSet rule selections | FieldRule fieldRule <- rule = applyToChildren |> fieldRule field'
| ArgumentsRule fieldRule _ <- rule = applyToChildren |> fieldRule field'
| otherwise = applyToChildren
where
applyToChildren = selectionSet rule selections >< directives rule directives'
inlineFragment :: Rule m -> InlineFragment -> Seq (RuleT m) inlineFragment :: Rule m -> InlineFragment -> Seq (RuleT m)
inlineFragment (FragmentRule _ rule) inlineFragment' = inlineFragment rule inlineFragment'@(InlineFragment _ directives' selections _)
pure $ rule inlineFragment' | FragmentRule _ fragmentRule <- rule =
inlineFragment rule (InlineFragment _ _ selections _) = applyToChildren |> fragmentRule inlineFragment'
selectionSet rule selections | otherwise = applyToChildren
where
applyToChildren = selectionSet rule selections
>< directives rule directives'
fragmentSpread :: Rule m -> FragmentSpread -> RuleT m fragmentSpread :: Rule m -> FragmentSpread -> Seq (RuleT m)
fragmentSpread (FragmentSpreadRule rule) fragmentSpread' = rule fragmentSpread' fragmentSpread rule fragmentSpread'@(FragmentSpread _ directives' _)
fragmentSpread _ _ = lift mempty | FragmentSpreadRule fragmentRule <- rule =
applyToChildren |> fragmentRule fragmentSpread'
| otherwise = applyToChildren
where
applyToChildren = directives rule directives'
directives :: Traversable t => Rule m -> t Directive -> Seq (RuleT m)
directives = foldMap . fmap pure . directive
directive :: Rule m -> Directive -> RuleT m
directive (ArgumentsRule _ rule) = rule
directive _ = lift . const mempty

View File

@ -18,6 +18,7 @@ module Language.GraphQL.Validate.Rules
, noUnusedFragmentsRule , noUnusedFragmentsRule
, singleFieldSubscriptionsRule , singleFieldSubscriptionsRule
, specifiedRules , specifiedRules
, uniqueArgumentNamesRule
, uniqueFragmentNamesRule , uniqueFragmentNamesRule
, uniqueOperationNamesRule , uniqueOperationNamesRule
) where ) where
@ -31,9 +32,10 @@ import Data.Foldable (find)
import qualified Data.HashMap.Strict as HashMap import qualified Data.HashMap.Strict as HashMap
import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict (HashMap)
import qualified Data.HashSet as HashSet import qualified Data.HashSet as HashSet
import Data.List (sortBy) import Data.List (groupBy, sortBy, sortOn)
import Data.Ord (comparing) import Data.Ord (comparing)
import Data.Sequence (Seq(..)) import Data.Sequence (Seq(..))
import qualified Data.Sequence as Seq
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as Text import qualified Data.Text as Text
import Language.GraphQL.AST.Document import Language.GraphQL.AST.Document
@ -50,6 +52,8 @@ specifiedRules =
, singleFieldSubscriptionsRule , singleFieldSubscriptionsRule
, loneAnonymousOperationRule , loneAnonymousOperationRule
, uniqueOperationNamesRule , uniqueOperationNamesRule
-- Arguments.
, uniqueArgumentNamesRule
-- Fragments. -- Fragments.
, uniqueFragmentNamesRule , uniqueFragmentNamesRule
, fragmentSpreadTypeExistenceRule , fragmentSpreadTypeExistenceRule
@ -441,3 +445,29 @@ noFragmentCyclesRule = FragmentDefinitionRule $ \case
Nothing -> pure accumulator Nothing -> pure accumulator
Just (FragmentDefinition _ _ _ selections _) -> Just (FragmentDefinition _ _ _ selections _) ->
(accumulator <>) <$> collectFields selections (accumulator <>) <$> collectFields selections
-- | Fields and directives treat arguments as a mapping of argument name to
-- value. More than one argument with the same name in an argument set is
-- ambiguous and invalid.
uniqueArgumentNamesRule :: forall m. Rule m
uniqueArgumentNamesRule = ArgumentsRule fieldRule directiveRule
where
fieldRule (Field _ _ arguments _ _ _) = filterDuplicates arguments
directiveRule (Directive _ arguments) = filterDuplicates arguments
filterDuplicates = lift
. Seq.fromList
. fmap makeError
. filter ((> 1) . length)
. groupBy equalByName
. sortOn getName
getName (Argument argumentName _ _) = argumentName
makeError arguments = Error
{ message = makeMessage $ head arguments
, locations = (\(Argument _ _ location) -> location) <$> arguments
}
makeMessage argument = concat
[ "There can be only one argument named \""
, Text.unpack $ getName argument
, "\"."
]
equalByName lhs rhs = getName lhs == getName rhs

View File

@ -41,6 +41,7 @@ data Rule m
| FragmentRule (FragmentDefinition -> RuleT m) (InlineFragment -> RuleT m) | FragmentRule (FragmentDefinition -> RuleT m) (InlineFragment -> RuleT m)
| FragmentSpreadRule (FragmentSpread -> RuleT m) | FragmentSpreadRule (FragmentSpread -> RuleT m)
| FieldRule (Field -> RuleT m) | FieldRule (Field -> RuleT m)
| ArgumentsRule (Field -> RuleT m) (Directive -> RuleT m)
-- | Monad transformer used by the rules. -- | Monad transformer used by the rules.
type RuleT m = ReaderT (Validation m) Seq Error type RuleT m = ReaderT (Validation m) Seq Error

View File

@ -1,4 +1,4 @@
resolver: lts-16.13 resolver: lts-16.14
packages: packages:
- . - .

View File

@ -413,3 +413,18 @@ spec =
, locations = [AST.Location 7 15] , locations = [AST.Location 7 15]
} }
in validate queryString `shouldBe` Seq.fromList [error1, error2] in validate queryString `shouldBe` Seq.fromList [error1, error2]
it "rejects duplicate field arguments" $ do
let queryString = [r|
{
dog {
isHousetrained(atOtherHomes: true, atOtherHomes: true)
}
}
|]
expected = Error
{ message =
"There can be only one argument named \"atOtherHomes\"."
, locations = [AST.Location 4 34, AST.Location 4 54]
}
in validate queryString `shouldBe` Seq.singleton expected