Validate arguments have unique names
This commit is contained in:
parent
6e644c5b4b
commit
497b93c41b
@ -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`.
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
resolver: lts-16.13
|
resolver: lts-16.14
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
- .
|
- .
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user