-
Notifications
You must be signed in to change notification settings - Fork 32
Variable definitions validation #186
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
d7ac50e
0330216
112c494
1c1393d
83c3c5e
471a658
dc1ea69
dd9d4fe
38382a0
07a242f
e3106fa
d8f2040
79aeb22
c48296f
11139e2
ae3c668
2e3a5ed
8c9d7fb
4415537
f8e7098
59e532e
205a4ab
2ee2296
aa7af4b
03b43ba
8ae8810
1fb7249
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,9 +33,11 @@ module GraphQL.Internal.Schema | |
, NonNullType(..) | ||
, DefinesTypes(..) | ||
, doesFragmentTypeApply | ||
, getInputTypeDefinition | ||
-- * The schema | ||
, Schema | ||
, makeSchema | ||
, emptySchema | ||
, lookupType | ||
) where | ||
|
||
|
@@ -58,6 +60,9 @@ newtype Schema = Schema (Map Name TypeDefinition) deriving (Eq, Ord, Show) | |
makeSchema :: ObjectTypeDefinition -> Schema | ||
makeSchema = Schema . getDefinedTypes | ||
|
||
emptySchema :: Schema | ||
emptySchema = Schema (Map.empty :: (Map Name TypeDefinition)) | ||
|
||
-- | Find the type with the given name in the schema. | ||
lookupType :: Schema -> Name -> Maybe TypeDefinition | ||
lookupType (Schema schema) name = Map.lookup name schema | ||
|
@@ -301,3 +306,14 @@ doesFragmentTypeApply objectType fragmentType = | |
where | ||
implements (ObjectTypeDefinition _ interfaces _) int = int `elem` interfaces | ||
branchOf obj (UnionTypeDefinition _ branches) = obj `elem` branches | ||
|
||
-- | Convert the given TypeDefinition to an InputTypeDefinition if it's a valid InputTypeDefinition | ||
-- (because InputTypeDefinition is a subset of TypeDefinition) | ||
-- see <http://facebook.github.io/graphql/June2018/#sec-Input-and-Output-Types> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please put single quotes around |
||
getInputTypeDefinition :: TypeDefinition -> Maybe InputTypeDefinition | ||
getInputTypeDefinition td = | ||
case td of | ||
TypeDefinitionInputObject itd -> Just (InputTypeDefinitionObject itd) | ||
TypeDefinitionScalar itd -> Just (InputTypeDefinitionScalar itd) | ||
TypeDefinitionEnum itd -> Just (InputTypeDefinitionEnum itd) | ||
_ -> Nothing |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -81,6 +81,13 @@ import GraphQL.Internal.Schema | |
, Schema | ||
, doesFragmentTypeApply | ||
, lookupType | ||
, AnnotatedType(..) | ||
, InputType | ||
, InputType (BuiltinInputType, DefinedInputType) | ||
, Builtin (..) | ||
, AnnotatedType (TypeNamed, TypeNonNull) | ||
, NonNullType(NonNullTypeNamed) | ||
, getInputTypeDefinition | ||
) | ||
import GraphQL.Value | ||
( Value | ||
|
@@ -174,7 +181,7 @@ validateOperations schema fragments ops = do | |
traverse validateNode deduped | ||
where | ||
validateNode (operationType, AST.Node _ vars directives ss) = | ||
operationType <$> lift (validateVariableDefinitions vars) | ||
operationType <$> lift ((validateVariableDefinitions schema) vars) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think the inner parentheses are necessary. |
||
<*> lift (validateDirectives directives) | ||
<*> validateSelectionSet schema fragments ss | ||
|
||
|
@@ -626,7 +633,8 @@ validateArguments args = Arguments <$> mapErrors DuplicateArgument (makeMap [(na | |
data VariableDefinition | ||
= VariableDefinition | ||
{ variable :: Variable -- ^ The name of the variable | ||
, variableType :: AST.GType -- ^ The type of the variable | ||
-- , variableType :: AST.GType -- ^ The type of the variable | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please remove all commented out code. |
||
, variableType :: AnnotatedType InputType -- ^ The type of the variable | ||
, defaultValue :: Maybe Value -- ^ An optional default value for the variable | ||
} deriving (Eq, Ord, Show) | ||
|
||
|
@@ -642,16 +650,47 @@ emptyVariableDefinitions :: VariableDefinitions | |
emptyVariableDefinitions = mempty | ||
|
||
-- | Ensure that a set of variable definitions is valid. | ||
validateVariableDefinitions :: [AST.VariableDefinition] -> Validation VariableDefinitions | ||
validateVariableDefinitions vars = do | ||
validatedDefns <- traverse validateVariableDefinition vars | ||
validateVariableDefinitions :: Schema -> [AST.VariableDefinition] -> Validation VariableDefinitions | ||
validateVariableDefinitions schema vars = do | ||
validatedDefns <- traverse (validateVariableDefinition schema) vars | ||
let items = [ (variable defn, defn) | defn <- validatedDefns] | ||
mapErrors DuplicateVariableDefinition (makeMap items) | ||
|
||
-- | Ensure that a variable definition is a valid one. | ||
validateVariableDefinition :: AST.VariableDefinition -> Validation VariableDefinition | ||
validateVariableDefinition (AST.VariableDefinition name varType value) = | ||
VariableDefinition name varType <$> traverse validateDefaultValue value | ||
validateVariableDefinition :: Schema -> AST.VariableDefinition -> Validation VariableDefinition | ||
validateVariableDefinition schema (AST.VariableDefinition var varType value) = | ||
case validateTypeAssertion schema var varType of | ||
Left e -> throwE e | ||
Right t -> VariableDefinition var t <$> traverse validateDefaultValue value | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Couple of things here. First, I think this code could be written the same way as: do
t <- validateTypeAssertion schema var varType
VariableDefinition var t <$> traverse validateDefaultValue value since that's the way the But, you probably don't want to use the monadic behaviour. Why? Because writing this as "first From a user experience point of view, this means if there's a problem with the type assertion and a problem with the default value, they will only get the error from the type assertion. This is not ideal. Instead, you want to use the applicative behaviour. e.g. VariableDefinition var <$> validateTypeAssertion schema var varType <*> traverse validateDefaultValue value This clearly shows that the two are independent, and the Does this make sense? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. Not sure I thoroughly grasp the entire concepts of Applicative and Monad, but as far as this example is concerned I think I understand. |
||
|
||
-- | Ensure that a variable has a correct type declaration given a schema. | ||
validateTypeAssertion :: Schema -> Variable -> AST.GType -> Either ValidationError (AnnotatedType InputType) | ||
validateTypeAssertion schema var varTypeAST = | ||
case typeDef of | ||
Nothing -> fmap (astAnnotationToSchemaAnnotation varTypeAST) (validateVariableTypeBuiltin var varTypeNameAST) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use e.g. astAnnotationToSchemaAnnotation varTypeAST <$> validateVariableTypeBuiltin var varTypeNameAST There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, since both branches of the |
||
Just value -> astAnnotationToSchemaAnnotation varTypeAST . DefinedInputType <$> maybeToEither (VariableTypeIsNotInputType var varTypeNameAST) (getInputTypeDefinition value) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we use |
||
where | ||
varTypeNameAST = getName varTypeAST | ||
typeDef = lookupType schema varTypeNameAST | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd inline this in the |
||
|
||
-- | validate a variable type which has no type definition (either builtin or not in the schema) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit. Please capitalize "validate" and end the sentence with a full stop. |
||
validateVariableTypeBuiltin :: Variable -> Name -> Either ValidationError InputType | ||
validateVariableTypeBuiltin var tname | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please say There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does this take a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It takes a variable name because it enables us to give the variable name in the error message (if the given type is not a builtin type, which is the error message if we give a random type name for instance). I do believe this is valuable. |
||
| tname == getName GInt = Right (BuiltinInputType GInt) | ||
| tname == getName GBool = Right (BuiltinInputType GBool) | ||
| tname == getName GString = Right (BuiltinInputType GString) | ||
| tname == getName GFloat = Right (BuiltinInputType GFloat) | ||
| tname == getName GID = Right (BuiltinInputType GID) | ||
| otherwise = Left (VariableTypeNotFound var tname) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel like there should be a better way of doing this. At least part of the answer is that there should be a function in |
||
|
||
-- | simple translation between ast annotation types and schema annotation types | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: capital first letter, capitalize "AST" and end with a full stop. |
||
astAnnotationToSchemaAnnotation :: AST.GType -> a -> AnnotatedType a | ||
astAnnotationToSchemaAnnotation gtype schematn = | ||
case gtype of | ||
AST.TypeNamed _ -> TypeNamed schematn | ||
AST.TypeList (AST.ListType asttn) -> astAnnotationToSchemaAnnotation asttn schematn | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks wrong. The annotated type of "list of int" should be "list of int", not "int". |
||
AST.TypeNonNull (AST.NonNullTypeNamed _) -> TypeNonNull (NonNullTypeNamed schematn) | ||
AST.TypeNonNull (AST.NonNullTypeList (AST.ListType asttn)) -> astAnnotationToSchemaAnnotation asttn schematn | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function should live in |
||
|
||
-- | Ensure that a default value contains no variables. | ||
validateDefaultValue :: AST.DefaultValue -> Validation Value | ||
|
@@ -776,6 +815,11 @@ data ValidationError | |
| IncompatibleFields Name | ||
-- | There's a type condition that's not present in the schema. | ||
| TypeConditionNotFound Name | ||
-- | There's a variable type that's not present in the schema. | ||
| VariableTypeNotFound Variable Name | ||
-- | A variable was defined with a non input type. | ||
-- <http://facebook.github.io/graphql/June2018/#sec-Variables-Are-Input-Types> | ||
| VariableTypeIsNotInputType Variable Name | ||
deriving (Eq, Show) | ||
|
||
instance GraphQLError ValidationError where | ||
|
@@ -798,6 +842,8 @@ instance GraphQLError ValidationError where | |
formatError (MismatchedArguments name) = "Two different sets of arguments given for same response key: " <> show name | ||
formatError (IncompatibleFields name) = "Field " <> show name <> " has a leaf in one place and a non-leaf in another." | ||
formatError (TypeConditionNotFound name) = "Type condition " <> show name <> " not found in schema." | ||
formatError (VariableTypeNotFound var name) = "Type named " <> show name <> " for variable " <> show var <> " is not in the schema." | ||
formatError (VariableTypeIsNotInputType var name) = "Type named " <> show name <> " for variable " <> show var <> " is not an input type." | ||
|
||
type ValidationErrors = NonEmpty ValidationError | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
{-# LANGUAGE TypeApplications #-} | ||
{-# LANGUAGE DataKinds #-} | ||
|
||
-- | Tests for query validation. | ||
module ValidationTests (tests) where | ||
|
@@ -11,8 +12,9 @@ import Test.Tasty (TestTree) | |
import Test.Tasty.Hspec (testSpec, describe, it, shouldBe) | ||
|
||
import GraphQL.Internal.Name (Name) | ||
import qualified Data.Map as Map | ||
import qualified GraphQL.Internal.Syntax.AST as AST | ||
import GraphQL.Internal.Schema (Schema) | ||
import GraphQL.Internal.Schema (Schema, emptySchema) | ||
import GraphQL.Internal.Validation | ||
( ValidationError(..) | ||
, findDuplicates | ||
|
@@ -27,11 +29,11 @@ someName = "name" | |
|
||
dog :: Name | ||
dog = "dog" | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Please don't add unnecessary whitespace. |
||
-- | Schema used for these tests. Since none of them do type-level stuff, we | ||
-- don't need to define it. | ||
schema :: Schema | ||
schema = panic "schema evaluated. We weren't expecting that." | ||
schema = emptySchema | ||
|
||
tests :: IO TestTree | ||
tests = testSpec "Validation" $ do | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add a doc comment. Ideally it should say what it's used for, or why you might want to use it.