2020-07-20 21:29:12 +02:00
|
|
|
|
{- This Source Code Form is subject to the terms of the Mozilla Public License,
|
|
|
|
|
v. 2.0. If a copy of the MPL was not distributed with this file, You can
|
|
|
|
|
obtain one at https://mozilla.org/MPL/2.0/. -}
|
|
|
|
|
|
2020-08-26 18:58:48 +02:00
|
|
|
|
{-# LANGUAGE LambdaCase #-}
|
2021-02-03 05:42:10 +01:00
|
|
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
2020-08-25 21:03:42 +02:00
|
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
2020-09-30 05:14:52 +02:00
|
|
|
|
{-# LANGUAGE RecordWildCards #-}
|
2020-09-21 07:28:40 +02:00
|
|
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
2020-08-27 09:04:31 +02:00
|
|
|
|
{-# LANGUAGE ViewPatterns #-}
|
2020-08-25 21:03:42 +02:00
|
|
|
|
|
2020-07-24 21:34:31 +02:00
|
|
|
|
-- | This module contains default rules defined in the GraphQL specification.
|
2020-07-20 21:29:12 +02:00
|
|
|
|
module Language.GraphQL.Validate.Rules
|
2020-10-02 06:31:38 +02:00
|
|
|
|
( directivesInValidLocationsRule
|
|
|
|
|
, executableDefinitionsRule
|
2020-09-25 21:57:25 +02:00
|
|
|
|
, fieldsOnCorrectTypeRule
|
2020-09-07 22:01:49 +02:00
|
|
|
|
, fragmentsOnCompositeTypesRule
|
2020-09-04 19:12:19 +02:00
|
|
|
|
, fragmentSpreadTargetDefinedRule
|
|
|
|
|
, fragmentSpreadTypeExistenceRule
|
2020-08-27 09:04:31 +02:00
|
|
|
|
, loneAnonymousOperationRule
|
2020-09-28 07:06:15 +02:00
|
|
|
|
, knownArgumentNamesRule
|
2020-09-29 06:21:32 +02:00
|
|
|
|
, knownDirectiveNamesRule
|
2020-09-30 05:14:52 +02:00
|
|
|
|
, knownInputFieldNamesRule
|
2020-09-11 08:03:49 +02:00
|
|
|
|
, noFragmentCyclesRule
|
2020-09-21 07:28:40 +02:00
|
|
|
|
, noUndefinedVariablesRule
|
2020-09-09 17:04:31 +02:00
|
|
|
|
, noUnusedFragmentsRule
|
2020-09-22 04:42:25 +02:00
|
|
|
|
, noUnusedVariablesRule
|
2020-11-15 10:11:09 +01:00
|
|
|
|
, overlappingFieldsCanBeMergedRule
|
2020-11-19 08:48:37 +01:00
|
|
|
|
, possibleFragmentSpreadsRule
|
2020-10-04 18:51:21 +02:00
|
|
|
|
, providedRequiredInputFieldsRule
|
2020-10-03 07:34:34 +02:00
|
|
|
|
, providedRequiredArgumentsRule
|
2020-09-26 09:06:30 +02:00
|
|
|
|
, scalarLeafsRule
|
2020-08-27 09:04:31 +02:00
|
|
|
|
, singleFieldSubscriptionsRule
|
2020-07-20 21:29:12 +02:00
|
|
|
|
, specifiedRules
|
2020-09-17 10:33:37 +02:00
|
|
|
|
, uniqueArgumentNamesRule
|
2020-09-18 07:32:58 +02:00
|
|
|
|
, uniqueDirectiveNamesRule
|
2020-08-28 08:32:21 +02:00
|
|
|
|
, uniqueFragmentNamesRule
|
2020-09-24 05:47:31 +02:00
|
|
|
|
, uniqueInputFieldNamesRule
|
2020-08-27 09:04:31 +02:00
|
|
|
|
, uniqueOperationNamesRule
|
2020-09-19 18:18:26 +02:00
|
|
|
|
, uniqueVariableNamesRule
|
2021-02-03 05:42:10 +01:00
|
|
|
|
, valuesOfCorrectTypeRule
|
2020-12-26 06:31:56 +01:00
|
|
|
|
, variablesInAllowedPositionRule
|
2020-09-20 06:59:27 +02:00
|
|
|
|
, variablesAreInputTypesRule
|
2020-07-20 21:29:12 +02:00
|
|
|
|
) where
|
|
|
|
|
|
2020-09-20 06:59:27 +02:00
|
|
|
|
import Control.Monad ((>=>), foldM)
|
2020-08-25 21:03:42 +02:00
|
|
|
|
import Control.Monad.Trans.Class (MonadTrans(..))
|
2020-11-15 10:11:09 +01:00
|
|
|
|
import Control.Monad.Trans.Reader (ReaderT(..), ask, asks, mapReaderT)
|
2020-09-11 08:03:49 +02:00
|
|
|
|
import Control.Monad.Trans.State (StateT, evalStateT, gets, modify)
|
|
|
|
|
import Data.Bifunctor (first)
|
2020-12-26 06:31:56 +01:00
|
|
|
|
import Data.Foldable (find, fold, foldl', toList)
|
2020-09-04 19:12:19 +02:00
|
|
|
|
import qualified Data.HashMap.Strict as HashMap
|
2020-09-11 08:03:49 +02:00
|
|
|
|
import Data.HashMap.Strict (HashMap)
|
2020-09-21 07:28:40 +02:00
|
|
|
|
import Data.HashSet (HashSet)
|
2020-08-25 21:03:42 +02:00
|
|
|
|
import qualified Data.HashSet as HashSet
|
2020-09-17 10:33:37 +02:00
|
|
|
|
import Data.List (groupBy, sortBy, sortOn)
|
2021-02-03 05:42:10 +01:00
|
|
|
|
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, mapMaybe)
|
2020-11-15 10:11:09 +01:00
|
|
|
|
import Data.List.NonEmpty (NonEmpty(..))
|
2020-09-11 08:03:49 +02:00
|
|
|
|
import Data.Ord (comparing)
|
2020-09-28 07:06:15 +02:00
|
|
|
|
import Data.Sequence (Seq(..), (|>))
|
2020-09-17 10:33:37 +02:00
|
|
|
|
import qualified Data.Sequence as Seq
|
2020-09-04 19:12:19 +02:00
|
|
|
|
import Data.Text (Text)
|
2020-08-25 21:03:42 +02:00
|
|
|
|
import qualified Data.Text as Text
|
2020-10-07 05:24:51 +02:00
|
|
|
|
import qualified Language.GraphQL.AST.Document as Full
|
2020-09-25 21:57:25 +02:00
|
|
|
|
import qualified Language.GraphQL.Type.Definition as Definition
|
2020-10-07 05:24:51 +02:00
|
|
|
|
import qualified Language.GraphQL.Type.Internal as Type
|
2020-09-30 05:14:52 +02:00
|
|
|
|
import qualified Language.GraphQL.Type.In as In
|
2020-09-25 21:57:25 +02:00
|
|
|
|
import qualified Language.GraphQL.Type.Out as Out
|
2020-08-25 21:03:42 +02:00
|
|
|
|
import qualified Language.GraphQL.Type.Schema as Schema
|
|
|
|
|
import Language.GraphQL.Validate.Validation
|
2020-07-20 21:29:12 +02:00
|
|
|
|
|
2020-09-21 07:28:40 +02:00
|
|
|
|
-- Local help type that contains a hash set to track visited fragments.
|
2020-10-07 05:24:51 +02:00
|
|
|
|
type ValidationState m a =
|
|
|
|
|
StateT (HashSet Full.Name) (ReaderT (Validation m) Seq) a
|
2020-09-21 07:28:40 +02:00
|
|
|
|
|
2020-08-22 06:39:52 +02:00
|
|
|
|
-- | Default rules given in the specification.
|
2020-08-25 21:03:42 +02:00
|
|
|
|
specifiedRules :: forall m. [Rule m]
|
2020-07-20 21:29:12 +02:00
|
|
|
|
specifiedRules =
|
2020-09-04 19:12:19 +02:00
|
|
|
|
-- Documents.
|
2020-07-20 21:29:12 +02:00
|
|
|
|
[ executableDefinitionsRule
|
2020-09-04 19:12:19 +02:00
|
|
|
|
-- Operations.
|
2020-08-25 21:03:42 +02:00
|
|
|
|
, singleFieldSubscriptionsRule
|
2020-08-26 18:58:48 +02:00
|
|
|
|
, loneAnonymousOperationRule
|
2020-08-27 09:04:31 +02:00
|
|
|
|
, uniqueOperationNamesRule
|
2020-09-25 21:57:25 +02:00
|
|
|
|
-- Fields
|
|
|
|
|
, fieldsOnCorrectTypeRule
|
2020-09-26 09:06:30 +02:00
|
|
|
|
, scalarLeafsRule
|
2020-11-15 10:11:09 +01:00
|
|
|
|
, overlappingFieldsCanBeMergedRule
|
2020-09-17 10:33:37 +02:00
|
|
|
|
-- Arguments.
|
2020-09-28 07:06:15 +02:00
|
|
|
|
, knownArgumentNamesRule
|
2020-09-17 10:33:37 +02:00
|
|
|
|
, uniqueArgumentNamesRule
|
2020-10-03 07:34:34 +02:00
|
|
|
|
, providedRequiredArgumentsRule
|
2020-09-04 19:12:19 +02:00
|
|
|
|
-- Fragments.
|
2020-08-28 08:32:21 +02:00
|
|
|
|
, uniqueFragmentNamesRule
|
2020-09-04 19:12:19 +02:00
|
|
|
|
, fragmentSpreadTypeExistenceRule
|
2020-09-07 22:01:49 +02:00
|
|
|
|
, fragmentsOnCompositeTypesRule
|
2020-09-09 17:04:31 +02:00
|
|
|
|
, noUnusedFragmentsRule
|
|
|
|
|
, fragmentSpreadTargetDefinedRule
|
2020-09-11 08:03:49 +02:00
|
|
|
|
, noFragmentCyclesRule
|
2020-11-19 08:48:37 +01:00
|
|
|
|
, possibleFragmentSpreadsRule
|
2020-09-24 05:47:31 +02:00
|
|
|
|
-- Values
|
2021-02-03 05:42:10 +01:00
|
|
|
|
, valuesOfCorrectTypeRule
|
2020-09-30 05:14:52 +02:00
|
|
|
|
, knownInputFieldNamesRule
|
2020-09-24 05:47:31 +02:00
|
|
|
|
, uniqueInputFieldNamesRule
|
2020-10-04 18:51:21 +02:00
|
|
|
|
, providedRequiredInputFieldsRule
|
2020-09-18 07:32:58 +02:00
|
|
|
|
-- Directives.
|
2020-09-29 06:21:32 +02:00
|
|
|
|
, knownDirectiveNamesRule
|
2020-10-02 06:31:38 +02:00
|
|
|
|
, directivesInValidLocationsRule
|
2020-09-18 07:32:58 +02:00
|
|
|
|
, uniqueDirectiveNamesRule
|
2020-09-19 18:18:26 +02:00
|
|
|
|
-- Variables.
|
|
|
|
|
, uniqueVariableNamesRule
|
2020-09-20 06:59:27 +02:00
|
|
|
|
, variablesAreInputTypesRule
|
2020-09-21 07:28:40 +02:00
|
|
|
|
, noUndefinedVariablesRule
|
2020-09-22 04:42:25 +02:00
|
|
|
|
, noUnusedVariablesRule
|
2020-12-26 06:31:56 +01:00
|
|
|
|
, variablesInAllowedPositionRule
|
2020-07-20 21:29:12 +02:00
|
|
|
|
]
|
|
|
|
|
|
2020-07-24 21:34:31 +02:00
|
|
|
|
-- | Definition must be OperationDefinition or FragmentDefinition.
|
2020-08-25 21:03:42 +02:00
|
|
|
|
executableDefinitionsRule :: forall m. Rule m
|
2020-08-26 18:58:48 +02:00
|
|
|
|
executableDefinitionsRule = DefinitionRule $ \case
|
2020-10-07 05:24:51 +02:00
|
|
|
|
Full.ExecutableDefinition _ -> lift mempty
|
|
|
|
|
Full.TypeSystemDefinition _ location' -> pure $ error' location'
|
|
|
|
|
Full.TypeSystemExtension _ location' -> pure $ error' location'
|
2020-08-27 09:04:31 +02:00
|
|
|
|
where
|
2020-09-30 05:14:52 +02:00
|
|
|
|
error' location' = Error
|
2020-08-27 09:04:31 +02:00
|
|
|
|
{ message =
|
|
|
|
|
"Definition must be OperationDefinition or FragmentDefinition."
|
2020-09-30 05:14:52 +02:00
|
|
|
|
, locations = [location']
|
2020-08-27 09:04:31 +02:00
|
|
|
|
}
|
2020-08-25 21:03:42 +02:00
|
|
|
|
|
|
|
|
|
-- | Subscription operations must have exactly one root field.
|
|
|
|
|
singleFieldSubscriptionsRule :: forall m. Rule m
|
2020-08-26 18:58:48 +02:00
|
|
|
|
singleFieldSubscriptionsRule = OperationDefinitionRule $ \case
|
2020-10-07 05:24:51 +02:00
|
|
|
|
Full.OperationDefinition Full.Subscription name' _ _ rootFields location' -> do
|
2020-11-15 10:11:09 +01:00
|
|
|
|
groupedFieldSet <- evalStateT (collectFields rootFields) HashSet.empty
|
|
|
|
|
case HashSet.size groupedFieldSet of
|
2020-09-14 07:49:33 +02:00
|
|
|
|
1 -> lift mempty
|
2020-08-25 21:03:42 +02:00
|
|
|
|
_
|
2020-08-27 09:04:31 +02:00
|
|
|
|
| Just name <- name' -> pure $ Error
|
2020-11-06 08:33:51 +01:00
|
|
|
|
{ message = concat
|
|
|
|
|
[ "Subscription \""
|
2020-08-27 09:04:31 +02:00
|
|
|
|
, Text.unpack name
|
2020-11-06 08:33:51 +01:00
|
|
|
|
, "\" must select only one top level field."
|
2020-08-27 09:04:31 +02:00
|
|
|
|
]
|
2020-09-30 05:14:52 +02:00
|
|
|
|
, locations = [location']
|
2020-08-27 09:04:31 +02:00
|
|
|
|
}
|
|
|
|
|
| otherwise -> pure $ Error
|
|
|
|
|
{ message = errorMessage
|
2020-09-30 05:14:52 +02:00
|
|
|
|
, locations = [location']
|
2020-08-27 09:04:31 +02:00
|
|
|
|
}
|
2020-09-14 07:49:33 +02:00
|
|
|
|
_ -> lift mempty
|
2020-08-26 18:58:48 +02:00
|
|
|
|
where
|
2020-08-27 09:04:31 +02:00
|
|
|
|
errorMessage =
|
|
|
|
|
"Anonymous Subscription must select only one top level field."
|
2020-11-15 10:11:09 +01:00
|
|
|
|
collectFields selectionSet = foldM forEach HashSet.empty selectionSet
|
2020-09-07 22:01:49 +02:00
|
|
|
|
forEach accumulator = \case
|
2020-10-07 05:24:51 +02:00
|
|
|
|
Full.FieldSelection fieldSelection -> forField accumulator fieldSelection
|
|
|
|
|
Full.FragmentSpreadSelection fragmentSelection ->
|
2020-09-07 22:01:49 +02:00
|
|
|
|
forSpread accumulator fragmentSelection
|
2020-10-07 05:24:51 +02:00
|
|
|
|
Full.InlineFragmentSelection fragmentSelection ->
|
2020-09-07 22:01:49 +02:00
|
|
|
|
forInline accumulator fragmentSelection
|
2020-11-15 10:11:09 +01:00
|
|
|
|
forField accumulator (Full.Field alias name _ directives' _ _)
|
2020-09-28 07:06:15 +02:00
|
|
|
|
| any skip directives' = pure accumulator
|
2020-11-15 10:11:09 +01:00
|
|
|
|
| Just aliasedName <- alias = pure
|
|
|
|
|
$ HashSet.insert aliasedName accumulator
|
|
|
|
|
| otherwise = pure $ HashSet.insert name accumulator
|
2020-10-07 05:24:51 +02:00
|
|
|
|
forSpread accumulator (Full.FragmentSpread fragmentName directives' _)
|
2020-09-28 07:06:15 +02:00
|
|
|
|
| any skip directives' = pure accumulator
|
2020-08-25 21:03:42 +02:00
|
|
|
|
| otherwise = do
|
|
|
|
|
inVisitetFragments <- gets $ HashSet.member fragmentName
|
|
|
|
|
if inVisitetFragments
|
|
|
|
|
then pure accumulator
|
|
|
|
|
else collectFromSpread fragmentName accumulator
|
2020-10-07 05:24:51 +02:00
|
|
|
|
forInline accumulator (Full.InlineFragment maybeType directives' selections _)
|
2020-09-28 07:06:15 +02:00
|
|
|
|
| any skip directives' = pure accumulator
|
2020-09-07 22:01:49 +02:00
|
|
|
|
| Just typeCondition <- maybeType =
|
|
|
|
|
collectFromFragment typeCondition selections accumulator
|
2020-11-15 10:11:09 +01:00
|
|
|
|
| otherwise = HashSet.union accumulator
|
|
|
|
|
<$> collectFields selections
|
|
|
|
|
skip (Full.Directive "skip" [Full.Argument "if" (Full.Node argumentValue _) _] _) =
|
|
|
|
|
Full.Boolean True == argumentValue
|
|
|
|
|
skip (Full.Directive "include" [Full.Argument "if" (Full.Node argumentValue _) _] _) =
|
|
|
|
|
Full.Boolean False == argumentValue
|
|
|
|
|
skip _ = False
|
|
|
|
|
collectFromFragment typeCondition selectionSet accumulator = do
|
2020-10-07 05:24:51 +02:00
|
|
|
|
types' <- lift $ asks $ Schema.types . schema
|
2020-08-25 21:03:42 +02:00
|
|
|
|
schema' <- lift $ asks schema
|
2020-10-07 05:24:51 +02:00
|
|
|
|
case Type.lookupTypeCondition typeCondition types' of
|
2020-08-25 21:03:42 +02:00
|
|
|
|
Nothing -> pure accumulator
|
|
|
|
|
Just compositeType
|
|
|
|
|
| Just objectType <- Schema.subscription schema'
|
2020-10-07 05:24:51 +02:00
|
|
|
|
, True <- Type.doesFragmentTypeApply compositeType objectType ->
|
2020-11-15 10:11:09 +01:00
|
|
|
|
HashSet.union accumulator <$> collectFields selectionSet
|
2020-08-25 21:03:42 +02:00
|
|
|
|
| otherwise -> pure accumulator
|
|
|
|
|
collectFromSpread fragmentName accumulator = do
|
|
|
|
|
modify $ HashSet.insert fragmentName
|
|
|
|
|
ast' <- lift $ asks ast
|
2020-11-06 08:33:51 +01:00
|
|
|
|
case findFragmentDefinition fragmentName ast' of
|
2020-08-25 21:03:42 +02:00
|
|
|
|
Nothing -> pure accumulator
|
2020-11-15 10:11:09 +01:00
|
|
|
|
Just (Full.FragmentDefinition _ typeCondition _ selectionSet _) ->
|
|
|
|
|
collectFromFragment typeCondition selectionSet accumulator
|
2020-08-26 18:58:48 +02:00
|
|
|
|
|
|
|
|
|
-- | GraphQL allows a short‐hand form for defining query operations when only
|
|
|
|
|
-- that one operation exists in the document.
|
|
|
|
|
loneAnonymousOperationRule :: forall m. Rule m
|
|
|
|
|
loneAnonymousOperationRule = OperationDefinitionRule $ \case
|
2020-10-07 05:24:51 +02:00
|
|
|
|
Full.SelectionSet _ thisLocation -> check thisLocation
|
|
|
|
|
Full.OperationDefinition _ Nothing _ _ _ thisLocation ->
|
|
|
|
|
check thisLocation
|
2020-09-14 07:49:33 +02:00
|
|
|
|
_ -> lift mempty
|
2020-08-26 18:58:48 +02:00
|
|
|
|
where
|
|
|
|
|
check thisLocation = asks ast
|
2020-09-14 07:49:33 +02:00
|
|
|
|
>>= lift . foldr (filterAnonymousOperations thisLocation) mempty
|
|
|
|
|
filterAnonymousOperations thisLocation definition Empty
|
2020-08-27 09:04:31 +02:00
|
|
|
|
| (viewOperation -> Just operationDefinition) <- definition =
|
2020-08-26 18:58:48 +02:00
|
|
|
|
compareAnonymousOperations thisLocation operationDefinition
|
|
|
|
|
filterAnonymousOperations _ _ accumulator = accumulator
|
2020-08-27 09:04:31 +02:00
|
|
|
|
compareAnonymousOperations thisLocation = \case
|
2020-10-07 05:24:51 +02:00
|
|
|
|
Full.OperationDefinition _ _ _ _ _ thatLocation
|
2020-08-27 09:04:31 +02:00
|
|
|
|
| thisLocation /= thatLocation -> pure $ error' thisLocation
|
2020-10-07 05:24:51 +02:00
|
|
|
|
Full.SelectionSet _ thatLocation
|
2020-08-27 09:04:31 +02:00
|
|
|
|
| thisLocation /= thatLocation -> pure $ error' thisLocation
|
2020-09-14 07:49:33 +02:00
|
|
|
|
_ -> mempty
|
2020-09-30 05:14:52 +02:00
|
|
|
|
error' location' = Error
|
2020-08-27 09:04:31 +02:00
|
|
|
|
{ message =
|
|
|
|
|
"This anonymous operation must be the only defined operation."
|
2020-09-30 05:14:52 +02:00
|
|
|
|
, locations = [location']
|
2020-08-27 09:04:31 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
-- | Each named operation definition must be unique within a document when
|
|
|
|
|
-- referred to by its name.
|
|
|
|
|
uniqueOperationNamesRule :: forall m. Rule m
|
|
|
|
|
uniqueOperationNamesRule = OperationDefinitionRule $ \case
|
2020-10-07 05:24:51 +02:00
|
|
|
|
Full.OperationDefinition _ (Just thisName) _ _ _ thisLocation ->
|
2020-08-28 08:32:21 +02:00
|
|
|
|
findDuplicates (filterByName thisName) thisLocation (error' thisName)
|
2020-09-14 07:49:33 +02:00
|
|
|
|
_ -> lift mempty
|
2020-08-27 09:04:31 +02:00
|
|
|
|
where
|
2020-08-28 08:32:21 +02:00
|
|
|
|
error' operationName = concat
|
|
|
|
|
[ "There can be only one operation named \""
|
|
|
|
|
, Text.unpack operationName
|
|
|
|
|
, "\"."
|
|
|
|
|
]
|
2020-08-27 09:04:31 +02:00
|
|
|
|
filterByName thisName definition' accumulator
|
|
|
|
|
| (viewOperation -> Just operationDefinition) <- definition'
|
2020-10-07 05:24:51 +02:00
|
|
|
|
, Full.OperationDefinition _ (Just thatName) _ _ _ thatLocation <- operationDefinition
|
2020-08-27 09:04:31 +02:00
|
|
|
|
, thisName == thatName = thatLocation : accumulator
|
|
|
|
|
| otherwise = accumulator
|
|
|
|
|
|
2020-10-07 05:24:51 +02:00
|
|
|
|
findDuplicates :: (Full.Definition -> [Full.Location] -> [Full.Location])
|
|
|
|
|
-> Full.Location
|
2020-08-28 08:32:21 +02:00
|
|
|
|
-> String
|
|
|
|
|
-> RuleT m
|
|
|
|
|
findDuplicates filterByName thisLocation errorMessage = do
|
|
|
|
|
ast' <- asks ast
|
|
|
|
|
let locations' = foldr filterByName [] ast'
|
|
|
|
|
if length locations' > 1 && head locations' == thisLocation
|
|
|
|
|
then pure $ error' locations'
|
2020-09-14 07:49:33 +02:00
|
|
|
|
else lift mempty
|
2020-08-28 08:32:21 +02:00
|
|
|
|
where
|
|
|
|
|
error' locations' = Error
|
|
|
|
|
{ message = errorMessage
|
|
|
|
|
, locations = locations'
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-07 05:24:51 +02:00
|
|
|
|
viewOperation :: Full.Definition -> Maybe Full.OperationDefinition
|
2020-08-27 09:04:31 +02:00
|
|
|
|
viewOperation definition
|
2020-10-07 05:24:51 +02:00
|
|
|
|
| Full.ExecutableDefinition executableDefinition <- definition
|
|
|
|
|
, Full.DefinitionOperation operationDefinition <- executableDefinition =
|
2020-08-27 09:04:31 +02:00
|
|
|
|
Just operationDefinition
|
|
|
|
|
viewOperation _ = Nothing
|
2020-08-28 08:32:21 +02:00
|
|
|
|
|
2020-10-07 05:24:51 +02:00
|
|
|
|
viewFragment :: Full.Definition -> Maybe Full.FragmentDefinition
|
2020-08-31 11:06:27 +02:00
|
|
|
|
viewFragment definition
|
2020-10-07 05:24:51 +02:00
|
|
|
|
| Full.ExecutableDefinition executableDefinition <- definition
|
|
|
|
|
, Full.DefinitionFragment fragmentDefinition <- executableDefinition =
|
2020-08-31 11:06:27 +02:00
|
|
|
|
Just fragmentDefinition
|
|
|
|
|
viewFragment _ = Nothing
|
|
|
|
|
|
2020-08-28 08:32:21 +02:00
|
|
|
|
-- | Fragment definitions are referenced in fragment spreads by name. To avoid
|
|
|
|
|
-- ambiguity, each fragment’s name must be unique within a document.
|
|
|
|
|
--
|
|
|
|
|
-- Inline fragments are not considered fragment definitions, and are unaffected
|
|
|
|
|
-- by this validation rule.
|
|
|
|
|
uniqueFragmentNamesRule :: forall m. Rule m
|
|
|
|
|
uniqueFragmentNamesRule = FragmentDefinitionRule $ \case
|
2020-10-07 05:24:51 +02:00
|
|
|
|
Full.FragmentDefinition thisName _ _ _ thisLocation ->
|
2020-08-28 08:32:21 +02:00
|
|
|
|
findDuplicates (filterByName thisName) thisLocation (error' thisName)
|
|
|
|
|
where
|
|
|
|
|
error' fragmentName = concat
|
|
|
|
|
[ "There can be only one fragment named \""
|
|
|
|
|
, Text.unpack fragmentName
|
|
|
|
|
, "\"."
|
|
|
|
|
]
|
|
|
|
|
filterByName thisName definition accumulator
|
2020-08-31 11:06:27 +02:00
|
|
|
|
| Just fragmentDefinition <- viewFragment definition
|
2020-10-07 05:24:51 +02:00
|
|
|
|
, Full.FragmentDefinition thatName _ _ _ thatLocation <- fragmentDefinition
|
2020-08-28 08:32:21 +02:00
|
|
|
|
, thisName == thatName = thatLocation : accumulator
|
|
|
|
|
| otherwise = accumulator
|
2020-08-31 11:06:27 +02:00
|
|
|
|
|
|
|
|
|
-- | 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
|
2020-09-07 22:01:49 +02:00
|
|
|
|
fragmentSpreadTargetDefinedRule = FragmentSpreadRule $ \case
|
2020-10-07 05:24:51 +02:00
|
|
|
|
Full.FragmentSpread fragmentName _ location' -> do
|
2020-08-31 11:06:27 +02:00
|
|
|
|
ast' <- asks ast
|
2020-09-04 19:12:19 +02:00
|
|
|
|
case find (isSpreadTarget fragmentName) ast' of
|
2020-08-31 11:06:27 +02:00
|
|
|
|
Nothing -> pure $ Error
|
|
|
|
|
{ message = error' fragmentName
|
2020-09-30 05:14:52 +02:00
|
|
|
|
, locations = [location']
|
2020-08-31 11:06:27 +02:00
|
|
|
|
}
|
2020-09-14 07:49:33 +02:00
|
|
|
|
Just _ -> lift mempty
|
2020-08-31 11:06:27 +02:00
|
|
|
|
where
|
|
|
|
|
error' fragmentName = concat
|
|
|
|
|
[ "Fragment target \""
|
|
|
|
|
, Text.unpack fragmentName
|
|
|
|
|
, "\" is undefined."
|
|
|
|
|
]
|
2020-09-04 19:12:19 +02:00
|
|
|
|
|
2020-10-07 05:24:51 +02:00
|
|
|
|
isSpreadTarget :: Text -> Full.Definition -> Bool
|
2020-09-04 19:12:19 +02:00
|
|
|
|
isSpreadTarget thisName (viewFragment -> Just fragmentDefinition)
|
2020-10-07 05:24:51 +02:00
|
|
|
|
| Full.FragmentDefinition thatName _ _ _ _ <- fragmentDefinition
|
2020-09-04 19:12:19 +02:00
|
|
|
|
, thisName == thatName = True
|
|
|
|
|
isSpreadTarget _ _ = False
|
|
|
|
|
|
|
|
|
|
-- | Fragments must be specified on types that exist in the schema. This applies
|
|
|
|
|
-- for both named and inline fragments. If they are not defined in the schema,
|
|
|
|
|
-- the query does not validate.
|
|
|
|
|
fragmentSpreadTypeExistenceRule :: forall m. Rule m
|
2020-09-25 21:57:25 +02:00
|
|
|
|
fragmentSpreadTypeExistenceRule = SelectionRule $ const $ \case
|
2020-10-07 05:24:51 +02:00
|
|
|
|
Full.FragmentSpreadSelection fragmentSelection
|
|
|
|
|
| Full.FragmentSpread fragmentName _ location' <- fragmentSelection -> do
|
|
|
|
|
types' <- asks $ Schema.types . schema
|
2020-11-19 08:48:37 +01:00
|
|
|
|
typeCondition <- findSpreadTarget fragmentName
|
2020-09-07 22:01:49 +02:00
|
|
|
|
case HashMap.lookup typeCondition types' of
|
|
|
|
|
Nothing -> pure $ Error
|
|
|
|
|
{ message = spreadError fragmentName typeCondition
|
2020-09-30 05:14:52 +02:00
|
|
|
|
, locations = [location']
|
2020-09-07 22:01:49 +02:00
|
|
|
|
}
|
2020-09-14 07:49:33 +02:00
|
|
|
|
Just _ -> lift mempty
|
2020-10-07 05:24:51 +02:00
|
|
|
|
Full.InlineFragmentSelection fragmentSelection
|
|
|
|
|
| Full.InlineFragment maybeType _ _ location' <- fragmentSelection
|
2020-09-07 22:01:49 +02:00
|
|
|
|
, Just typeCondition <- maybeType -> do
|
2020-10-07 05:24:51 +02:00
|
|
|
|
types' <- asks $ Schema.types . schema
|
2020-09-07 22:01:49 +02:00
|
|
|
|
case HashMap.lookup typeCondition types' of
|
|
|
|
|
Nothing -> pure $ Error
|
|
|
|
|
{ message = inlineError typeCondition
|
2020-09-30 05:14:52 +02:00
|
|
|
|
, locations = [location']
|
2020-09-07 22:01:49 +02:00
|
|
|
|
}
|
2020-09-14 07:49:33 +02:00
|
|
|
|
Just _ -> lift mempty
|
|
|
|
|
_ -> lift mempty
|
2020-09-04 19:12:19 +02:00
|
|
|
|
where
|
2020-09-05 10:00:58 +02:00
|
|
|
|
spreadError fragmentName typeCondition = concat
|
2020-09-04 19:12:19 +02:00
|
|
|
|
[ "Fragment \""
|
|
|
|
|
, Text.unpack fragmentName
|
|
|
|
|
, "\" is specified on type \""
|
|
|
|
|
, Text.unpack typeCondition
|
|
|
|
|
, "\" which doesn't exist in the schema."
|
|
|
|
|
]
|
2020-09-05 10:00:58 +02:00
|
|
|
|
inlineError typeCondition = concat
|
|
|
|
|
[ "Inline fragment is specified on type \""
|
|
|
|
|
, Text.unpack typeCondition
|
|
|
|
|
, "\" which doesn't exist in the schema."
|
|
|
|
|
]
|
2020-09-07 22:01:49 +02:00
|
|
|
|
|
2021-01-04 08:24:50 +01:00
|
|
|
|
maybeToSeq :: forall a. Maybe a -> Seq a
|
|
|
|
|
maybeToSeq (Just x) = pure x
|
|
|
|
|
maybeToSeq Nothing = mempty
|
2020-09-14 07:49:33 +02:00
|
|
|
|
|
2020-09-07 22:01:49 +02:00
|
|
|
|
-- | Fragments can only be declared on unions, interfaces, and objects. They are
|
|
|
|
|
-- invalid on scalars. They can only be applied on non‐leaf fields. This rule
|
|
|
|
|
-- applies to both inline and named fragments.
|
|
|
|
|
fragmentsOnCompositeTypesRule :: forall m. Rule m
|
|
|
|
|
fragmentsOnCompositeTypesRule = FragmentRule definitionRule inlineRule
|
|
|
|
|
where
|
2020-10-07 05:24:51 +02:00
|
|
|
|
inlineRule (Full.InlineFragment (Just typeCondition) _ _ location') =
|
2020-09-30 05:14:52 +02:00
|
|
|
|
check typeCondition location'
|
2020-09-14 07:49:33 +02:00
|
|
|
|
inlineRule _ = lift mempty
|
2020-10-07 05:24:51 +02:00
|
|
|
|
definitionRule (Full.FragmentDefinition _ typeCondition _ _ location') =
|
2020-09-30 05:14:52 +02:00
|
|
|
|
check typeCondition location'
|
|
|
|
|
check typeCondition location' = do
|
2020-10-07 05:24:51 +02:00
|
|
|
|
types' <- asks $ Schema.types . schema
|
2020-09-07 22:01:49 +02:00
|
|
|
|
-- Skip unknown types, they are checked by another rule.
|
2021-01-04 08:24:50 +01:00
|
|
|
|
_ <- lift $ maybeToSeq $ HashMap.lookup typeCondition types'
|
2020-10-07 05:24:51 +02:00
|
|
|
|
case Type.lookupTypeCondition typeCondition types' of
|
2020-09-07 22:01:49 +02:00
|
|
|
|
Nothing -> pure $ Error
|
|
|
|
|
{ message = errorMessage typeCondition
|
2020-09-30 05:14:52 +02:00
|
|
|
|
, locations = [location']
|
2020-09-07 22:01:49 +02:00
|
|
|
|
}
|
2020-09-14 07:49:33 +02:00
|
|
|
|
Just _ -> lift mempty
|
2020-09-07 22:01:49 +02:00
|
|
|
|
errorMessage typeCondition = concat
|
|
|
|
|
[ "Fragment cannot condition on non composite type \""
|
|
|
|
|
, Text.unpack typeCondition,
|
|
|
|
|
"\"."
|
|
|
|
|
]
|
2020-09-09 17:04:31 +02:00
|
|
|
|
|
|
|
|
|
-- | Defined fragments must be used within a document.
|
|
|
|
|
noUnusedFragmentsRule :: forall m. Rule m
|
2020-09-21 07:28:40 +02:00
|
|
|
|
noUnusedFragmentsRule = FragmentDefinitionRule $ \fragment -> do
|
2020-10-07 05:24:51 +02:00
|
|
|
|
let Full.FragmentDefinition fragmentName _ _ _ location' = fragment
|
2020-09-30 05:14:52 +02:00
|
|
|
|
in mapReaderT (checkFragmentName fragmentName location')
|
2020-09-21 07:28:40 +02:00
|
|
|
|
$ asks ast
|
|
|
|
|
>>= flip evalStateT HashSet.empty
|
|
|
|
|
. filterSelections evaluateSelection
|
|
|
|
|
. foldMap definitionSelections
|
2020-09-09 17:04:31 +02:00
|
|
|
|
where
|
2020-09-30 05:14:52 +02:00
|
|
|
|
checkFragmentName fragmentName location' elements
|
2020-09-21 07:28:40 +02:00
|
|
|
|
| fragmentName `elem` elements = mempty
|
2020-09-30 05:14:52 +02:00
|
|
|
|
| otherwise = pure $ makeError fragmentName location'
|
|
|
|
|
makeError fragName location' = Error
|
2020-09-21 07:28:40 +02:00
|
|
|
|
{ message = errorMessage fragName
|
2020-09-30 05:14:52 +02:00
|
|
|
|
, locations = [location']
|
2020-09-21 07:28:40 +02:00
|
|
|
|
}
|
2020-09-09 17:04:31 +02:00
|
|
|
|
errorMessage fragName = concat
|
|
|
|
|
[ "Fragment \""
|
|
|
|
|
, Text.unpack fragName
|
|
|
|
|
, "\" is never used."
|
|
|
|
|
]
|
2020-09-21 07:28:40 +02:00
|
|
|
|
evaluateSelection selection
|
2020-10-07 05:24:51 +02:00
|
|
|
|
| Full.FragmentSpreadSelection spreadSelection <- selection
|
|
|
|
|
, Full.FragmentSpread spreadName _ _ <- spreadSelection =
|
2020-09-21 07:28:40 +02:00
|
|
|
|
lift $ pure spreadName
|
|
|
|
|
evaluateSelection _ = lift $ lift mempty
|
|
|
|
|
|
2020-10-07 05:24:51 +02:00
|
|
|
|
definitionSelections :: Full.Definition -> Full.SelectionSetOpt
|
2020-09-21 07:28:40 +02:00
|
|
|
|
definitionSelections (viewOperation -> Just operation)
|
2020-10-07 05:24:51 +02:00
|
|
|
|
| Full.OperationDefinition _ _ _ _ selections _ <- operation =
|
|
|
|
|
toList selections
|
|
|
|
|
| Full.SelectionSet selections _ <- operation = toList selections
|
2020-09-21 07:28:40 +02:00
|
|
|
|
definitionSelections (viewFragment -> Just fragment)
|
2020-10-07 05:24:51 +02:00
|
|
|
|
| Full.FragmentDefinition _ _ _ selections _ <- fragment = toList selections
|
2020-09-21 07:28:40 +02:00
|
|
|
|
definitionSelections _ = []
|
|
|
|
|
|
|
|
|
|
filterSelections :: Foldable t
|
|
|
|
|
=> forall a m
|
2020-10-07 05:24:51 +02:00
|
|
|
|
. (Full.Selection -> ValidationState m a)
|
|
|
|
|
-> t Full.Selection
|
2020-09-21 07:28:40 +02:00
|
|
|
|
-> ValidationState m a
|
|
|
|
|
filterSelections applyFilter selections
|
|
|
|
|
= (lift . lift) (Seq.fromList $ foldr evaluateSelection mempty selections)
|
|
|
|
|
>>= applyFilter
|
|
|
|
|
where
|
|
|
|
|
evaluateSelection selection accumulator
|
2020-10-07 05:24:51 +02:00
|
|
|
|
| Full.FragmentSpreadSelection{} <- selection = selection : accumulator
|
|
|
|
|
| Full.FieldSelection fieldSelection <- selection
|
|
|
|
|
, Full.Field _ _ _ _ subselections _ <- fieldSelection =
|
2020-09-21 07:28:40 +02:00
|
|
|
|
selection : foldr evaluateSelection accumulator subselections
|
2020-10-07 05:24:51 +02:00
|
|
|
|
| Full.InlineFragmentSelection inlineSelection <- selection
|
|
|
|
|
, Full.InlineFragment _ _ subselections _ <- inlineSelection =
|
2020-09-21 07:28:40 +02:00
|
|
|
|
selection : foldr evaluateSelection accumulator subselections
|
2020-09-11 08:03:49 +02:00
|
|
|
|
|
|
|
|
|
-- | The graph of fragment spreads must not form any cycles including spreading
|
|
|
|
|
-- itself. Otherwise an operation could infinitely spread or infinitely execute
|
|
|
|
|
-- on cycles in the underlying data.
|
|
|
|
|
noFragmentCyclesRule :: forall m. Rule m
|
|
|
|
|
noFragmentCyclesRule = FragmentDefinitionRule $ \case
|
2020-10-07 05:24:51 +02:00
|
|
|
|
Full.FragmentDefinition fragmentName _ _ selections location' -> do
|
2020-11-11 08:49:45 +01:00
|
|
|
|
state <- evalStateT (collectCycles selections) (0, fragmentName)
|
2020-09-11 08:03:49 +02:00
|
|
|
|
let spreadPath = fst <$> sortBy (comparing snd) (HashMap.toList state)
|
|
|
|
|
case reverse spreadPath of
|
|
|
|
|
x : _ | x == fragmentName -> pure $ Error
|
|
|
|
|
{ message = concat
|
|
|
|
|
[ "Cannot spread fragment \""
|
|
|
|
|
, Text.unpack fragmentName
|
|
|
|
|
, "\" within itself (via "
|
|
|
|
|
, Text.unpack $ Text.intercalate " -> " $ fragmentName : spreadPath
|
|
|
|
|
, ")."
|
|
|
|
|
]
|
2020-09-30 05:14:52 +02:00
|
|
|
|
, locations = [location']
|
2020-09-11 08:03:49 +02:00
|
|
|
|
}
|
2020-09-14 07:49:33 +02:00
|
|
|
|
_ -> lift mempty
|
2020-09-11 08:03:49 +02:00
|
|
|
|
where
|
2020-11-11 08:49:45 +01:00
|
|
|
|
collectCycles :: Traversable t
|
2020-10-07 05:24:51 +02:00
|
|
|
|
=> t Full.Selection
|
|
|
|
|
-> StateT (Int, Full.Name) (ReaderT (Validation m) Seq) (HashMap Full.Name Int)
|
2020-11-11 08:49:45 +01:00
|
|
|
|
collectCycles selectionSet = foldM forEach HashMap.empty selectionSet
|
2020-09-11 08:03:49 +02:00
|
|
|
|
forEach accumulator = \case
|
2020-10-07 05:24:51 +02:00
|
|
|
|
Full.FieldSelection fieldSelection -> forField accumulator fieldSelection
|
|
|
|
|
Full.InlineFragmentSelection fragmentSelection ->
|
2020-09-11 08:03:49 +02:00
|
|
|
|
forInline accumulator fragmentSelection
|
2020-10-07 05:24:51 +02:00
|
|
|
|
Full.FragmentSpreadSelection fragmentSelection ->
|
2020-09-11 08:03:49 +02:00
|
|
|
|
forSpread accumulator fragmentSelection
|
2020-10-07 05:24:51 +02:00
|
|
|
|
forSpread accumulator (Full.FragmentSpread fragmentName _ _) = do
|
2020-09-11 08:03:49 +02:00
|
|
|
|
firstFragmentName <- gets snd
|
|
|
|
|
modify $ first (+ 1)
|
|
|
|
|
lastIndex <- gets fst
|
|
|
|
|
let newAccumulator = HashMap.insert fragmentName lastIndex accumulator
|
2020-09-21 07:28:40 +02:00
|
|
|
|
let inVisitetFragment = HashMap.member fragmentName accumulator
|
2020-09-11 08:03:49 +02:00
|
|
|
|
if fragmentName == firstFragmentName || inVisitetFragment
|
|
|
|
|
then pure newAccumulator
|
|
|
|
|
else collectFromSpread fragmentName newAccumulator
|
2020-10-07 05:24:51 +02:00
|
|
|
|
forInline accumulator (Full.InlineFragment _ _ selections _) =
|
2020-11-11 08:49:45 +01:00
|
|
|
|
(accumulator <>) <$> collectCycles selections
|
2020-10-07 05:24:51 +02:00
|
|
|
|
forField accumulator (Full.Field _ _ _ _ selections _) =
|
2020-11-11 08:49:45 +01:00
|
|
|
|
(accumulator <>) <$> collectCycles selections
|
2020-11-06 08:33:51 +01:00
|
|
|
|
collectFromSpread fragmentName accumulator = do
|
2020-09-11 08:03:49 +02:00
|
|
|
|
ast' <- lift $ asks ast
|
2020-11-06 08:33:51 +01:00
|
|
|
|
case findFragmentDefinition fragmentName ast' of
|
2020-09-11 08:03:49 +02:00
|
|
|
|
Nothing -> pure accumulator
|
2020-10-07 05:24:51 +02:00
|
|
|
|
Just (Full.FragmentDefinition _ _ _ selections _) ->
|
2020-11-11 08:49:45 +01:00
|
|
|
|
(accumulator <>) <$> collectCycles selections
|
2020-09-17 10:33:37 +02:00
|
|
|
|
|
2020-11-06 08:33:51 +01:00
|
|
|
|
findFragmentDefinition :: Text
|
|
|
|
|
-> NonEmpty Full.Definition
|
|
|
|
|
-> Maybe Full.FragmentDefinition
|
|
|
|
|
findFragmentDefinition fragmentName = foldr compareDefinition Nothing
|
|
|
|
|
where
|
|
|
|
|
compareDefinition (Full.ExecutableDefinition executableDefinition) Nothing
|
|
|
|
|
| Full.DefinitionFragment fragmentDefinition <- executableDefinition
|
|
|
|
|
, Full.FragmentDefinition anotherName _ _ _ _ <- fragmentDefinition
|
|
|
|
|
, anotherName == fragmentName = Just fragmentDefinition
|
|
|
|
|
compareDefinition _ accumulator = accumulator
|
|
|
|
|
|
2020-09-17 10:33:37 +02:00
|
|
|
|
-- | 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
|
2020-10-07 05:24:51 +02:00
|
|
|
|
fieldRule _ (Full.Field _ _ arguments _ _ _) =
|
2020-09-24 05:47:31 +02:00
|
|
|
|
lift $ filterDuplicates extract "argument" arguments
|
2020-10-07 05:24:51 +02:00
|
|
|
|
directiveRule (Full.Directive _ arguments _) =
|
2020-09-24 05:47:31 +02:00
|
|
|
|
lift $ filterDuplicates extract "argument" arguments
|
2020-10-07 05:24:51 +02:00
|
|
|
|
extract (Full.Argument argumentName _ location') = (argumentName, location')
|
2020-09-18 07:32:58 +02:00
|
|
|
|
|
|
|
|
|
-- | Directives are used to describe some metadata or behavioral change on the
|
|
|
|
|
-- definition they apply to. When more than one directive of the same name is
|
|
|
|
|
-- used, the expected metadata or behavior becomes ambiguous, therefore only one
|
|
|
|
|
-- of each directive is allowed per location.
|
|
|
|
|
uniqueDirectiveNamesRule :: forall m. Rule m
|
|
|
|
|
uniqueDirectiveNamesRule = DirectivesRule
|
2020-10-02 06:31:38 +02:00
|
|
|
|
$ const $ lift . filterDuplicates extract "directive"
|
2020-09-18 07:32:58 +02:00
|
|
|
|
where
|
2020-10-07 05:24:51 +02:00
|
|
|
|
extract (Full.Directive directiveName _ location') =
|
|
|
|
|
(directiveName, location')
|
2020-09-18 07:32:58 +02:00
|
|
|
|
|
2020-11-11 08:49:45 +01:00
|
|
|
|
groupSorted :: forall a. (a -> Text) -> [a] -> [[a]]
|
|
|
|
|
groupSorted getName = groupBy equalByName . sortOn getName
|
|
|
|
|
where
|
|
|
|
|
equalByName lhs rhs = getName lhs == getName rhs
|
|
|
|
|
|
|
|
|
|
filterDuplicates :: forall a
|
|
|
|
|
. (a -> (Text, Full.Location))
|
|
|
|
|
-> String
|
|
|
|
|
-> [a]
|
|
|
|
|
-> Seq Error
|
2020-09-24 05:47:31 +02:00
|
|
|
|
filterDuplicates extract nodeType = Seq.fromList
|
2020-09-18 07:32:58 +02:00
|
|
|
|
. fmap makeError
|
|
|
|
|
. filter ((> 1) . length)
|
2020-11-11 08:49:45 +01:00
|
|
|
|
. groupSorted getName
|
2020-09-18 07:32:58 +02:00
|
|
|
|
where
|
|
|
|
|
getName = fst . extract
|
2020-09-28 07:06:15 +02:00
|
|
|
|
makeError directives' = Error
|
|
|
|
|
{ message = makeMessage $ head directives'
|
|
|
|
|
, locations = snd . extract <$> directives'
|
2020-09-17 10:33:37 +02:00
|
|
|
|
}
|
2020-09-18 07:32:58 +02:00
|
|
|
|
makeMessage directive = concat
|
|
|
|
|
[ "There can be only one "
|
|
|
|
|
, nodeType
|
|
|
|
|
, " named \""
|
|
|
|
|
, Text.unpack $ fst $ extract directive
|
2020-09-17 10:33:37 +02:00
|
|
|
|
, "\"."
|
|
|
|
|
]
|
2020-09-19 18:18:26 +02:00
|
|
|
|
|
|
|
|
|
-- | If any operation defines more than one variable with the same name, it is
|
|
|
|
|
-- ambiguous and invalid. It is invalid even if the type of the duplicate
|
|
|
|
|
-- variable is the same.
|
|
|
|
|
uniqueVariableNamesRule :: forall m. Rule m
|
|
|
|
|
uniqueVariableNamesRule = VariablesRule
|
2020-09-24 05:47:31 +02:00
|
|
|
|
$ lift . filterDuplicates extract "variable"
|
2020-09-19 18:18:26 +02:00
|
|
|
|
where
|
2020-10-07 05:24:51 +02:00
|
|
|
|
extract (Full.VariableDefinition variableName _ _ location') =
|
2020-09-30 05:14:52 +02:00
|
|
|
|
(variableName, location')
|
2020-09-20 06:59:27 +02:00
|
|
|
|
|
|
|
|
|
-- | Variables can only be input types. Objects, unions and interfaces cannot be
|
|
|
|
|
-- used as inputs.
|
|
|
|
|
variablesAreInputTypesRule :: forall m. Rule m
|
|
|
|
|
variablesAreInputTypesRule = VariablesRule
|
|
|
|
|
$ (traverse check . Seq.fromList) >=> lift
|
|
|
|
|
where
|
2020-10-07 05:24:51 +02:00
|
|
|
|
check (Full.VariableDefinition name typeName _ location')
|
|
|
|
|
= asks (Schema.types . schema)
|
2020-09-20 06:59:27 +02:00
|
|
|
|
>>= lift
|
2020-09-30 05:14:52 +02:00
|
|
|
|
. maybe (makeError name typeName location') (const mempty)
|
2020-10-07 05:24:51 +02:00
|
|
|
|
. Type.lookupInputType typeName
|
2020-09-30 05:14:52 +02:00
|
|
|
|
makeError name typeName location' = pure $ Error
|
2020-09-20 06:59:27 +02:00
|
|
|
|
{ message = concat
|
|
|
|
|
[ "Variable \"$"
|
|
|
|
|
, Text.unpack name
|
|
|
|
|
, "\" cannot be non-input type \""
|
|
|
|
|
, Text.unpack $ getTypeName typeName
|
|
|
|
|
, "\"."
|
|
|
|
|
]
|
2020-09-30 05:14:52 +02:00
|
|
|
|
, locations = [location']
|
2020-09-20 06:59:27 +02:00
|
|
|
|
}
|
2020-10-07 05:24:51 +02:00
|
|
|
|
getTypeName (Full.TypeNamed name) = name
|
|
|
|
|
getTypeName (Full.TypeList name) = getTypeName name
|
|
|
|
|
getTypeName (Full.TypeNonNull (Full.NonNullTypeNamed nonNull)) = nonNull
|
|
|
|
|
getTypeName (Full.TypeNonNull (Full.NonNullTypeList nonNull)) =
|
|
|
|
|
getTypeName nonNull
|
2020-09-21 07:28:40 +02:00
|
|
|
|
|
|
|
|
|
-- | Variables are scoped on a per‐operation basis. That means that any variable
|
|
|
|
|
-- used within the context of an operation must be defined at the top level of
|
|
|
|
|
-- that operation.
|
|
|
|
|
noUndefinedVariablesRule :: forall m. Rule m
|
2020-09-22 04:42:25 +02:00
|
|
|
|
noUndefinedVariablesRule =
|
|
|
|
|
variableUsageDifference (flip HashMap.difference) errorMessage
|
|
|
|
|
where
|
|
|
|
|
errorMessage Nothing variableName = concat
|
|
|
|
|
[ "Variable \"$"
|
|
|
|
|
, Text.unpack variableName
|
|
|
|
|
, "\" is not defined."
|
|
|
|
|
]
|
|
|
|
|
errorMessage (Just operationName) variableName = concat
|
|
|
|
|
[ "Variable \"$"
|
|
|
|
|
, Text.unpack variableName
|
|
|
|
|
, "\" is not defined by operation \""
|
|
|
|
|
, Text.unpack operationName
|
|
|
|
|
, "\"."
|
|
|
|
|
]
|
|
|
|
|
|
2020-10-07 05:24:51 +02:00
|
|
|
|
type UsageDifference
|
|
|
|
|
= HashMap Full.Name [Full.Location]
|
|
|
|
|
-> HashMap Full.Name [Full.Location]
|
|
|
|
|
-> HashMap Full.Name [Full.Location]
|
|
|
|
|
|
|
|
|
|
variableUsageDifference :: forall m. UsageDifference
|
|
|
|
|
-> (Maybe Full.Name -> Full.Name -> String)
|
2020-09-22 04:42:25 +02:00
|
|
|
|
-> Rule m
|
|
|
|
|
variableUsageDifference difference errorMessage = OperationDefinitionRule $ \case
|
2020-10-07 05:24:51 +02:00
|
|
|
|
Full.SelectionSet _ _ -> lift mempty
|
|
|
|
|
Full.OperationDefinition _ operationName variables _ selections _ ->
|
2020-09-21 07:28:40 +02:00
|
|
|
|
let variableNames = HashMap.fromList $ getVariableName <$> variables
|
|
|
|
|
in mapReaderT (readerMapper operationName variableNames)
|
|
|
|
|
$ flip evalStateT HashSet.empty
|
|
|
|
|
$ filterSelections'
|
|
|
|
|
$ toList selections
|
|
|
|
|
where
|
|
|
|
|
readerMapper operationName variableNames' = Seq.fromList
|
|
|
|
|
. fmap (makeError operationName)
|
|
|
|
|
. HashMap.toList
|
2020-09-22 04:42:25 +02:00
|
|
|
|
. difference variableNames'
|
2020-09-21 07:28:40 +02:00
|
|
|
|
. HashMap.fromListWith (++)
|
|
|
|
|
. toList
|
2020-10-07 05:24:51 +02:00
|
|
|
|
getVariableName (Full.VariableDefinition variableName _ _ location') =
|
2020-09-30 05:14:52 +02:00
|
|
|
|
(variableName, [location'])
|
2020-09-21 07:28:40 +02:00
|
|
|
|
filterSelections' :: Foldable t
|
2020-10-07 05:24:51 +02:00
|
|
|
|
=> t Full.Selection
|
|
|
|
|
-> ValidationState m (Full.Name, [Full.Location])
|
2020-09-21 07:28:40 +02:00
|
|
|
|
filterSelections' = filterSelections variableFilter
|
2020-10-07 05:24:51 +02:00
|
|
|
|
variableFilter :: Full.Selection -> ValidationState m (Full.Name, [Full.Location])
|
|
|
|
|
variableFilter (Full.InlineFragmentSelection inline)
|
|
|
|
|
| Full.InlineFragment _ directives' _ _ <- inline =
|
2020-09-28 07:06:15 +02:00
|
|
|
|
lift $ lift $ mapDirectives directives'
|
2020-10-07 05:24:51 +02:00
|
|
|
|
variableFilter (Full.FieldSelection fieldSelection)
|
|
|
|
|
| Full.Field _ _ arguments directives' _ _ <- fieldSelection =
|
2020-09-28 07:06:15 +02:00
|
|
|
|
lift $ lift $ mapArguments arguments <> mapDirectives directives'
|
2020-10-07 05:24:51 +02:00
|
|
|
|
variableFilter (Full.FragmentSpreadSelection spread)
|
|
|
|
|
| Full.FragmentSpread fragmentName _ _ <- spread = do
|
2020-12-26 06:31:56 +01:00
|
|
|
|
nonVisitedFragmentDefinition <- visitFragmentDefinition fragmentName
|
|
|
|
|
case nonVisitedFragmentDefinition of
|
|
|
|
|
Just fragmentDefinition -> diveIntoSpread fragmentDefinition
|
2020-09-21 07:28:40 +02:00
|
|
|
|
_ -> lift $ lift mempty
|
2020-10-07 05:24:51 +02:00
|
|
|
|
diveIntoSpread (Full.FragmentDefinition _ _ directives' selections _)
|
2020-09-21 07:28:40 +02:00
|
|
|
|
= filterSelections' selections
|
2020-09-28 07:06:15 +02:00
|
|
|
|
>>= lift . mapReaderT (<> mapDirectives directives') . pure
|
2020-10-07 05:24:51 +02:00
|
|
|
|
findDirectiveVariables (Full.Directive _ arguments _) = mapArguments arguments
|
2020-09-21 07:28:40 +02:00
|
|
|
|
mapArguments = Seq.fromList . mapMaybe findArgumentVariables
|
|
|
|
|
mapDirectives = foldMap findDirectiveVariables
|
2020-10-07 05:24:51 +02:00
|
|
|
|
findArgumentVariables (Full.Argument _ Full.Node{ node = Full.Variable value', ..} _) =
|
2020-09-30 05:14:52 +02:00
|
|
|
|
Just (value', [location])
|
2020-09-21 07:28:40 +02:00
|
|
|
|
findArgumentVariables _ = Nothing
|
|
|
|
|
makeError operationName (variableName, locations') = Error
|
|
|
|
|
{ message = errorMessage operationName variableName
|
|
|
|
|
, locations = locations'
|
|
|
|
|
}
|
2020-09-22 04:42:25 +02:00
|
|
|
|
|
|
|
|
|
-- | All variables defined by an operation must be used in that operation or a
|
|
|
|
|
-- fragment transitively included by that operation. Unused variables cause a
|
|
|
|
|
-- validation error.
|
|
|
|
|
noUnusedVariablesRule :: forall m. Rule m
|
|
|
|
|
noUnusedVariablesRule = variableUsageDifference HashMap.difference errorMessage
|
|
|
|
|
where
|
2020-09-21 07:28:40 +02:00
|
|
|
|
errorMessage Nothing variableName = concat
|
|
|
|
|
[ "Variable \"$"
|
|
|
|
|
, Text.unpack variableName
|
2020-09-22 04:42:25 +02:00
|
|
|
|
, "\" is never used."
|
2020-09-21 07:28:40 +02:00
|
|
|
|
]
|
|
|
|
|
errorMessage (Just operationName) variableName = concat
|
|
|
|
|
[ "Variable \"$"
|
|
|
|
|
, Text.unpack variableName
|
2020-09-22 04:42:25 +02:00
|
|
|
|
, "\" is never used in operation \""
|
2020-09-21 07:28:40 +02:00
|
|
|
|
, Text.unpack operationName
|
|
|
|
|
, "\"."
|
|
|
|
|
]
|
2020-09-24 05:47:31 +02:00
|
|
|
|
|
|
|
|
|
-- | Input objects must not contain more than one field of the same name,
|
|
|
|
|
-- otherwise an ambiguity would exist which includes an ignored portion of
|
|
|
|
|
-- syntax.
|
|
|
|
|
uniqueInputFieldNamesRule :: forall m. Rule m
|
2020-09-30 05:14:52 +02:00
|
|
|
|
uniqueInputFieldNamesRule =
|
|
|
|
|
ValueRule (const $ lift . go) (const $ lift . constGo)
|
2020-09-24 05:47:31 +02:00
|
|
|
|
where
|
2020-10-07 05:24:51 +02:00
|
|
|
|
go (Full.Node (Full.Object fields) _) = filterFieldDuplicates fields
|
2020-09-24 05:47:31 +02:00
|
|
|
|
go _ = mempty
|
|
|
|
|
filterFieldDuplicates fields =
|
|
|
|
|
filterDuplicates getFieldName "input field" fields
|
2020-10-07 05:24:51 +02:00
|
|
|
|
getFieldName (Full.ObjectField fieldName _ location') = (fieldName, location')
|
|
|
|
|
constGo (Full.Node (Full.ConstObject fields) _) = filterFieldDuplicates fields
|
2020-09-24 05:47:31 +02:00
|
|
|
|
constGo _ = mempty
|
2020-09-25 21:57:25 +02:00
|
|
|
|
|
|
|
|
|
-- | The target field of a field selection must be defined on the scoped type of
|
|
|
|
|
-- the selection set. There are no limitations on alias names.
|
|
|
|
|
fieldsOnCorrectTypeRule :: forall m. Rule m
|
2020-09-28 07:06:15 +02:00
|
|
|
|
fieldsOnCorrectTypeRule = FieldRule fieldRule
|
2020-09-25 21:57:25 +02:00
|
|
|
|
where
|
2020-10-07 05:24:51 +02:00
|
|
|
|
fieldRule parentType (Full.Field _ fieldName _ _ _ location')
|
2020-09-28 07:06:15 +02:00
|
|
|
|
| Just objectType <- parentType
|
2020-10-07 05:24:51 +02:00
|
|
|
|
, Nothing <- Type.lookupTypeField fieldName objectType
|
2020-11-19 08:48:37 +01:00
|
|
|
|
, Just typeName <- typeNameIfComposite objectType = pure $ Error
|
2020-09-26 09:06:30 +02:00
|
|
|
|
{ message = errorMessage fieldName typeName
|
2020-09-30 05:14:52 +02:00
|
|
|
|
, locations = [location']
|
2020-09-25 21:57:25 +02:00
|
|
|
|
}
|
2020-09-26 09:06:30 +02:00
|
|
|
|
| otherwise = lift mempty
|
|
|
|
|
errorMessage fieldName typeName = concat
|
2020-09-25 21:57:25 +02:00
|
|
|
|
[ "Cannot query field \""
|
|
|
|
|
, Text.unpack fieldName
|
|
|
|
|
, "\" on type \""
|
2020-09-26 09:06:30 +02:00
|
|
|
|
, Text.unpack typeName
|
2020-09-25 21:57:25 +02:00
|
|
|
|
, "\"."
|
|
|
|
|
]
|
2020-09-28 07:06:15 +02:00
|
|
|
|
|
2020-11-19 08:48:37 +01:00
|
|
|
|
compositeTypeName :: forall m. Type.CompositeType m -> Full.Name
|
|
|
|
|
compositeTypeName (Type.CompositeObjectType (Out.ObjectType typeName _ _ _)) =
|
|
|
|
|
typeName
|
|
|
|
|
compositeTypeName (Type.CompositeInterfaceType interfaceType) =
|
2020-09-28 07:06:15 +02:00
|
|
|
|
let Out.InterfaceType typeName _ _ _ = interfaceType
|
2020-11-19 08:48:37 +01:00
|
|
|
|
in typeName
|
|
|
|
|
compositeTypeName (Type.CompositeUnionType (Out.UnionType typeName _ _)) =
|
|
|
|
|
typeName
|
|
|
|
|
|
|
|
|
|
typeNameIfComposite :: forall m. Out.Type m -> Maybe Full.Name
|
|
|
|
|
typeNameIfComposite = fmap compositeTypeName . Type.outToComposite
|
2020-09-26 09:06:30 +02:00
|
|
|
|
|
|
|
|
|
-- | Field selections on scalars or enums are never allowed, because they are
|
|
|
|
|
-- the leaf nodes of any GraphQL query.
|
|
|
|
|
scalarLeafsRule :: forall m. Rule m
|
2020-09-28 07:06:15 +02:00
|
|
|
|
scalarLeafsRule = FieldRule fieldRule
|
2020-09-26 09:06:30 +02:00
|
|
|
|
where
|
2020-10-07 05:24:51 +02:00
|
|
|
|
fieldRule parentType selectionField@(Full.Field _ fieldName _ _ _ _)
|
2020-09-28 07:06:15 +02:00
|
|
|
|
| Just objectType <- parentType
|
2020-10-07 05:24:51 +02:00
|
|
|
|
, Just field <- Type.lookupTypeField fieldName objectType =
|
2020-09-28 07:06:15 +02:00
|
|
|
|
let Out.Field _ fieldType _ = field
|
|
|
|
|
in lift $ check fieldType selectionField
|
2020-09-26 09:06:30 +02:00
|
|
|
|
| otherwise = lift mempty
|
|
|
|
|
check (Out.ObjectBaseType (Out.ObjectType typeName _ _ _)) =
|
|
|
|
|
checkNotEmpty typeName
|
|
|
|
|
check (Out.InterfaceBaseType (Out.InterfaceType typeName _ _ _)) =
|
|
|
|
|
checkNotEmpty typeName
|
|
|
|
|
check (Out.UnionBaseType (Out.UnionType typeName _ _)) =
|
|
|
|
|
checkNotEmpty typeName
|
|
|
|
|
check (Out.ScalarBaseType (Definition.ScalarType typeName _)) =
|
|
|
|
|
checkEmpty typeName
|
|
|
|
|
check (Out.EnumBaseType (Definition.EnumType typeName _ _)) =
|
|
|
|
|
checkEmpty typeName
|
|
|
|
|
check (Out.ListBaseType wrappedType) = check wrappedType
|
2020-10-07 05:24:51 +02:00
|
|
|
|
checkNotEmpty typeName (Full.Field _ fieldName _ _ [] location') =
|
2020-09-26 09:06:30 +02:00
|
|
|
|
let fieldName' = Text.unpack fieldName
|
2020-09-30 05:14:52 +02:00
|
|
|
|
in makeError location' $ concat
|
2020-09-26 09:06:30 +02:00
|
|
|
|
[ "Field \""
|
|
|
|
|
, fieldName'
|
|
|
|
|
, "\" of type \""
|
|
|
|
|
, Text.unpack typeName
|
|
|
|
|
, "\" must have a selection of subfields. Did you mean \""
|
|
|
|
|
, fieldName'
|
|
|
|
|
, " { ... }\"?"
|
|
|
|
|
]
|
|
|
|
|
checkNotEmpty _ _ = mempty
|
2020-10-07 05:24:51 +02:00
|
|
|
|
checkEmpty _ (Full.Field _ _ _ _ [] _) = mempty
|
2020-09-26 09:06:30 +02:00
|
|
|
|
checkEmpty typeName field' =
|
2020-10-07 05:24:51 +02:00
|
|
|
|
let Full.Field _ fieldName _ _ _ location' = field'
|
2020-09-30 05:14:52 +02:00
|
|
|
|
in makeError location' $ concat
|
2020-09-26 09:06:30 +02:00
|
|
|
|
[ "Field \""
|
|
|
|
|
, Text.unpack fieldName
|
|
|
|
|
, "\" must not have a selection since type \""
|
|
|
|
|
, Text.unpack typeName
|
|
|
|
|
, "\" has no subfields."
|
|
|
|
|
]
|
2020-09-30 05:14:52 +02:00
|
|
|
|
makeError location' errorMessage = pure $ Error
|
2020-09-26 09:06:30 +02:00
|
|
|
|
{ message = errorMessage
|
2020-09-30 05:14:52 +02:00
|
|
|
|
, locations = [location']
|
2020-09-26 09:06:30 +02:00
|
|
|
|
}
|
2020-09-28 07:06:15 +02:00
|
|
|
|
|
|
|
|
|
-- | Every argument provided to a field or directive must be defined in the set
|
|
|
|
|
-- of possible arguments of that field or directive.
|
|
|
|
|
knownArgumentNamesRule :: forall m. Rule m
|
|
|
|
|
knownArgumentNamesRule = ArgumentsRule fieldRule directiveRule
|
|
|
|
|
where
|
2020-10-07 05:24:51 +02:00
|
|
|
|
fieldRule (Just objectType) (Full.Field _ fieldName arguments _ _ _)
|
|
|
|
|
| Just typeField <- Type.lookupTypeField fieldName objectType
|
2020-11-19 08:48:37 +01:00
|
|
|
|
, Just typeName <- typeNameIfComposite objectType =
|
2020-09-28 07:06:15 +02:00
|
|
|
|
lift $ foldr (go typeName fieldName typeField) Seq.empty arguments
|
|
|
|
|
fieldRule _ _ = lift mempty
|
2020-10-07 05:24:51 +02:00
|
|
|
|
go typeName fieldName fieldDefinition (Full.Argument argumentName _ location') errors
|
2020-09-28 07:06:15 +02:00
|
|
|
|
| Out.Field _ _ definitions <- fieldDefinition
|
|
|
|
|
, Just _ <- HashMap.lookup argumentName definitions = errors
|
|
|
|
|
| otherwise = errors |> Error
|
|
|
|
|
{ message = fieldMessage argumentName fieldName typeName
|
2020-09-30 05:14:52 +02:00
|
|
|
|
, locations = [location']
|
2020-09-28 07:06:15 +02:00
|
|
|
|
}
|
|
|
|
|
fieldMessage argumentName fieldName typeName = concat
|
|
|
|
|
[ "Unknown argument \""
|
|
|
|
|
, Text.unpack argumentName
|
|
|
|
|
, "\" on field \""
|
|
|
|
|
, Text.unpack typeName
|
|
|
|
|
, "."
|
|
|
|
|
, Text.unpack fieldName
|
|
|
|
|
, "\"."
|
|
|
|
|
]
|
2020-10-07 05:24:51 +02:00
|
|
|
|
directiveRule (Full.Directive directiveName arguments _) = do
|
|
|
|
|
available <- asks $ HashMap.lookup directiveName
|
|
|
|
|
. Schema.directives . schema
|
|
|
|
|
Full.Argument argumentName _ location' <- lift $ Seq.fromList arguments
|
2020-09-28 07:06:15 +02:00
|
|
|
|
case available of
|
|
|
|
|
Just (Schema.Directive _ _ definitions)
|
|
|
|
|
| not $ HashMap.member argumentName definitions ->
|
2020-09-30 05:14:52 +02:00
|
|
|
|
pure $ makeError argumentName directiveName location'
|
2020-09-28 07:06:15 +02:00
|
|
|
|
_ -> lift mempty
|
2020-09-30 05:14:52 +02:00
|
|
|
|
makeError argumentName directiveName location' = Error
|
2020-09-28 07:06:15 +02:00
|
|
|
|
{ message = directiveMessage argumentName directiveName
|
2020-09-30 05:14:52 +02:00
|
|
|
|
, locations = [location']
|
2020-09-28 07:06:15 +02:00
|
|
|
|
}
|
|
|
|
|
directiveMessage argumentName directiveName = concat
|
|
|
|
|
[ "Unknown argument \""
|
|
|
|
|
, Text.unpack argumentName
|
|
|
|
|
, "\" on directive \"@"
|
|
|
|
|
, Text.unpack directiveName
|
|
|
|
|
, "\"."
|
|
|
|
|
]
|
2020-09-29 06:21:32 +02:00
|
|
|
|
|
|
|
|
|
-- | GraphQL servers define what directives they support. For each usage of a
|
|
|
|
|
-- directive, the directive must be available on that server.
|
|
|
|
|
knownDirectiveNamesRule :: Rule m
|
2020-10-02 06:31:38 +02:00
|
|
|
|
knownDirectiveNamesRule = DirectivesRule $ const $ \directives' -> do
|
2020-10-07 05:24:51 +02:00
|
|
|
|
definitions' <- asks $ Schema.directives . schema
|
2020-09-29 06:21:32 +02:00
|
|
|
|
let directiveSet = HashSet.fromList $ fmap directiveName directives'
|
|
|
|
|
let definitionSet = HashSet.fromList $ HashMap.keys definitions'
|
|
|
|
|
let difference = HashSet.difference directiveSet definitionSet
|
|
|
|
|
let undefined' = filter (definitionFilter difference) directives'
|
|
|
|
|
lift $ Seq.fromList $ makeError <$> undefined'
|
|
|
|
|
where
|
|
|
|
|
definitionFilter difference = flip HashSet.member difference
|
|
|
|
|
. directiveName
|
2020-10-07 05:24:51 +02:00
|
|
|
|
directiveName (Full.Directive directiveName' _ _) = directiveName'
|
|
|
|
|
makeError (Full.Directive directiveName' _ location') = Error
|
2020-09-29 06:21:32 +02:00
|
|
|
|
{ message = errorMessage directiveName'
|
2020-09-30 05:14:52 +02:00
|
|
|
|
, locations = [location']
|
2020-09-29 06:21:32 +02:00
|
|
|
|
}
|
|
|
|
|
errorMessage directiveName' = concat
|
|
|
|
|
[ "Unknown directive \"@"
|
|
|
|
|
, Text.unpack directiveName'
|
|
|
|
|
, "\"."
|
|
|
|
|
]
|
2020-09-30 05:14:52 +02:00
|
|
|
|
|
|
|
|
|
-- | Every input field provided in an input object value must be defined in the
|
|
|
|
|
-- set of possible fields of that input object’s expected type.
|
|
|
|
|
knownInputFieldNamesRule :: Rule m
|
|
|
|
|
knownInputFieldNamesRule = ValueRule go constGo
|
|
|
|
|
where
|
2020-10-07 05:24:51 +02:00
|
|
|
|
go (Just valueType) (Full.Node (Full.Object inputFields) _)
|
2020-09-30 05:14:52 +02:00
|
|
|
|
| In.InputObjectBaseType objectType <- valueType =
|
|
|
|
|
lift $ Seq.fromList $ mapMaybe (forEach objectType) inputFields
|
|
|
|
|
go _ _ = lift mempty
|
2020-10-07 05:24:51 +02:00
|
|
|
|
constGo (Just valueType) (Full.Node (Full.ConstObject inputFields) _)
|
2020-09-30 05:14:52 +02:00
|
|
|
|
| In.InputObjectBaseType objectType <- valueType =
|
|
|
|
|
lift $ Seq.fromList $ mapMaybe (forEach objectType) inputFields
|
|
|
|
|
constGo _ _ = lift mempty
|
2020-10-07 05:24:51 +02:00
|
|
|
|
forEach objectType (Full.ObjectField inputFieldName _ location')
|
2020-09-30 05:14:52 +02:00
|
|
|
|
| In.InputObjectType _ _ fieldTypes <- objectType
|
|
|
|
|
, Just _ <- HashMap.lookup inputFieldName fieldTypes = Nothing
|
|
|
|
|
| otherwise
|
|
|
|
|
, In.InputObjectType typeName _ _ <- objectType = pure $ Error
|
|
|
|
|
{ message = errorMessage inputFieldName typeName
|
|
|
|
|
, locations = [location']
|
|
|
|
|
}
|
|
|
|
|
errorMessage fieldName typeName = concat
|
|
|
|
|
[ "Field \""
|
|
|
|
|
, Text.unpack fieldName
|
|
|
|
|
, "\" is not defined by type \""
|
|
|
|
|
, Text.unpack typeName
|
|
|
|
|
, "\"."
|
|
|
|
|
]
|
2020-10-02 06:31:38 +02:00
|
|
|
|
|
|
|
|
|
-- | GraphQL servers define what directives they support and where they support
|
|
|
|
|
-- them. For each usage of a directive, the directive must be used in a location
|
|
|
|
|
-- that the server has declared support for.
|
|
|
|
|
directivesInValidLocationsRule :: Rule m
|
|
|
|
|
directivesInValidLocationsRule = DirectivesRule directivesRule
|
|
|
|
|
where
|
|
|
|
|
directivesRule directiveLocation directives' = do
|
2020-10-07 05:24:51 +02:00
|
|
|
|
Full.Directive directiveName _ location <- lift $ Seq.fromList directives'
|
|
|
|
|
maybeDefinition <- asks
|
|
|
|
|
$ HashMap.lookup directiveName . Schema.directives . schema
|
2020-10-02 06:31:38 +02:00
|
|
|
|
case maybeDefinition of
|
|
|
|
|
Just (Schema.Directive _ allowedLocations _)
|
|
|
|
|
| directiveLocation `notElem` allowedLocations -> pure $ Error
|
|
|
|
|
{ message = errorMessage directiveName directiveLocation
|
|
|
|
|
, locations = [location]
|
|
|
|
|
}
|
|
|
|
|
_ -> lift mempty
|
|
|
|
|
errorMessage directiveName directiveLocation = concat
|
|
|
|
|
[ "Directive \"@"
|
|
|
|
|
, Text.unpack directiveName
|
|
|
|
|
, "\" may not be used on "
|
|
|
|
|
, show directiveLocation
|
|
|
|
|
, "."
|
|
|
|
|
]
|
2020-10-03 07:34:34 +02:00
|
|
|
|
|
|
|
|
|
-- | Arguments can be required. An argument is required if the argument type is
|
|
|
|
|
-- non‐null and does not have a default value. Otherwise, the argument is
|
|
|
|
|
-- optional.
|
|
|
|
|
providedRequiredArgumentsRule :: Rule m
|
|
|
|
|
providedRequiredArgumentsRule = ArgumentsRule fieldRule directiveRule
|
|
|
|
|
where
|
2020-10-07 05:24:51 +02:00
|
|
|
|
fieldRule (Just objectType) (Full.Field _ fieldName arguments _ _ location')
|
|
|
|
|
| Just typeField <- Type.lookupTypeField fieldName objectType
|
2020-10-03 07:34:34 +02:00
|
|
|
|
, Out.Field _ _ definitions <- typeField =
|
|
|
|
|
let forEach = go (fieldMessage fieldName) arguments location'
|
|
|
|
|
in lift $ HashMap.foldrWithKey forEach Seq.empty definitions
|
|
|
|
|
fieldRule _ _ = lift mempty
|
2020-10-07 05:24:51 +02:00
|
|
|
|
directiveRule (Full.Directive directiveName arguments location') = do
|
|
|
|
|
available <- asks
|
|
|
|
|
$ HashMap.lookup directiveName . Schema.directives . schema
|
2020-10-03 07:34:34 +02:00
|
|
|
|
case available of
|
|
|
|
|
Just (Schema.Directive _ _ definitions) ->
|
|
|
|
|
let forEach = go (directiveMessage directiveName) arguments location'
|
|
|
|
|
in lift $ HashMap.foldrWithKey forEach Seq.empty definitions
|
|
|
|
|
_ -> lift mempty
|
|
|
|
|
go makeMessage arguments location' argumentName argumentType errors
|
|
|
|
|
| In.Argument _ type' optionalValue <- argumentType
|
|
|
|
|
, In.isNonNullType type'
|
|
|
|
|
, typeName <- inputTypeName type'
|
|
|
|
|
, isNothing optionalValue
|
|
|
|
|
, isNothingOrNull $ find (lookupArgument argumentName) arguments
|
|
|
|
|
= errors
|
|
|
|
|
|> makeError (makeMessage argumentName typeName) location'
|
|
|
|
|
| otherwise = errors
|
|
|
|
|
makeError errorMessage location' = Error
|
|
|
|
|
{ message = errorMessage
|
|
|
|
|
, locations = [location']
|
|
|
|
|
}
|
2020-10-07 05:24:51 +02:00
|
|
|
|
isNothingOrNull (Just (Full.Argument _ (Full.Node Full.Null _) _)) = True
|
2020-10-03 07:34:34 +02:00
|
|
|
|
isNothingOrNull x = isNothing x
|
2020-10-07 05:24:51 +02:00
|
|
|
|
lookupArgument needle (Full.Argument argumentName _ _) =
|
|
|
|
|
needle == argumentName
|
2020-10-03 07:34:34 +02:00
|
|
|
|
fieldMessage fieldName argumentName typeName = concat
|
|
|
|
|
[ "Field \""
|
|
|
|
|
, Text.unpack fieldName
|
|
|
|
|
, "\" argument \""
|
|
|
|
|
, Text.unpack argumentName
|
|
|
|
|
, "\" of type \""
|
|
|
|
|
, Text.unpack typeName
|
|
|
|
|
, "\" is required, but it was not provided."
|
|
|
|
|
]
|
|
|
|
|
directiveMessage directiveName argumentName typeName = concat
|
|
|
|
|
[ "Directive \"@"
|
|
|
|
|
, Text.unpack directiveName
|
|
|
|
|
, "\" argument \""
|
|
|
|
|
, Text.unpack argumentName
|
|
|
|
|
, "\" of type \""
|
|
|
|
|
, Text.unpack typeName
|
|
|
|
|
, "\" is required, but it was not provided."
|
|
|
|
|
]
|
2020-10-04 18:51:21 +02:00
|
|
|
|
|
|
|
|
|
inputTypeName :: In.Type -> Text
|
|
|
|
|
inputTypeName (In.ScalarBaseType (Definition.ScalarType typeName _)) = typeName
|
|
|
|
|
inputTypeName (In.EnumBaseType (Definition.EnumType typeName _ _)) = typeName
|
|
|
|
|
inputTypeName (In.InputObjectBaseType (In.InputObjectType typeName _ _)) =
|
|
|
|
|
typeName
|
|
|
|
|
inputTypeName (In.ListBaseType listType) = inputTypeName listType
|
|
|
|
|
|
|
|
|
|
-- | Input object fields may be required. Much like a field may have required
|
|
|
|
|
-- arguments, an input object may have required fields. An input field is
|
|
|
|
|
-- required if it has a non‐null type and does not have a default value.
|
|
|
|
|
-- Otherwise, the input object field is optional.
|
|
|
|
|
providedRequiredInputFieldsRule :: Rule m
|
|
|
|
|
providedRequiredInputFieldsRule = ValueRule go constGo
|
|
|
|
|
where
|
2020-10-07 05:24:51 +02:00
|
|
|
|
go (Just valueType) (Full.Node (Full.Object inputFields) location')
|
2020-10-04 18:51:21 +02:00
|
|
|
|
| In.InputObjectBaseType objectType <- valueType
|
|
|
|
|
, In.InputObjectType objectTypeName _ fieldDefinitions <- objectType
|
|
|
|
|
= lift
|
|
|
|
|
$ Seq.fromList
|
|
|
|
|
$ HashMap.elems
|
|
|
|
|
$ flip HashMap.mapMaybeWithKey fieldDefinitions
|
|
|
|
|
$ forEach inputFields objectTypeName location'
|
|
|
|
|
go _ _ = lift mempty
|
|
|
|
|
constGo _ _ = lift mempty
|
|
|
|
|
forEach inputFields typeName location' definitionName fieldDefinition
|
|
|
|
|
| In.InputField _ inputType optionalValue <- fieldDefinition
|
|
|
|
|
, In.isNonNullType inputType
|
|
|
|
|
, isNothing optionalValue
|
|
|
|
|
, isNothingOrNull $ find (lookupField definitionName) inputFields =
|
|
|
|
|
Just $ makeError definitionName typeName location'
|
|
|
|
|
| otherwise = Nothing
|
2020-10-07 05:24:51 +02:00
|
|
|
|
isNothingOrNull (Just (Full.ObjectField _ (Full.Node Full.Null _) _)) = True
|
2020-10-04 18:51:21 +02:00
|
|
|
|
isNothingOrNull x = isNothing x
|
2020-10-07 05:24:51 +02:00
|
|
|
|
lookupField needle (Full.ObjectField fieldName _ _) = needle == fieldName
|
2020-10-04 18:51:21 +02:00
|
|
|
|
makeError fieldName typeName location' = Error
|
|
|
|
|
{ message = errorMessage fieldName typeName
|
|
|
|
|
, locations = [location']
|
|
|
|
|
}
|
|
|
|
|
errorMessage fieldName typeName = concat
|
|
|
|
|
[ "Input field \""
|
|
|
|
|
, Text.unpack fieldName
|
|
|
|
|
, "\" of type \""
|
|
|
|
|
, Text.unpack typeName
|
|
|
|
|
, "\" is required, but it was not provided."
|
|
|
|
|
]
|
2020-11-15 10:11:09 +01:00
|
|
|
|
|
|
|
|
|
-- | If multiple field selections with the same response names are encountered
|
|
|
|
|
-- during execution, the field and arguments to execute and the resulting value
|
|
|
|
|
-- should be unambiguous. Therefore any two field selections which might both be
|
|
|
|
|
-- encountered for the same object are only valid if they are equivalent.
|
|
|
|
|
--
|
|
|
|
|
-- For simple hand‐written GraphQL, this rule is obviously a clear developer
|
|
|
|
|
-- error, however nested fragments can make this difficult to detect manually.
|
|
|
|
|
overlappingFieldsCanBeMergedRule :: Rule m
|
|
|
|
|
overlappingFieldsCanBeMergedRule = OperationDefinitionRule $ \case
|
|
|
|
|
Full.SelectionSet selectionSet _ -> do
|
|
|
|
|
schema' <- asks schema
|
|
|
|
|
go (toList selectionSet)
|
|
|
|
|
$ Type.CompositeObjectType
|
|
|
|
|
$ Schema.query schema'
|
|
|
|
|
Full.OperationDefinition operationType _ _ _ selectionSet _ -> do
|
|
|
|
|
schema' <- asks schema
|
|
|
|
|
let root = go (toList selectionSet) . Type.CompositeObjectType
|
|
|
|
|
case operationType of
|
|
|
|
|
Full.Query -> root $ Schema.query schema'
|
|
|
|
|
Full.Mutation
|
|
|
|
|
| Just objectType <- Schema.mutation schema' -> root objectType
|
|
|
|
|
Full.Subscription
|
|
|
|
|
| Just objectType <- Schema.mutation schema' -> root objectType
|
|
|
|
|
_ -> lift mempty
|
|
|
|
|
where
|
|
|
|
|
go selectionSet selectionType = do
|
|
|
|
|
fieldTuples <- evalStateT (collectFields selectionType selectionSet) HashSet.empty
|
|
|
|
|
fieldsInSetCanMerge fieldTuples
|
|
|
|
|
fieldsInSetCanMerge :: forall m
|
|
|
|
|
. HashMap Full.Name (NonEmpty (Full.Field, Type.CompositeType m))
|
|
|
|
|
-> ReaderT (Validation m) Seq Error
|
|
|
|
|
fieldsInSetCanMerge fieldTuples = do
|
|
|
|
|
validation <- ask
|
|
|
|
|
let (lonely, paired) = flattenPairs fieldTuples
|
|
|
|
|
let reader = flip runReaderT validation
|
|
|
|
|
lift $ foldMap (reader . visitLonelyFields) lonely
|
|
|
|
|
<> foldMap (reader . forEachFieldTuple) paired
|
|
|
|
|
forEachFieldTuple :: forall m
|
|
|
|
|
. (FieldInfo m, FieldInfo m)
|
|
|
|
|
-> ReaderT (Validation m) Seq Error
|
|
|
|
|
forEachFieldTuple (fieldA, fieldB) =
|
|
|
|
|
case (parent fieldA, parent fieldB) of
|
|
|
|
|
(parentA@Type.CompositeObjectType{}, parentB@Type.CompositeObjectType{})
|
|
|
|
|
| parentA /= parentB -> sameResponseShape fieldA fieldB
|
|
|
|
|
_ -> mapReaderT (checkEquality (node fieldA) (node fieldB))
|
|
|
|
|
$ sameResponseShape fieldA fieldB
|
|
|
|
|
checkEquality fieldA fieldB Seq.Empty
|
|
|
|
|
| Full.Field _ fieldNameA _ _ _ _ <- fieldA
|
|
|
|
|
, Full.Field _ fieldNameB _ _ _ _ <- fieldB
|
|
|
|
|
, fieldNameA /= fieldNameB = pure $ makeError fieldA fieldB
|
|
|
|
|
| Full.Field _ fieldNameA argumentsA _ _ locationA <- fieldA
|
|
|
|
|
, Full.Field _ _ argumentsB _ _ locationB <- fieldB
|
|
|
|
|
, argumentsA /= argumentsB =
|
|
|
|
|
let message = concat
|
|
|
|
|
[ "Fields \""
|
|
|
|
|
, Text.unpack fieldNameA
|
|
|
|
|
, "\" conflict because they have different arguments. Use "
|
|
|
|
|
, "different aliases on the fields to fetch both if this "
|
|
|
|
|
, "was intentional."
|
|
|
|
|
]
|
|
|
|
|
in pure $ Error message [locationB, locationA]
|
|
|
|
|
checkEquality _ _ previousErrors = previousErrors
|
|
|
|
|
visitLonelyFields FieldInfo{..} =
|
|
|
|
|
let Full.Field _ _ _ _ subSelections _ = node
|
|
|
|
|
compositeFieldType = Type.outToComposite type'
|
|
|
|
|
in maybe (lift Seq.empty) (go subSelections) compositeFieldType
|
|
|
|
|
sameResponseShape :: forall m
|
|
|
|
|
. FieldInfo m
|
|
|
|
|
-> FieldInfo m
|
|
|
|
|
-> ReaderT (Validation m) Seq Error
|
|
|
|
|
sameResponseShape fieldA fieldB =
|
|
|
|
|
let Full.Field _ _ _ _ selectionsA _ = node fieldA
|
|
|
|
|
Full.Field _ _ _ _ selectionsB _ = node fieldB
|
|
|
|
|
in case unwrapTypes (type' fieldA) (type' fieldB) of
|
|
|
|
|
Left True -> lift mempty
|
|
|
|
|
Right (compositeA, compositeB) -> do
|
|
|
|
|
validation <- ask
|
|
|
|
|
let collectFields' composite = flip runReaderT validation
|
|
|
|
|
. flip evalStateT HashSet.empty
|
|
|
|
|
. collectFields composite
|
|
|
|
|
let collectA = collectFields' compositeA selectionsA
|
|
|
|
|
let collectB = collectFields' compositeB selectionsB
|
|
|
|
|
fieldsInSetCanMerge
|
|
|
|
|
$ foldl' (HashMap.unionWith (<>)) HashMap.empty
|
|
|
|
|
$ collectA <> collectB
|
|
|
|
|
_ -> pure $ makeError (node fieldA) (node fieldB)
|
|
|
|
|
makeError fieldA fieldB =
|
|
|
|
|
let Full.Field aliasA fieldNameA _ _ _ locationA = fieldA
|
|
|
|
|
Full.Field _ fieldNameB _ _ _ locationB = fieldB
|
|
|
|
|
message = concat
|
|
|
|
|
[ "Fields \""
|
|
|
|
|
, Text.unpack (fromMaybe fieldNameA aliasA)
|
|
|
|
|
, "\" conflict because \""
|
|
|
|
|
, Text.unpack fieldNameB
|
|
|
|
|
, "\" and \""
|
|
|
|
|
, Text.unpack fieldNameA
|
|
|
|
|
, "\" are different fields. Use different aliases on the fields "
|
|
|
|
|
, "to fetch both if this was intentional."
|
|
|
|
|
]
|
|
|
|
|
in Error message [locationB, locationA]
|
|
|
|
|
unwrapTypes typeA@Out.ScalarBaseType{} typeB@Out.ScalarBaseType{} =
|
|
|
|
|
Left $ typeA == typeB
|
|
|
|
|
unwrapTypes typeA@Out.EnumBaseType{} typeB@Out.EnumBaseType{} =
|
|
|
|
|
Left $ typeA == typeB
|
|
|
|
|
unwrapTypes (Out.ListType listA) (Out.ListType listB) =
|
|
|
|
|
unwrapTypes listA listB
|
|
|
|
|
unwrapTypes (Out.NonNullListType listA) (Out.NonNullListType listB) =
|
|
|
|
|
unwrapTypes listA listB
|
|
|
|
|
unwrapTypes typeA typeB
|
|
|
|
|
| Out.isNonNullType typeA == Out.isNonNullType typeB
|
|
|
|
|
, Just compositeA <- Type.outToComposite typeA
|
|
|
|
|
, Just compositeB <- Type.outToComposite typeB =
|
|
|
|
|
Right (compositeA, compositeB)
|
|
|
|
|
| otherwise = Left False
|
|
|
|
|
flattenPairs :: forall m
|
|
|
|
|
. HashMap Full.Name (NonEmpty (Full.Field, Type.CompositeType m))
|
|
|
|
|
-> (Seq (FieldInfo m), Seq (FieldInfo m, FieldInfo m))
|
|
|
|
|
flattenPairs xs = HashMap.foldr splitSingleFields (Seq.empty, Seq.empty)
|
|
|
|
|
$ foldr lookupTypeField [] <$> xs
|
|
|
|
|
splitSingleFields :: forall m
|
|
|
|
|
. [FieldInfo m]
|
|
|
|
|
-> (Seq (FieldInfo m), Seq (FieldInfo m, FieldInfo m))
|
|
|
|
|
-> (Seq (FieldInfo m), Seq (FieldInfo m, FieldInfo m))
|
|
|
|
|
splitSingleFields [head'] (fields, pairList) = (fields |> head', pairList)
|
|
|
|
|
splitSingleFields xs (fields, pairList) = (fields, pairs pairList xs)
|
|
|
|
|
lookupTypeField (field, parentType) accumulator =
|
|
|
|
|
let Full.Field _ fieldName _ _ _ _ = field
|
|
|
|
|
in case Type.lookupCompositeField fieldName parentType of
|
|
|
|
|
Nothing -> accumulator
|
|
|
|
|
Just (Out.Field _ typeField _) ->
|
|
|
|
|
FieldInfo field typeField parentType : accumulator
|
|
|
|
|
pairs :: forall m
|
|
|
|
|
. Seq (FieldInfo m, FieldInfo m)
|
|
|
|
|
-> [FieldInfo m]
|
|
|
|
|
-> Seq (FieldInfo m, FieldInfo m)
|
|
|
|
|
pairs accumulator [] = accumulator
|
|
|
|
|
pairs accumulator (fieldA : fields) =
|
|
|
|
|
pair fieldA (pairs accumulator fields) fields
|
|
|
|
|
pair _ accumulator [] = accumulator
|
|
|
|
|
pair field accumulator (fieldA : fields) =
|
|
|
|
|
pair field accumulator fields |> (field, fieldA)
|
|
|
|
|
collectFields objectType = accumulateFields objectType mempty
|
|
|
|
|
accumulateFields = foldM . forEach
|
|
|
|
|
forEach parentType accumulator = \case
|
|
|
|
|
Full.FieldSelection fieldSelection ->
|
|
|
|
|
forField parentType accumulator fieldSelection
|
|
|
|
|
Full.FragmentSpreadSelection fragmentSelection ->
|
|
|
|
|
forSpread accumulator fragmentSelection
|
|
|
|
|
Full.InlineFragmentSelection fragmentSelection ->
|
|
|
|
|
forInline parentType accumulator fragmentSelection
|
|
|
|
|
forField parentType accumulator field@(Full.Field alias fieldName _ _ _ _) =
|
|
|
|
|
let key = fromMaybe fieldName alias
|
|
|
|
|
value = (field, parentType) :| []
|
|
|
|
|
in pure $ HashMap.insertWith (<>) key value accumulator
|
|
|
|
|
forSpread accumulator (Full.FragmentSpread fragmentName _ _) = do
|
|
|
|
|
inVisitetFragments <- gets $ HashSet.member fragmentName
|
|
|
|
|
if inVisitetFragments
|
|
|
|
|
then pure accumulator
|
|
|
|
|
else collectFromSpread fragmentName accumulator
|
|
|
|
|
forInline parentType accumulator = \case
|
|
|
|
|
Full.InlineFragment maybeType _ selections _
|
|
|
|
|
| Just typeCondition <- maybeType ->
|
|
|
|
|
collectFromFragment typeCondition selections accumulator
|
|
|
|
|
| otherwise -> accumulateFields parentType accumulator $ toList selections
|
|
|
|
|
collectFromFragment typeCondition selectionSet' accumulator = do
|
|
|
|
|
types' <- lift $ asks $ Schema.types . schema
|
|
|
|
|
case Type.lookupTypeCondition typeCondition types' of
|
|
|
|
|
Nothing -> pure accumulator
|
|
|
|
|
Just compositeType ->
|
|
|
|
|
accumulateFields compositeType accumulator $ toList selectionSet'
|
|
|
|
|
collectFromSpread fragmentName accumulator = do
|
|
|
|
|
modify $ HashSet.insert fragmentName
|
|
|
|
|
ast' <- lift $ asks ast
|
|
|
|
|
case findFragmentDefinition fragmentName ast' of
|
|
|
|
|
Nothing -> pure accumulator
|
|
|
|
|
Just (Full.FragmentDefinition _ typeCondition _ selectionSet' _) ->
|
|
|
|
|
collectFromFragment typeCondition selectionSet' accumulator
|
|
|
|
|
|
|
|
|
|
data FieldInfo m = FieldInfo
|
|
|
|
|
{ node :: Full.Field
|
|
|
|
|
, type' :: Out.Type m
|
|
|
|
|
, parent :: Type.CompositeType m
|
|
|
|
|
}
|
2020-11-19 08:48:37 +01:00
|
|
|
|
|
|
|
|
|
-- | Fragments are declared on a type and will only apply when the runtime
|
|
|
|
|
-- object type matches the type condition. They also are spread within the
|
|
|
|
|
-- context of a parent type. A fragment spread is only valid if its type
|
|
|
|
|
-- condition could ever apply within the parent type.
|
|
|
|
|
possibleFragmentSpreadsRule :: forall m. Rule m
|
|
|
|
|
possibleFragmentSpreadsRule = SelectionRule go
|
|
|
|
|
where
|
|
|
|
|
go (Just parentType) (Full.InlineFragmentSelection fragmentSelection)
|
|
|
|
|
| Full.InlineFragment maybeType _ _ location' <- fragmentSelection
|
|
|
|
|
, Just typeCondition <- maybeType = do
|
|
|
|
|
(fragmentTypeName, parentTypeName) <-
|
|
|
|
|
compareTypes typeCondition parentType
|
|
|
|
|
pure $ Error
|
|
|
|
|
{ message = concat
|
|
|
|
|
[ "Fragment cannot be spread here as objects of type \""
|
|
|
|
|
, Text.unpack parentTypeName
|
|
|
|
|
, "\" can never be of type \""
|
|
|
|
|
, Text.unpack fragmentTypeName
|
|
|
|
|
, "\"."
|
|
|
|
|
]
|
|
|
|
|
, locations = [location']
|
|
|
|
|
}
|
|
|
|
|
go (Just parentType) (Full.FragmentSpreadSelection fragmentSelection)
|
|
|
|
|
| Full.FragmentSpread fragmentName _ location' <- fragmentSelection = do
|
|
|
|
|
typeCondition <- findSpreadTarget fragmentName
|
|
|
|
|
(fragmentTypeName, parentTypeName) <-
|
|
|
|
|
compareTypes typeCondition parentType
|
|
|
|
|
pure $ Error
|
|
|
|
|
{ message = concat
|
|
|
|
|
[ "Fragment \""
|
|
|
|
|
, Text.unpack fragmentName
|
|
|
|
|
, "\" cannot be spread here as objects of type \""
|
|
|
|
|
, Text.unpack parentTypeName
|
|
|
|
|
, "\" can never be of type \""
|
|
|
|
|
, Text.unpack fragmentTypeName
|
|
|
|
|
, "\"."
|
|
|
|
|
]
|
|
|
|
|
, locations = [location']
|
|
|
|
|
}
|
|
|
|
|
go _ _ = lift mempty
|
|
|
|
|
compareTypes typeCondition parentType = do
|
|
|
|
|
types' <- asks $ Schema.types . schema
|
2021-01-04 08:24:50 +01:00
|
|
|
|
fragmentType <- lift
|
|
|
|
|
$ maybeToSeq
|
2020-11-19 08:48:37 +01:00
|
|
|
|
$ Type.lookupTypeCondition typeCondition types'
|
2021-01-04 08:24:50 +01:00
|
|
|
|
parentComposite <- lift
|
|
|
|
|
$ maybeToSeq
|
2020-11-19 08:48:37 +01:00
|
|
|
|
$ Type.outToComposite parentType
|
|
|
|
|
possibleFragments <- getPossibleTypes fragmentType
|
|
|
|
|
possibleParents <- getPossibleTypes parentComposite
|
|
|
|
|
let fragmentTypeName = compositeTypeName fragmentType
|
|
|
|
|
let parentTypeName = compositeTypeName parentComposite
|
|
|
|
|
if HashSet.null $ HashSet.intersection possibleFragments possibleParents
|
|
|
|
|
then pure (fragmentTypeName, parentTypeName)
|
|
|
|
|
else lift mempty
|
|
|
|
|
getPossibleTypeList (Type.CompositeObjectType objectType) =
|
|
|
|
|
pure [Schema.ObjectType objectType]
|
|
|
|
|
getPossibleTypeList (Type.CompositeUnionType unionType) =
|
|
|
|
|
let Out.UnionType _ _ members = unionType
|
|
|
|
|
in pure $ Schema.ObjectType <$> members
|
|
|
|
|
getPossibleTypeList (Type.CompositeInterfaceType interfaceType) =
|
|
|
|
|
let Out.InterfaceType typeName _ _ _ = interfaceType
|
|
|
|
|
in HashMap.lookupDefault [] typeName
|
|
|
|
|
<$> asks (Schema.implementations . schema)
|
|
|
|
|
getPossibleTypes compositeType
|
|
|
|
|
= foldr (HashSet.insert . internalTypeName) HashSet.empty
|
|
|
|
|
<$> getPossibleTypeList compositeType
|
|
|
|
|
|
|
|
|
|
internalTypeName :: forall m. Schema.Type m -> Full.Name
|
|
|
|
|
internalTypeName (Schema.ScalarType (Definition.ScalarType typeName _)) =
|
|
|
|
|
typeName
|
|
|
|
|
internalTypeName (Schema.EnumType (Definition.EnumType typeName _ _)) = typeName
|
|
|
|
|
internalTypeName (Schema.ObjectType (Out.ObjectType typeName _ _ _)) = typeName
|
|
|
|
|
internalTypeName (Schema.InputObjectType (In.InputObjectType typeName _ _)) =
|
|
|
|
|
typeName
|
|
|
|
|
internalTypeName (Schema.InterfaceType (Out.InterfaceType typeName _ _ _)) =
|
|
|
|
|
typeName
|
|
|
|
|
internalTypeName (Schema.UnionType (Out.UnionType typeName _ _)) = typeName
|
|
|
|
|
|
|
|
|
|
findSpreadTarget :: Full.Name -> ReaderT (Validation m1) Seq Full.TypeCondition
|
|
|
|
|
findSpreadTarget fragmentName = do
|
|
|
|
|
ast' <- asks ast
|
|
|
|
|
let target = find (isSpreadTarget fragmentName) ast'
|
2021-01-04 08:24:50 +01:00
|
|
|
|
lift $ maybeToSeq $ target >>= extractTypeCondition
|
2020-11-19 08:48:37 +01:00
|
|
|
|
where
|
|
|
|
|
extractTypeCondition (viewFragment -> Just fragmentDefinition) =
|
|
|
|
|
let Full.FragmentDefinition _ typeCondition _ _ _ = fragmentDefinition
|
|
|
|
|
in Just typeCondition
|
|
|
|
|
extractTypeCondition _ = Nothing
|
2020-12-26 06:31:56 +01:00
|
|
|
|
|
|
|
|
|
visitFragmentDefinition :: forall m
|
|
|
|
|
. Text
|
|
|
|
|
-> ValidationState m (Maybe Full.FragmentDefinition)
|
|
|
|
|
visitFragmentDefinition fragmentName = do
|
|
|
|
|
definitions <- lift $ asks ast
|
|
|
|
|
visited <- gets (HashSet.member fragmentName)
|
|
|
|
|
modify (HashSet.insert fragmentName)
|
|
|
|
|
case find (isSpreadTarget fragmentName) definitions of
|
|
|
|
|
Just (viewFragment -> Just fragmentDefinition)
|
|
|
|
|
| not visited -> pure $ Just fragmentDefinition
|
|
|
|
|
_ -> pure Nothing
|
|
|
|
|
|
|
|
|
|
-- | Variable usages must be compatible with the arguments they are passed to.
|
|
|
|
|
--
|
|
|
|
|
-- Validation failures occur when variables are used in the context of types
|
|
|
|
|
-- that are complete mismatches, or if a nullable type in a variable is passed
|
|
|
|
|
-- to a non‐null argument type.
|
|
|
|
|
variablesInAllowedPositionRule :: forall m. Rule m
|
|
|
|
|
variablesInAllowedPositionRule = OperationDefinitionRule $ \case
|
|
|
|
|
Full.OperationDefinition operationType _ variables _ selectionSet _ -> do
|
|
|
|
|
schema' <- asks schema
|
|
|
|
|
let root = go variables (toList selectionSet) . Type.CompositeObjectType
|
|
|
|
|
case operationType of
|
|
|
|
|
Full.Query -> root $ Schema.query schema'
|
|
|
|
|
Full.Mutation
|
|
|
|
|
| Just objectType <- Schema.mutation schema' -> root objectType
|
|
|
|
|
Full.Subscription
|
|
|
|
|
| Just objectType <- Schema.mutation schema' -> root objectType
|
|
|
|
|
_ -> lift mempty
|
|
|
|
|
_ -> lift mempty
|
|
|
|
|
where
|
|
|
|
|
go variables selections selectionType = mapReaderT (foldr (<>) Seq.empty)
|
|
|
|
|
$ flip evalStateT HashSet.empty
|
|
|
|
|
$ visitSelectionSet variables selectionType
|
|
|
|
|
$ toList selections
|
|
|
|
|
visitSelectionSet :: Foldable t
|
|
|
|
|
=> [Full.VariableDefinition]
|
|
|
|
|
-> Type.CompositeType m
|
|
|
|
|
-> t Full.Selection
|
|
|
|
|
-> ValidationState m (Seq Error)
|
|
|
|
|
visitSelectionSet variables selectionType selections =
|
|
|
|
|
foldM (evaluateSelection variables selectionType) mempty selections
|
2020-12-27 11:47:29 +01:00
|
|
|
|
evaluateFieldSelection variables selections accumulator = \case
|
|
|
|
|
Just newParentType -> do
|
|
|
|
|
let folder = evaluateSelection variables newParentType
|
|
|
|
|
selectionErrors <- foldM folder accumulator selections
|
|
|
|
|
pure $ accumulator <> selectionErrors
|
|
|
|
|
Nothing -> pure accumulator
|
2020-12-26 06:31:56 +01:00
|
|
|
|
evaluateSelection :: [Full.VariableDefinition]
|
|
|
|
|
-> Type.CompositeType m
|
|
|
|
|
-> Seq Error
|
|
|
|
|
-> Full.Selection
|
|
|
|
|
-> ValidationState m (Seq Error)
|
|
|
|
|
evaluateSelection variables selectionType accumulator selection
|
|
|
|
|
| Full.FragmentSpreadSelection spread <- selection
|
|
|
|
|
, Full.FragmentSpread fragmentName _ _ <- spread = do
|
|
|
|
|
types' <- lift $ asks $ Schema.types . schema
|
|
|
|
|
nonVisitedFragmentDefinition <- visitFragmentDefinition fragmentName
|
|
|
|
|
case nonVisitedFragmentDefinition of
|
|
|
|
|
Just fragmentDefinition
|
|
|
|
|
| Full.FragmentDefinition _ typeCondition _ _ _ <- fragmentDefinition
|
|
|
|
|
, Just spreadType <- Type.lookupTypeCondition typeCondition types' -> do
|
2020-12-27 11:47:29 +01:00
|
|
|
|
spreadErrors <- spreadVariables variables spread
|
|
|
|
|
selectionErrors <- diveIntoSpread variables spreadType fragmentDefinition
|
|
|
|
|
pure $ accumulator <> spreadErrors <> selectionErrors
|
2020-12-26 06:31:56 +01:00
|
|
|
|
_ -> lift $ lift mempty
|
|
|
|
|
| Full.FieldSelection fieldSelection <- selection
|
|
|
|
|
, Full.Field _ fieldName _ _ subselections _ <- fieldSelection =
|
|
|
|
|
case Type.lookupCompositeField fieldName selectionType of
|
|
|
|
|
Just (Out.Field _ typeField argumentTypes) -> do
|
2020-12-27 11:47:29 +01:00
|
|
|
|
fieldErrors <- fieldVariables variables argumentTypes fieldSelection
|
|
|
|
|
selectionErrors <- evaluateFieldSelection variables subselections accumulator
|
|
|
|
|
$ Type.outToComposite typeField
|
|
|
|
|
pure $ selectionErrors <> fieldErrors
|
2020-12-26 06:31:56 +01:00
|
|
|
|
Nothing -> pure accumulator
|
|
|
|
|
| Full.InlineFragmentSelection inlineSelection <- selection
|
|
|
|
|
, Full.InlineFragment typeCondition _ subselections _ <- inlineSelection = do
|
|
|
|
|
types' <- lift $ asks $ Schema.types . schema
|
|
|
|
|
let inlineType = fromMaybe selectionType
|
2020-12-27 11:47:29 +01:00
|
|
|
|
$ typeCondition >>= flip Type.lookupTypeCondition types'
|
|
|
|
|
fragmentErrors <- inlineVariables variables inlineSelection
|
|
|
|
|
let folder = evaluateSelection variables inlineType
|
|
|
|
|
selectionErrors <- foldM folder accumulator subselections
|
|
|
|
|
pure $ accumulator <> fragmentErrors <> selectionErrors
|
2020-12-26 06:31:56 +01:00
|
|
|
|
inlineVariables variables inline
|
|
|
|
|
| Full.InlineFragment _ directives' _ _ <- inline =
|
|
|
|
|
mapDirectives variables directives'
|
|
|
|
|
fieldVariables :: [Full.VariableDefinition]
|
|
|
|
|
-> In.Arguments
|
|
|
|
|
-> Full.Field
|
|
|
|
|
-> ValidationState m (Seq Error)
|
|
|
|
|
fieldVariables variables argumentTypes fieldSelection = do
|
|
|
|
|
let Full.Field _ _ arguments directives' _ _ = fieldSelection
|
|
|
|
|
argumentErrors <- mapArguments variables argumentTypes arguments
|
|
|
|
|
directiveErrors <- mapDirectives variables directives'
|
|
|
|
|
pure $ argumentErrors <> directiveErrors
|
|
|
|
|
spreadVariables variables (Full.FragmentSpread _ directives' _) =
|
|
|
|
|
mapDirectives variables directives'
|
|
|
|
|
diveIntoSpread variables fieldType fragmentDefinition = do
|
|
|
|
|
let Full.FragmentDefinition _ _ directives' selections _ =
|
|
|
|
|
fragmentDefinition
|
|
|
|
|
selectionErrors <- visitSelectionSet variables fieldType selections
|
|
|
|
|
directiveErrors <- mapDirectives variables directives'
|
|
|
|
|
pure $ selectionErrors <> directiveErrors
|
|
|
|
|
findDirectiveVariables variables directive = do
|
|
|
|
|
let Full.Directive directiveName arguments _ = directive
|
|
|
|
|
directiveDefinitions <- lift $ asks $ Schema.directives . schema
|
|
|
|
|
case HashMap.lookup directiveName directiveDefinitions of
|
|
|
|
|
Just (Schema.Directive _ _ directiveArguments) ->
|
|
|
|
|
mapArguments variables directiveArguments arguments
|
|
|
|
|
Nothing -> pure mempty
|
2021-01-04 08:24:50 +01:00
|
|
|
|
mapArguments variables argumentTypes = fmap fold
|
2020-12-26 06:31:56 +01:00
|
|
|
|
. traverse (findArgumentVariables variables argumentTypes)
|
|
|
|
|
mapDirectives variables = fmap fold
|
|
|
|
|
<$> traverse (findDirectiveVariables variables)
|
2021-01-22 09:26:22 +01:00
|
|
|
|
lookupInputObject variables objectFieldValue locationInfo
|
2021-02-02 07:15:30 +01:00
|
|
|
|
| Full.Node{ node = Full.Object objectFields } <- objectFieldValue
|
2021-01-22 09:26:22 +01:00
|
|
|
|
, Just (expectedType, _) <- locationInfo
|
|
|
|
|
, In.InputObjectBaseType inputObjectType <- expectedType
|
|
|
|
|
, In.InputObjectType _ _ fieldTypes' <- inputObjectType =
|
|
|
|
|
fold <$> traverse (traverseObjectField variables fieldTypes') objectFields
|
|
|
|
|
| otherwise = pure mempty
|
|
|
|
|
maybeUsageAllowed variableName variables locationInfo
|
|
|
|
|
| Just (locationType, locationValue) <- locationInfo
|
|
|
|
|
, findVariableDefinition' <- findVariableDefinition variableName
|
|
|
|
|
, Just variableDefinition <- find findVariableDefinition' variables
|
|
|
|
|
= maybeToSeq
|
|
|
|
|
<$> isVariableUsageAllowed locationType locationValue variableDefinition
|
|
|
|
|
| otherwise = pure mempty
|
2021-01-04 08:24:50 +01:00
|
|
|
|
findArgumentVariables :: [Full.VariableDefinition]
|
|
|
|
|
-> HashMap Full.Name In.Argument
|
|
|
|
|
-> Full.Argument
|
|
|
|
|
-> ValidationState m (Seq Error)
|
2020-12-26 06:31:56 +01:00
|
|
|
|
findArgumentVariables variables argumentTypes argument
|
|
|
|
|
| Full.Argument argumentName argumentValue _ <- argument
|
2021-02-02 07:15:30 +01:00
|
|
|
|
, Full.Node{ node = Full.Variable variableName } <- argumentValue
|
2021-01-22 09:26:22 +01:00
|
|
|
|
= maybeUsageAllowed variableName variables
|
|
|
|
|
$ locationPair extractArgument argumentTypes argumentName
|
2021-01-04 08:24:50 +01:00
|
|
|
|
| Full.Argument argumentName argumentValue _ <- argument
|
2021-01-22 09:26:22 +01:00
|
|
|
|
= lookupInputObject variables argumentValue
|
|
|
|
|
$ locationPair extractArgument argumentTypes argumentName
|
|
|
|
|
extractField (In.InputField _ locationType locationValue) =
|
|
|
|
|
(locationType, locationValue)
|
|
|
|
|
extractArgument (In.Argument _ locationType locationValue) =
|
|
|
|
|
(locationType, locationValue)
|
|
|
|
|
locationPair extract fieldTypes name =
|
|
|
|
|
extract <$> HashMap.lookup name fieldTypes
|
|
|
|
|
traverseObjectField variables fieldTypes Full.ObjectField{..}
|
|
|
|
|
| Full.Node{ node = Full.Variable variableName } <- value
|
|
|
|
|
= maybeUsageAllowed variableName variables
|
|
|
|
|
$ locationPair extractField fieldTypes name
|
|
|
|
|
| otherwise = lookupInputObject variables value
|
|
|
|
|
$ locationPair extractField fieldTypes name
|
2020-12-26 06:31:56 +01:00
|
|
|
|
findVariableDefinition variableName variableDefinition =
|
|
|
|
|
let Full.VariableDefinition variableName' _ _ _ = variableDefinition
|
|
|
|
|
in variableName == variableName'
|
2021-01-22 09:26:22 +01:00
|
|
|
|
isVariableUsageAllowed locationType locationDefaultValue variableDefinition
|
2020-12-27 11:47:29 +01:00
|
|
|
|
| Full.VariableDefinition _ variableType _ _ <- variableDefinition
|
|
|
|
|
, Full.TypeNonNull _ <- variableType =
|
2020-12-26 06:31:56 +01:00
|
|
|
|
typesCompatibleOrError variableDefinition locationType
|
|
|
|
|
| Just nullableLocationType <- unwrapInType locationType
|
2021-01-04 08:24:50 +01:00
|
|
|
|
, Full.VariableDefinition _ variableType variableDefaultValue _ <-
|
|
|
|
|
variableDefinition
|
2020-12-27 11:47:29 +01:00
|
|
|
|
, hasNonNullVariableDefaultValue' <-
|
|
|
|
|
hasNonNullVariableDefaultValue variableDefaultValue
|
2020-12-26 06:31:56 +01:00
|
|
|
|
, hasLocationDefaultValue <- isJust locationDefaultValue =
|
2021-01-04 08:24:50 +01:00
|
|
|
|
if (hasNonNullVariableDefaultValue' || hasLocationDefaultValue)
|
|
|
|
|
&& areTypesCompatible variableType nullableLocationType
|
|
|
|
|
then pure Nothing
|
2020-12-27 11:47:29 +01:00
|
|
|
|
else pure $ makeError variableDefinition locationType
|
2020-12-26 06:31:56 +01:00
|
|
|
|
| otherwise = typesCompatibleOrError variableDefinition locationType
|
|
|
|
|
typesCompatibleOrError variableDefinition locationType
|
|
|
|
|
| Full.VariableDefinition _ variableType _ _ <- variableDefinition
|
|
|
|
|
, areTypesCompatible variableType locationType = pure Nothing
|
2020-12-27 11:47:29 +01:00
|
|
|
|
| otherwise = pure $ makeError variableDefinition locationType
|
|
|
|
|
areTypesCompatible nonNullType (unwrapInType -> Just nullableLocationType)
|
|
|
|
|
| Full.TypeNonNull (Full.NonNullTypeNamed namedType) <- nonNullType =
|
|
|
|
|
areTypesCompatible (Full.TypeNamed namedType) nullableLocationType
|
|
|
|
|
| Full.TypeNonNull (Full.NonNullTypeList namedList) <- nonNullType =
|
|
|
|
|
areTypesCompatible (Full.TypeList namedList) nullableLocationType
|
2020-12-26 06:31:56 +01:00
|
|
|
|
areTypesCompatible _ (In.isNonNullType -> True) = False
|
|
|
|
|
areTypesCompatible (Full.TypeNonNull nonNullType) locationType
|
|
|
|
|
| Full.NonNullTypeNamed namedType <- nonNullType =
|
|
|
|
|
areTypesCompatible (Full.TypeNamed namedType) locationType
|
|
|
|
|
| Full.NonNullTypeList namedType <- nonNullType =
|
|
|
|
|
areTypesCompatible (Full.TypeList namedType) locationType
|
|
|
|
|
areTypesCompatible variableType locationType
|
|
|
|
|
| Full.TypeList itemVariableType <- variableType
|
|
|
|
|
, In.ListType itemLocationType <- locationType =
|
|
|
|
|
areTypesCompatible itemVariableType itemLocationType
|
|
|
|
|
| areIdentical variableType locationType = True
|
|
|
|
|
| otherwise = False
|
|
|
|
|
areIdentical (Full.TypeList typeList) (In.ListType itemLocationType) =
|
|
|
|
|
areIdentical typeList itemLocationType
|
|
|
|
|
areIdentical (Full.TypeNonNull nonNullType) locationType
|
|
|
|
|
| Full.NonNullTypeList nonNullList <- nonNullType
|
|
|
|
|
, In.NonNullListType itemLocationType <- locationType =
|
|
|
|
|
areIdentical nonNullList itemLocationType
|
|
|
|
|
| Full.NonNullTypeNamed _ <- nonNullType
|
|
|
|
|
, In.ListBaseType _ <- locationType = False
|
|
|
|
|
| Full.NonNullTypeNamed nonNullList <- nonNullType
|
|
|
|
|
, In.isNonNullType locationType =
|
|
|
|
|
nonNullList == inputTypeName locationType
|
|
|
|
|
areIdentical (Full.TypeNamed _) (In.ListBaseType _) = False
|
|
|
|
|
areIdentical (Full.TypeNamed typeNamed) locationType
|
|
|
|
|
| not $ In.isNonNullType locationType =
|
|
|
|
|
typeNamed == inputTypeName locationType
|
|
|
|
|
areIdentical _ _ = False
|
|
|
|
|
hasNonNullVariableDefaultValue (Just (Full.Node Full.ConstNull _)) = False
|
|
|
|
|
hasNonNullVariableDefaultValue Nothing = False
|
|
|
|
|
hasNonNullVariableDefaultValue _ = True
|
|
|
|
|
unwrapInType (In.NonNullScalarType nonNullType) =
|
|
|
|
|
Just $ In.NamedScalarType nonNullType
|
|
|
|
|
unwrapInType (In.NonNullEnumType nonNullType) =
|
|
|
|
|
Just $ In.NamedEnumType nonNullType
|
|
|
|
|
unwrapInType (In.NonNullInputObjectType nonNullType) =
|
|
|
|
|
Just $ In.NamedInputObjectType nonNullType
|
|
|
|
|
unwrapInType (In.NonNullListType nonNullType) =
|
|
|
|
|
Just $ In.ListType nonNullType
|
|
|
|
|
unwrapInType _ = Nothing
|
2020-12-27 11:47:29 +01:00
|
|
|
|
makeError variableDefinition expectedType =
|
|
|
|
|
let Full.VariableDefinition variableName variableType _ location' =
|
|
|
|
|
variableDefinition
|
|
|
|
|
in Just $ Error
|
|
|
|
|
{ message = concat
|
|
|
|
|
[ "Variable \"$"
|
|
|
|
|
, Text.unpack variableName
|
|
|
|
|
, "\" of type \""
|
|
|
|
|
, show variableType
|
|
|
|
|
, "\" used in position expecting type \""
|
|
|
|
|
, show expectedType
|
|
|
|
|
, "\"."
|
|
|
|
|
]
|
|
|
|
|
, locations = [location']
|
|
|
|
|
}
|
2021-02-03 05:42:10 +01:00
|
|
|
|
|
|
|
|
|
-- | Literal values must be compatible with the type expected in the position
|
|
|
|
|
-- they are found as per the coercion rules.
|
|
|
|
|
--
|
|
|
|
|
-- The type expected in a position include the type defined by the argument a
|
|
|
|
|
-- value is provided for, the type defined by an input object field a value is
|
|
|
|
|
-- provided for, and the type of a variable definition a default value is
|
|
|
|
|
-- provided for.
|
|
|
|
|
valuesOfCorrectTypeRule :: forall m. Rule m
|
|
|
|
|
valuesOfCorrectTypeRule = ValueRule go constGo
|
|
|
|
|
where
|
|
|
|
|
go (Just inputType) value
|
|
|
|
|
| Just constValue <- toConstNode value =
|
|
|
|
|
lift $ check inputType constValue
|
2021-03-16 10:08:13 +01:00
|
|
|
|
go _ _ = lift mempty -- This rule checks only literals.
|
2021-02-03 05:42:10 +01:00
|
|
|
|
toConstNode Full.Node{..} = flip Full.Node location <$> toConst node
|
|
|
|
|
toConst (Full.Variable _) = Nothing
|
|
|
|
|
toConst (Full.Int integer) = Just $ Full.ConstInt integer
|
|
|
|
|
toConst (Full.Float double) = Just $ Full.ConstFloat double
|
|
|
|
|
toConst (Full.String string) = Just $ Full.ConstString string
|
|
|
|
|
toConst (Full.Boolean boolean) = Just $ Full.ConstBoolean boolean
|
|
|
|
|
toConst Full.Null = Just Full.ConstNull
|
|
|
|
|
toConst (Full.Enum enum) = Just $ Full.ConstEnum enum
|
|
|
|
|
toConst (Full.List values) =
|
2021-03-14 12:19:30 +01:00
|
|
|
|
Just $ Full.ConstList $ catMaybes $ toConstNode <$> values
|
2021-02-03 05:42:10 +01:00
|
|
|
|
toConst (Full.Object fields) = Just $ Full.ConstObject
|
|
|
|
|
$ catMaybes $ constObjectField <$> fields
|
|
|
|
|
constObjectField Full.ObjectField{..}
|
|
|
|
|
| Just constValue <- toConstNode value =
|
|
|
|
|
Just $ Full.ObjectField name constValue location
|
|
|
|
|
| otherwise = Nothing
|
|
|
|
|
constGo Nothing = const $ lift mempty
|
|
|
|
|
constGo (Just inputType) = lift . check inputType
|
|
|
|
|
check :: In.Type -> Full.Node Full.ConstValue -> Seq Error
|
|
|
|
|
check _ Full.Node{ node = Full.ConstNull } =
|
|
|
|
|
mempty -- Ignore, required fields are checked elsewhere.
|
|
|
|
|
check (In.ScalarBaseType scalarType) Full.Node{ node }
|
|
|
|
|
| Definition.ScalarType "Int" _ <- scalarType
|
|
|
|
|
, Full.ConstInt _ <- node = mempty
|
|
|
|
|
| Definition.ScalarType "Boolean" _ <- scalarType
|
|
|
|
|
, Full.ConstBoolean _ <- node = mempty
|
|
|
|
|
| Definition.ScalarType "String" _ <- scalarType
|
|
|
|
|
, Full.ConstString _ <- node = mempty
|
|
|
|
|
| Definition.ScalarType "ID" _ <- scalarType
|
|
|
|
|
, Full.ConstString _ <- node = mempty
|
|
|
|
|
| Definition.ScalarType "ID" _ <- scalarType
|
|
|
|
|
, Full.ConstInt _ <- node = mempty
|
|
|
|
|
| Definition.ScalarType "Float" _ <- scalarType
|
|
|
|
|
, Full.ConstFloat _ <- node = mempty
|
|
|
|
|
| Definition.ScalarType "Float" _ <- scalarType
|
|
|
|
|
, Full.ConstInt _ <- node = mempty
|
|
|
|
|
check (In.EnumBaseType enumType) Full.Node{ node }
|
|
|
|
|
| Definition.EnumType _ _ members <- enumType
|
|
|
|
|
, Full.ConstEnum memberValue <- node
|
|
|
|
|
, HashMap.member memberValue members = mempty
|
|
|
|
|
check (In.InputObjectBaseType objectType) Full.Node{ node }
|
|
|
|
|
| In.InputObjectType _ _ typeFields <- objectType
|
|
|
|
|
, Full.ConstObject valueFields <- node =
|
|
|
|
|
foldMap (checkObjectField typeFields) valueFields
|
|
|
|
|
check (In.ListBaseType listType) constValue@Full.Node{ .. }
|
2021-03-16 10:08:13 +01:00
|
|
|
|
-- Skip, lists are checked recursively by the validation traverser.
|
|
|
|
|
| Full.ConstList _ <- node = mempty
|
2021-02-03 05:42:10 +01:00
|
|
|
|
| otherwise = check listType constValue
|
|
|
|
|
check inputType Full.Node{ .. } = pure $ Error
|
|
|
|
|
{ message = concat
|
|
|
|
|
[ "Value "
|
|
|
|
|
, show node, " cannot be coerced to type \""
|
|
|
|
|
, show inputType
|
|
|
|
|
, "\"."
|
|
|
|
|
]
|
|
|
|
|
, locations = [location]
|
|
|
|
|
}
|
|
|
|
|
checkObjectField typeFields Full.ObjectField{..}
|
|
|
|
|
| Just typeField <- HashMap.lookup name typeFields
|
|
|
|
|
, In.InputField _ fieldType _ <- typeField =
|
|
|
|
|
check fieldType value
|
|
|
|
|
checkObjectField _ _ = mempty
|