Validate fragment spread target existence

This commit is contained in:
Eugen Wissner 2020-08-31 11:06:27 +02:00
parent 4b59da2fcb
commit 33318a3b01
10 changed files with 109 additions and 23 deletions

View File

@ -6,28 +6,36 @@ The format is based on
and this project adheres to and this project adheres to
[Haskell Package Versioning Policy](https://pvp.haskell.org/). [Haskell Package Versioning Policy](https://pvp.haskell.org/).
## [Unreleased]
### Changed
- Added location information to `AST.Document.Selection.FragmentSpread`.
### Added
- `Validate.Validation.Rule`: `SelectionRule` constructor.
- `Validate.Rules`: `fragmentSpreadTargetDefinedRule`.
## [0.10.0.0] - 2020-08-29 ## [0.10.0.0] - 2020-08-29
## Changed ### Changed
- `Test.Hspec.GraphQL.*`: replace `IO` in the resolver with any `MonadCatch`. - `Test.Hspec.GraphQL.*`: replace `IO` in the resolver with any `MonadCatch`.
- The `Location` argument of `AST.Document.Definition.ExecutableDefinition` was - The `Location` argument of `AST.Document.Definition.ExecutableDefinition` was
moved to `OperationDefinition` and `FragmentDefinition` since these are the moved to `OperationDefinition` and `FragmentDefinition` since these are the
actual elements that have a location in the document. actual elements that have a location in the document.
- `Validate.Rules` get the whole validation context (AST and schema). - `Validate.Rules` get the whole validation context (AST and schema).
## Added ### Added
- `Validate.Validation` contains data structures and functions used by the - `Validate.Validation` contains data structures and functions used by the
validator and concretet rules. validator and concretet rules.
- `Validate.Rules`: operation validation rules. - `Validate.Rules`: operation validation rules.
## [0.9.0.0] - 2020-07-24 ## [0.9.0.0] - 2020-07-24
## Fixed ### Fixed
- Location of a parse error is returned in a singleton array with key - Location of a parse error is returned in a singleton array with key
`locations`. `locations`.
- Parsing comments in the front of definitions. - Parsing comments in the front of definitions.
- Some missing labels were added to the parsers, some labels were fixed to - Some missing labels were added to the parsers, some labels were fixed to
refer to the AST nodes being parsed. refer to the AST nodes being parsed.
## Added ### Added
- `AST` reexports `AST.Parser`. - `AST` reexports `AST.Parser`.
- `AST.Document.Location` is a token location as a line and column pair. - `AST.Document.Location` is a token location as a line and column pair.
- `Execute` reexports `Execute.Coerce`. - `Execute` reexports `Execute.Coerce`.
@ -43,7 +51,7 @@ and this project adheres to
- `Test.Hspec.GraphQL` contains some test helpers. - `Test.Hspec.GraphQL` contains some test helpers.
- `Validate` contains the validator and standard rules. - `Validate` contains the validator and standard rules.
## Changed ### Changed
- `Type.Out.Resolver`: Interface fields don't have resolvers, object fields - `Type.Out.Resolver`: Interface fields don't have resolvers, object fields
have value resolvers, root subscription type resolvers need an additional have value resolvers, root subscription type resolvers need an additional
resolver that creates an event stream. `Resolver` represents these differences resolver that creates an event stream. `Resolver` represents these differences
@ -67,7 +75,7 @@ and this project adheres to
- The constraint of the base monad was changed to `MonadCatch` (and it implies - The constraint of the base monad was changed to `MonadCatch` (and it implies
`MonadThrow`). `MonadThrow`).
## Removed ### Removed
- `Trans.ActionT` is an unneeded layer of complexity. `Type.Out.Resolver` - `Trans.ActionT` is an unneeded layer of complexity. `Type.Out.Resolver`
represents possible resolver configurations. represents possible resolver configurations.
- `Execute.executeWithName`. `Execute.execute` takes the operation name and - `Execute.executeWithName`. `Execute.execute` takes the operation name and
@ -334,6 +342,7 @@ and this project adheres to
### Added ### Added
- Data types for the GraphQL language. - Data types for the GraphQL language.
[Unreleased]: https://github.com/caraus-ecms/graphql/compare/v0.10.0.0...HEAD
[0.10.0.0]: https://github.com/caraus-ecms/graphql/compare/v0.9.0.0...v0.10.0.0 [0.10.0.0]: https://github.com/caraus-ecms/graphql/compare/v0.9.0.0...v0.10.0.0
[0.9.0.0]: https://github.com/caraus-ecms/graphql/compare/v0.8.0.0...v0.9.0.0 [0.9.0.0]: https://github.com/caraus-ecms/graphql/compare/v0.8.0.0...v0.9.0.0
[0.8.0.0]: https://github.com/caraus-ecms/graphql/compare/v0.7.0.0...v0.8.0.0 [0.8.0.0]: https://github.com/caraus-ecms/graphql/compare/v0.7.0.0...v0.8.0.0

View File

@ -163,7 +163,7 @@ type SelectionSetOpt = [Selection]
-- @ -- @
data Selection data Selection
= Field (Maybe Alias) Name [Argument] [Directive] SelectionSetOpt = Field (Maybe Alias) Name [Argument] [Directive] SelectionSetOpt
| FragmentSpread Name [Directive] | FragmentSpread Name [Directive] Location
| InlineFragment (Maybe TypeCondition) [Directive] SelectionSet | InlineFragment (Maybe TypeCondition) [Directive] SelectionSet
deriving (Eq, Show) deriving (Eq, Show)

View File

@ -130,7 +130,7 @@ selection formatter = Lazy.Text.append indent' . encodeSelection
field incrementIndent alias name args directives' selections field incrementIndent alias name args directives' selections
encodeSelection (InlineFragment typeCondition directives' selections) = encodeSelection (InlineFragment typeCondition directives' selections) =
inlineFragment incrementIndent typeCondition directives' selections inlineFragment incrementIndent typeCondition directives' selections
encodeSelection (FragmentSpread name directives') = encodeSelection (FragmentSpread name directives' _) =
fragmentSpread incrementIndent name directives' fragmentSpread incrementIndent name directives'
incrementIndent incrementIndent
| Pretty indentation <- formatter = Pretty $ indentation + 1 | Pretty indentation <- formatter = Pretty $ indentation + 1

View File

@ -400,11 +400,12 @@ argument :: Parser Argument
argument = Argument <$> name <* colon <*> value <?> "Argument" argument = Argument <$> name <* colon <*> value <?> "Argument"
fragmentSpread :: Parser Selection fragmentSpread :: Parser Selection
fragmentSpread = FragmentSpread fragmentSpread = label "FragmentSpread" $ do
<$ spread location <- getLocation
<*> fragmentName _ <- spread
<*> directives fragmentName' <- fragmentName
<?> "FragmentSpread" directives' <- directives
pure $ FragmentSpread fragmentName' directives' location
inlineFragment :: Parser Selection inlineFragment :: Parser Selection
inlineFragment = InlineFragment inlineFragment = InlineFragment

View File

@ -299,7 +299,7 @@ selection (Full.Field alias name arguments' directives' selections) =
go arguments (Full.Argument name' value') = go arguments (Full.Argument name' value') =
inputField arguments name' value' inputField arguments name' value'
selection (Full.FragmentSpread name directives') = selection (Full.FragmentSpread name directives' _) =
maybe (Left mempty) (Right . SelectionFragment) <$> do maybe (Left mempty) (Right . SelectionFragment) <$> do
spreadDirectives <- Definition.selection <$> directives directives' spreadDirectives <- Definition.selection <$> directives directives'
fragments' <- gets fragments fragments' <- gets fragments

View File

@ -15,7 +15,7 @@ module Language.GraphQL.Validate
import Control.Monad (foldM) import Control.Monad (foldM)
import Control.Monad.Trans.Reader (Reader, asks, mapReaderT, runReader) import Control.Monad.Trans.Reader (Reader, asks, mapReaderT, runReader)
import Data.Foldable (foldrM) import Data.Foldable (fold, foldrM)
import Data.Sequence (Seq(..), (><), (|>)) import Data.Sequence (Seq(..), (><), (|>))
import qualified Data.Sequence as Seq import qualified Data.Sequence as Seq
import Language.GraphQL.AST.Document import Language.GraphQL.AST.Document
@ -66,15 +66,41 @@ executableDefinition (DefinitionFragment definition') =
operationDefinition :: forall m. OperationDefinition -> ValidateT m operationDefinition :: forall m. OperationDefinition -> ValidateT m
operationDefinition operation = operationDefinition operation =
asks rules >>= foldM ruleFilter Seq.empty let selectionSet = getSelectionSet operation
in visitChildSelections ruleFilter selectionSet
where where
ruleFilter accumulator (OperationDefinitionRule rule) = ruleFilter accumulator (OperationDefinitionRule rule) =
mapReaderT (runRule accumulator) $ rule operation mapReaderT (runRule accumulator) $ rule operation
ruleFilter accumulator _ = pure accumulator ruleFilter accumulator _ = pure accumulator
getSelectionSet (SelectionSet selectionSet _) = selectionSet
getSelectionSet (OperationDefinition _ _ _ _ selectionSet _) = selectionSet
selection :: forall m. Selection -> ValidateT m
selection selection'@FragmentSpread{} =
asks rules >>= foldM ruleFilter Seq.empty
where
ruleFilter accumulator (SelectionRule rule) =
mapReaderT (runRule accumulator) $ rule selection'
ruleFilter accumulator _ = pure accumulator
selection (Field _ _ _ _ selectionSet) = traverseSelectionSet selectionSet
selection (InlineFragment _ _ selectionSet) = traverseSelectionSet selectionSet
traverseSelectionSet :: Traversable t => forall m. t Selection -> ValidateT m
traverseSelectionSet = fmap fold . traverse selection
visitChildSelections :: Traversable t
=> (Seq Error -> Rule m -> ValidateT m)
-> t Selection
-> ValidateT m
visitChildSelections ruleFilter selectionSet = do
rules' <- asks rules
applied <- foldM ruleFilter Seq.empty rules'
children <- traverseSelectionSet selectionSet
pure $ children >< applied
fragmentDefinition :: forall m. FragmentDefinition -> ValidateT m fragmentDefinition :: forall m. FragmentDefinition -> ValidateT m
fragmentDefinition fragment = fragmentDefinition fragment@(FragmentDefinition _ _ _ selectionSet _) =
asks rules >>= foldM ruleFilter Seq.empty visitChildSelections ruleFilter selectionSet
where where
ruleFilter accumulator (FragmentDefinitionRule rule) = ruleFilter accumulator (FragmentDefinitionRule rule) =
mapReaderT (runRule accumulator) $ rule fragment mapReaderT (runRule accumulator) $ rule fragment

View File

@ -21,6 +21,7 @@ import Control.Monad (foldM)
import Control.Monad.Trans.Class (MonadTrans(..)) import Control.Monad.Trans.Class (MonadTrans(..))
import Control.Monad.Trans.Reader (asks) import Control.Monad.Trans.Reader (asks)
import Control.Monad.Trans.State (evalStateT, gets, modify) import Control.Monad.Trans.State (evalStateT, gets, modify)
import Data.Foldable (find)
import qualified Data.HashSet as HashSet import qualified Data.HashSet as HashSet
import qualified Data.Text as Text import qualified Data.Text as Text
import Language.GraphQL.AST.Document import Language.GraphQL.AST.Document
@ -36,6 +37,7 @@ specifiedRules =
, loneAnonymousOperationRule , loneAnonymousOperationRule
, uniqueOperationNamesRule , uniqueOperationNamesRule
, uniqueFragmentNamesRule , uniqueFragmentNamesRule
, fragmentSpreadTargetDefinedRule
] ]
-- | Definition must be OperationDefinition or FragmentDefinition. -- | Definition must be OperationDefinition or FragmentDefinition.
@ -84,7 +86,7 @@ singleFieldSubscriptionsRule = OperationDefinitionRule $ \case
| Just aliasedName <- alias = pure | Just aliasedName <- alias = pure
$ HashSet.insert aliasedName accumulator $ HashSet.insert aliasedName accumulator
| otherwise = pure $ HashSet.insert name accumulator | otherwise = pure $ HashSet.insert name accumulator
forEach accumulator (FragmentSpread fragmentName directives) forEach accumulator (FragmentSpread fragmentName directives _)
| any skip directives = pure accumulator | any skip directives = pure accumulator
| otherwise = do | otherwise = do
inVisitetFragments <- gets $ HashSet.member fragmentName inVisitetFragments <- gets $ HashSet.member fragmentName
@ -192,6 +194,13 @@ viewOperation definition
Just operationDefinition Just operationDefinition
viewOperation _ = Nothing viewOperation _ = Nothing
viewFragment :: Definition -> Maybe FragmentDefinition
viewFragment definition
| ExecutableDefinition executableDefinition <- definition
, DefinitionFragment fragmentDefinition <- executableDefinition =
Just fragmentDefinition
viewFragment _ = Nothing
-- | Fragment definitions are referenced in fragment spreads by name. To avoid -- | Fragment definitions are referenced in fragment spreads by name. To avoid
-- ambiguity, each fragments name must be unique within a document. -- ambiguity, each fragments name must be unique within a document.
-- --
@ -208,8 +217,32 @@ uniqueFragmentNamesRule = FragmentDefinitionRule $ \case
, "\"." , "\"."
] ]
filterByName thisName definition accumulator filterByName thisName definition accumulator
| ExecutableDefinition executableDefinition <- definition | Just fragmentDefinition <- viewFragment definition
, DefinitionFragment fragmentDefinition <- executableDefinition
, FragmentDefinition thatName _ _ _ thatLocation <- fragmentDefinition , FragmentDefinition thatName _ _ _ thatLocation <- fragmentDefinition
, thisName == thatName = thatLocation : accumulator , thisName == thatName = thatLocation : accumulator
| otherwise = accumulator | otherwise = accumulator
-- | Named fragment spreads must refer to fragments defined within the document.
-- It is a validation error if the target of a spread is not defined.
fragmentSpreadTargetDefinedRule :: forall m. Rule m
fragmentSpreadTargetDefinedRule = SelectionRule $ \case
FragmentSpread fragmentName _ location -> do
ast' <- asks ast
case find (findTarget fragmentName) ast' of
Nothing -> pure $ Error
{ message = error' fragmentName
, locations = [location]
, path = []
}
Just _ -> lift Nothing
_ -> lift Nothing
where
error' fragmentName = concat
[ "Fragment target \""
, Text.unpack fragmentName
, "\" is undefined."
]
findTarget thisName (viewFragment -> Just fragmentDefinition)
| FragmentDefinition thatName _ _ _ _ <- fragmentDefinition
, thisName == thatName = True
findTarget _ _ = False

View File

@ -49,6 +49,7 @@ data Rule m
= DefinitionRule (Definition -> RuleT m) = DefinitionRule (Definition -> RuleT m)
| OperationDefinitionRule (OperationDefinition -> RuleT m) | OperationDefinitionRule (OperationDefinition -> RuleT m)
| FragmentDefinitionRule (FragmentDefinition -> RuleT m) | FragmentDefinitionRule (FragmentDefinition -> RuleT m)
| SelectionRule (Selection -> RuleT m)
-- | Monad transformer used by the rules. -- | Monad transformer used by the rules.
type RuleT m = ReaderT (Validation m) Maybe Error type RuleT m = ReaderT (Validation m) Maybe Error

View File

@ -1,4 +1,4 @@
resolver: lts-16.11 resolver: lts-16.12
packages: packages:
- . - .
@ -6,4 +6,4 @@ packages:
extra-deps: [] extra-deps: []
flags: {} flags: {}
pvp-bounds: both pvp-bounds: lower

View File

@ -281,3 +281,19 @@ spec =
, path = [] , path = []
} }
in validate queryString `shouldBe` Seq.singleton expected in validate queryString `shouldBe` Seq.singleton expected
it "rejects the fragment spread without a target" $
let queryString = [r|
{
dog {
...undefinedFragment
}
}
|]
expected = Error
{ message =
"Fragment target \"undefinedFragment\" is undefined."
, locations = [AST.Location 4 19]
, path = []
}
in validate queryString `shouldBe` Seq.singleton expected