Skip to content

Commit 377c332

Browse files
authored
Merge pull request #186 from theobat/variable-definitions-validation
Variable definitions validation
2 parents f06c7af + 1fb7249 commit 377c332

File tree

10 files changed

+502
-25
lines changed

10 files changed

+502
-25
lines changed

graphql-api.cabal

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
-- This file has been generated from package.yaml by hpack version 0.20.0.
1+
-- This file has been generated from package.yaml by hpack version 0.28.2.
22
--
33
-- see: https://github.com/sol/hpack
44
--
5-
-- hash: 6a38b887cec0d4a157469f5d73041fd16cb286d8f445f4e213c6f08965dbc563
5+
-- hash: 6db006b020fe198ac64b8a50f8335017251389b7c34dfc553675e38eb001a428
66

77
name: graphql-api
88
version: 0.3.0
@@ -23,7 +23,6 @@ license: Apache
2323
license-file: LICENSE.Apache-2.0
2424
build-type: Simple
2525
cabal-version: >= 1.10
26-
2726
extra-source-files:
2827
CHANGELOG.rst
2928

scripts/hpc-ratchet

+4-4
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ In a just world, this would be a separate config file, or command-line arguments
3535
Each item represents the number of "things" we are OK with not being covered.
3636
"""
3737
COVERAGE_TOLERANCE = {
38-
ALTERNATIVES: 175,
38+
ALTERNATIVES: 161,
3939
BOOLEANS: 8,
40-
EXPRESSIONS: 1494,
41-
LOCAL_DECLS: 15,
42-
TOP_LEVEL_DECLS: 685,
40+
EXPRESSIONS: 1416,
41+
LOCAL_DECLS: 14,
42+
TOP_LEVEL_DECLS: 669,
4343
}
4444

4545

src/GraphQL/Internal/Execution.hs

+3-1
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ import GraphQL.Value
2828
, Object'(..)
2929
)
3030
import GraphQL.Internal.Output (GraphQLError(..))
31+
import GraphQL.Internal.Schema
32+
( AnnotatedType (TypeNonNull)
33+
)
3134
import GraphQL.Internal.Validation
3235
( Operation
3336
, QueryDocument(..)
3437
, VariableDefinition(..)
3538
, VariableValue
3639
, Variable
37-
, GType(..)
3840
)
3941

4042
-- | Get an operation from a GraphQL document

src/GraphQL/Internal/Schema.hs

+66-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ module GraphQL.Internal.Schema
2222
, InterfaceTypeDefinition(..)
2323
, ObjectTypeDefinition(..)
2424
, UnionTypeDefinition(..)
25+
, ScalarTypeDefinition(..)
2526
-- ** Input types
2627
, InputType(..)
2728
, InputTypeDefinition(..)
@@ -33,15 +34,20 @@ module GraphQL.Internal.Schema
3334
, NonNullType(..)
3435
, DefinesTypes(..)
3536
, doesFragmentTypeApply
37+
, getInputTypeDefinition
38+
, builtinFromName
39+
, astAnnotationToSchemaAnnotation
3640
-- * The schema
3741
, Schema
3842
, makeSchema
43+
, emptySchema
3944
, lookupType
4045
) where
4146

4247
import Protolude
4348

4449
import qualified Data.Map as Map
50+
import qualified GraphQL.Internal.Syntax.AST as AST
4551
import GraphQL.Value (Value)
4652
import GraphQL.Internal.Name (HasName(..), Name)
4753

@@ -58,6 +64,11 @@ newtype Schema = Schema (Map Name TypeDefinition) deriving (Eq, Ord, Show)
5864
makeSchema :: ObjectTypeDefinition -> Schema
5965
makeSchema = Schema . getDefinedTypes
6066

67+
-- | Create an empty schema for testing purpose.
68+
--
69+
emptySchema :: Schema
70+
emptySchema = Schema (Map.empty :: (Map Name TypeDefinition))
71+
6172
-- | Find the type with the given name in the schema.
6273
lookupType :: Schema -> Name -> Maybe TypeDefinition
6374
lookupType (Schema schema) name = Map.lookup name schema
@@ -157,14 +168,19 @@ instance HasName FieldDefinition where
157168
getName (FieldDefinition name _ _) = name
158169

159170
instance DefinesTypes FieldDefinition where
160-
getDefinedTypes (FieldDefinition _ _ retVal) = getDefinedTypes (getAnnotatedType retVal)
171+
getDefinedTypes (FieldDefinition _ args retVal) =
172+
getDefinedTypes (getAnnotatedType retVal) <>
173+
foldMap getDefinedTypes args
161174

162175
data ArgumentDefinition = ArgumentDefinition Name (AnnotatedType InputType) (Maybe DefaultValue)
163176
deriving (Eq, Ord, Show)
164177

165178
instance HasName ArgumentDefinition where
166179
getName (ArgumentDefinition name _ _) = name
167180

181+
instance DefinesTypes ArgumentDefinition where
182+
getDefinedTypes (ArgumentDefinition _ annotatedType _) = getDefinedTypes $ getAnnotatedType annotatedType
183+
168184
data InterfaceTypeDefinition = InterfaceTypeDefinition Name (NonEmpty FieldDefinition)
169185
deriving (Eq, Ord, Show)
170186

@@ -256,6 +272,12 @@ instance HasName InputType where
256272
getName (DefinedInputType x) = getName x
257273
getName (BuiltinInputType x) = getName x
258274

275+
instance DefinesTypes InputType where
276+
getDefinedTypes inputType =
277+
case inputType of
278+
DefinedInputType typeDefinition -> getDefinedTypes typeDefinition
279+
BuiltinInputType _ -> mempty
280+
259281
data InputTypeDefinition
260282
= InputTypeDefinitionObject InputObjectTypeDefinition
261283
| InputTypeDefinitionScalar ScalarTypeDefinition
@@ -267,6 +289,13 @@ instance HasName InputTypeDefinition where
267289
getName (InputTypeDefinitionScalar x) = getName x
268290
getName (InputTypeDefinitionEnum x) = getName x
269291

292+
instance DefinesTypes InputTypeDefinition where
293+
getDefinedTypes inputTypeDefinition =
294+
case inputTypeDefinition of
295+
InputTypeDefinitionObject typeDefinition -> getDefinedTypes (TypeDefinitionInputObject typeDefinition)
296+
InputTypeDefinitionScalar typeDefinition -> getDefinedTypes (TypeDefinitionScalar typeDefinition)
297+
InputTypeDefinitionEnum typeDefinition -> getDefinedTypes (TypeDefinitionEnum typeDefinition)
298+
270299
-- | A literal value specified as a default as part of a type definition.
271300
--
272301
-- Use this type alias when you want to be clear that a definition may include
@@ -301,3 +330,39 @@ doesFragmentTypeApply objectType fragmentType =
301330
where
302331
implements (ObjectTypeDefinition _ interfaces _) int = int `elem` interfaces
303332
branchOf obj (UnionTypeDefinition _ branches) = obj `elem` branches
333+
334+
-- | Convert the given 'TypeDefinition' to an 'InputTypeDefinition' if it's a valid 'InputTypeDefinition'
335+
-- (because 'InputTypeDefinition' is a subset of 'TypeDefinition')
336+
-- see <http://facebook.github.io/graphql/June2018/#sec-Input-and-Output-Types>
337+
getInputTypeDefinition :: TypeDefinition -> Maybe InputTypeDefinition
338+
getInputTypeDefinition td =
339+
case td of
340+
TypeDefinitionInputObject itd -> Just (InputTypeDefinitionObject itd)
341+
TypeDefinitionScalar itd -> Just (InputTypeDefinitionScalar itd)
342+
TypeDefinitionEnum itd -> Just (InputTypeDefinitionEnum itd)
343+
_ -> Nothing
344+
345+
-- | Create a 'Builtin' type from a 'Name'
346+
--
347+
-- Mostly used for the AST validation
348+
-- theobat: There's probably a better way to do it but can't find it right now
349+
builtinFromName :: Name -> Maybe Builtin
350+
builtinFromName typeName
351+
| typeName == getName GInt = Just GInt
352+
| typeName == getName GBool = Just GBool
353+
| typeName == getName GString = Just GString
354+
| typeName == getName GFloat = Just GFloat
355+
| typeName == getName GID = Just GID
356+
| otherwise = Nothing
357+
358+
-- | Simple translation between 'AST' annotation types and 'Schema' annotation types
359+
--
360+
-- AST type annotations do not need any validation.
361+
-- GraphQL annotations are semantic decorations around type names to indicate type composition (list/non null).
362+
astAnnotationToSchemaAnnotation :: AST.GType -> a -> AnnotatedType a
363+
astAnnotationToSchemaAnnotation gtype schemaTypeName =
364+
case gtype of
365+
AST.TypeNamed _ -> TypeNamed schemaTypeName
366+
AST.TypeList (AST.ListType astTypeName) -> TypeList (ListType $ astAnnotationToSchemaAnnotation astTypeName schemaTypeName)
367+
AST.TypeNonNull (AST.NonNullTypeNamed _) -> TypeNonNull (NonNullTypeNamed schemaTypeName)
368+
AST.TypeNonNull (AST.NonNullTypeList (AST.ListType astTypeName)) -> TypeNonNull (NonNullTypeList (ListType (astAnnotationToSchemaAnnotation astTypeName schemaTypeName)))

src/GraphQL/Internal/Syntax/AST.hs

+12-2
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,11 @@ import Protolude
5353
import Test.QuickCheck (Arbitrary(..), listOf, oneof)
5454

5555
import GraphQL.Internal.Arbitrary (arbitraryText)
56-
import GraphQL.Internal.Name (Name)
57-
56+
import GraphQL.Internal.Name
57+
( Name
58+
, HasName(..)
59+
)
60+
5861
-- * Documents
5962

6063
-- | A 'QueryDocument' is something a user might send us.
@@ -176,6 +179,13 @@ data GType = TypeNamed NamedType
176179
| TypeNonNull NonNullType
177180
deriving (Eq, Ord, Show)
178181

182+
-- | Get the name of the given 'GType'.
183+
instance HasName GType where
184+
getName (TypeNamed (NamedType n)) = n
185+
getName (TypeList (ListType t)) = getName t
186+
getName (TypeNonNull (NonNullTypeNamed (NamedType n))) = n
187+
getName (TypeNonNull (NonNullTypeList (ListType l))) = getName l
188+
179189
newtype NamedType = NamedType Name deriving (Eq, Ord, Show)
180190

181191
newtype ListType = ListType GType deriving (Eq, Ord, Show)

src/GraphQL/Internal/Validation.hs

+54-8
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ module GraphQL.Internal.Validation
5858
, getResponseKey
5959
-- * Exported for testing
6060
, findDuplicates
61+
, formatErrors
6162
) where
6263

6364
import Protolude hiding ((<>), throwE)
@@ -81,6 +82,12 @@ import GraphQL.Internal.Schema
8182
, Schema
8283
, doesFragmentTypeApply
8384
, lookupType
85+
, AnnotatedType(..)
86+
, InputType (BuiltinInputType, DefinedInputType)
87+
, AnnotatedType
88+
, getInputTypeDefinition
89+
, builtinFromName
90+
, astAnnotationToSchemaAnnotation
8491
)
8592
import GraphQL.Value
8693
( Value
@@ -174,7 +181,7 @@ validateOperations schema fragments ops = do
174181
traverse validateNode deduped
175182
where
176183
validateNode (operationType, AST.Node _ vars directives ss) =
177-
operationType <$> lift (validateVariableDefinitions vars)
184+
operationType <$> lift (validateVariableDefinitions schema vars)
178185
<*> lift (validateDirectives directives)
179186
<*> validateSelectionSet schema fragments ss
180187

@@ -626,7 +633,7 @@ validateArguments args = Arguments <$> mapErrors DuplicateArgument (makeMap [(na
626633
data VariableDefinition
627634
= VariableDefinition
628635
{ variable :: Variable -- ^ The name of the variable
629-
, variableType :: AST.GType -- ^ The type of the variable
636+
, variableType :: AnnotatedType InputType -- ^ The type of the variable
630637
, defaultValue :: Maybe Value -- ^ An optional default value for the variable
631638
} deriving (Eq, Ord, Show)
632639

@@ -642,16 +649,43 @@ emptyVariableDefinitions :: VariableDefinitions
642649
emptyVariableDefinitions = mempty
643650

644651
-- | Ensure that a set of variable definitions is valid.
645-
validateVariableDefinitions :: [AST.VariableDefinition] -> Validation VariableDefinitions
646-
validateVariableDefinitions vars = do
647-
validatedDefns <- traverse validateVariableDefinition vars
652+
validateVariableDefinitions :: Schema -> [AST.VariableDefinition] -> Validation VariableDefinitions
653+
validateVariableDefinitions schema vars = do
654+
validatedDefns <- traverse (validateVariableDefinition schema) vars
648655
let items = [ (variable defn, defn) | defn <- validatedDefns]
649656
mapErrors DuplicateVariableDefinition (makeMap items)
650657

651658
-- | Ensure that a variable definition is a valid one.
652-
validateVariableDefinition :: AST.VariableDefinition -> Validation VariableDefinition
653-
validateVariableDefinition (AST.VariableDefinition name varType value) =
654-
VariableDefinition name varType <$> traverse validateDefaultValue value
659+
validateVariableDefinition :: Schema -> AST.VariableDefinition -> Validation VariableDefinition
660+
validateVariableDefinition schema (AST.VariableDefinition var varType value) =
661+
VariableDefinition var
662+
<$> validateTypeAssertion schema var varType
663+
<*> traverse validateDefaultValue value
664+
665+
-- | Ensure that a variable has a correct type declaration given a schema.
666+
validateTypeAssertion :: Schema -> Variable -> AST.GType -> Validation (AnnotatedType InputType)
667+
validateTypeAssertion schema var varTypeAST =
668+
astAnnotationToSchemaAnnotation varTypeAST <$>
669+
case lookupType schema varTypeNameAST of
670+
Nothing -> validateVariableTypeBuiltin var varTypeNameAST
671+
Just cleanTypeDef -> validateVariableTypeDefinition var cleanTypeDef
672+
where
673+
varTypeNameAST = getName varTypeAST
674+
675+
-- | Validate a variable type which has a type definition in the schema.
676+
validateVariableTypeDefinition :: Variable -> TypeDefinition -> Validation InputType
677+
validateVariableTypeDefinition var typeDef =
678+
case getInputTypeDefinition typeDef of
679+
Nothing -> throwE (VariableTypeIsNotInputType var $ getName typeDef)
680+
Just value -> pure (DefinedInputType value)
681+
682+
683+
-- | Validate a variable type which has no type definition (either builtin or not in the schema).
684+
validateVariableTypeBuiltin :: Variable -> Name -> Validation InputType
685+
validateVariableTypeBuiltin var typeName =
686+
case builtinFromName typeName of
687+
Nothing -> throwE (VariableTypeNotFound var typeName)
688+
Just builtin -> pure (BuiltinInputType builtin)
655689

656690
-- | Ensure that a default value contains no variables.
657691
validateDefaultValue :: AST.DefaultValue -> Validation Value
@@ -776,6 +810,11 @@ data ValidationError
776810
| IncompatibleFields Name
777811
-- | There's a type condition that's not present in the schema.
778812
| TypeConditionNotFound Name
813+
-- | There's a variable type that's not present in the schema.
814+
| VariableTypeNotFound Variable Name
815+
-- | A variable was defined with a non input type.
816+
-- <http://facebook.github.io/graphql/June2018/#sec-Variables-Are-Input-Types>
817+
| VariableTypeIsNotInputType Variable Name
779818
deriving (Eq, Show)
780819

781820
instance GraphQLError ValidationError where
@@ -798,6 +837,8 @@ instance GraphQLError ValidationError where
798837
formatError (MismatchedArguments name) = "Two different sets of arguments given for same response key: " <> show name
799838
formatError (IncompatibleFields name) = "Field " <> show name <> " has a leaf in one place and a non-leaf in another."
800839
formatError (TypeConditionNotFound name) = "Type condition " <> show name <> " not found in schema."
840+
formatError (VariableTypeNotFound var name) = "Type named " <> show name <> " for variable " <> show var <> " is not in the schema."
841+
formatError (VariableTypeIsNotInputType var name) = "Type named " <> show name <> " for variable " <> show var <> " is not an input type."
801842

802843
type ValidationErrors = NonEmpty ValidationError
803844

@@ -841,6 +882,11 @@ makeMap entries =
841882

842883
-- * Error handling
843884

885+
-- | Utility function for tests, format ErrorTypes to their text representation
886+
-- returns a list of error messages
887+
formatErrors :: [ValidationError] -> [Text]
888+
formatErrors errors = formatError <$> errors
889+
844890
-- | A 'Validator' is a value that can either be valid or have a non-empty
845891
-- list of errors.
846892
newtype Validator e a = Validator { runValidator :: Either (NonEmpty e) a } deriving (Eq, Show, Functor, Monad)

0 commit comments

Comments
 (0)