graphql/Data/GraphQL/Parser.hs
2015-09-13 17:44:31 +02:00

311 lines
8.8 KiB
Haskell

{-# LANGUAGE OverloadedStrings #-}
module Data.GraphQL.Parser where
import Prelude hiding (takeWhile)
import Control.Applicative (Alternative, (<|>), empty, many)
import Data.Char
import Data.Attoparsec.Text
( Parser
, (<?>)
, anyChar
, decimal
, double
, endOfLine
, isEndOfLine
, many1
, manyTill
, satisfy
, sepBy
, sepBy1
, skipMany
, skipSpace
, skipWhile
, signed
, space
, takeWhile
, takeWhile1
)
import Data.GraphQL.AST
-- * Name
-- XXX: Handle starting `_` and no number at the beginning:
-- https://facebook.github.io/graphql/#sec-Names
name :: Parser Name
name = takeWhile1 isAlphaNum
-- * Document
document :: Parser Document
document = s *>
(Document <$> many1_ definition
-- Try SelectionSet when no definition
<|> Document <$> pure
<$> DefinitionOperation
<$> Query mempty empty empty
<$> selectionSet)
<* s
<?> "document error!"
definition :: Parser Definition
definition = DefinitionOperation <$> operationDefinition
<|> DefinitionFragment <$> fragmentDefinition
<|> DefinitionType <$> typeDefinition
<?> "definition error!"
operationDefinition :: Parser OperationDefinition
operationDefinition =
op Query "query"
<|> op Mutation "mutation"
<?> "operationDefinition error!"
where
op f n = f <$ n <* s1 <*> name <* s1
<*> (variableDefinitions <* s <|> empty)
<*> directives <* s
<*> selectionSet
variableDefinitions :: Parser [VariableDefinition]
variableDefinitions = "(" *> s *> many1_ variableDefinition <* s <* ")"
variableDefinition :: Parser VariableDefinition
variableDefinition =
VariableDefinition <$> variable <* s <* ":" <* s
<*> type_ <* s
<*> (defaultValue <|> empty)
defaultValue :: Parser DefaultValue
defaultValue = "=" *> s *> value
-- In defense of good taste, I'm taking liberty of not allowing space between
-- '$' and the 'name' even though that's not in the spec.
variable :: Parser Variable
variable = Variable <$ "$" <*> name
selectionSet :: Parser SelectionSet
selectionSet = "{" *> s *> (SelectionSet <$> many1_ selection) <* s <* "}"
selection :: Parser Selection
selection = SelectionField <$> field
-- Inline first to catch `on` case
<|> SelectionInlineFragment <$> inlineFragment
<|> SelectionFragmentSpread <$> fragmentSpread
<?> "selection error!"
field :: Parser Field
field = Field <$> (alias <* s1 <|> empty)
<*> name <* s
<*> (arguments <* s <|> empty)
<*> directives <* s
<*> selectionSet
<?> "field error!"
alias :: Parser Alias
alias = name <* s <* ":"
arguments :: Parser [Argument]
arguments = "(" *> s *> many1_ argument <* s <* ")"
argument :: Parser Argument
argument = Argument <$> name <* s <* ":" <* s <*> value
-- * Fragments
fragmentSpread :: Parser FragmentSpread
-- TODO: Make sure it fails when `... on`.
-- See https://facebook.github.io/graphql/#FragmentSpread
fragmentSpread = FragmentSpread <$ "..." <* s <*> name <* s1 <*> many_ directive
-- InlineFragment tried first in order to guard against 'on' keyword
inlineFragment :: Parser InlineFragment
inlineFragment = InlineFragment <$ "..." <* s <* "on" <* s1
<*> typeCondition <* s
<*> directives <* s
<*> selectionSet
fragmentDefinition :: Parser FragmentDefinition
fragmentDefinition =
FragmentDefinition <$ "fragment" <* s1
<*> name <* s
<* "on" <* s1
<*> typeCondition <* s
<*> directives <* s
<*> selectionSet
typeCondition :: Parser TypeCondition
typeCondition = namedType
-- * Values
-- This will try to pick the first type it can parse. If you are working with
-- explicit types use the `typedValue` parser.
value :: Parser Value
value = -- TODO: Handle arbitrary precision.
ValueInt <$> signed decimal
<|> ValueFloat <$> signed double
<|> ValueBoolean <$> bool
-- TODO: Handle escape characters, unicode, etc
<|> ValueString <$ "\"" <*> takeWhile isAlphaNum <* "\""
-- `true` and `false` have been tried before
<|> ValueEnum <$> name
<|> ValueList <$> listValue
<|> ValueObject <$> objectValue
-- Notice it can be empty
listValue :: Parser ListValue
listValue = ListValue <$ "[" <* s <*> many_ value <* s <* "]"
-- Notice it can be empty
objectValue :: Parser ObjectValue
objectValue = ObjectValue <$ "{" <* s <*> many_ objectField <* s <* "}"
objectField :: Parser ObjectField
objectField = ObjectField <$> name <* s <* ":" <* s <*> value
bool :: Parser Bool
bool = True <$ "true"
<|> False <$ "false"
-- * Directives
directives :: Parser [Directive]
directives = many_ directive
directive :: Parser Directive
directive = Directive <$ "@" <*> name <* s <*> arguments
-- * Type Reference
type_ :: Parser Type
type_ = TypeNamed <$> namedType
<|> TypeList <$> listType
<|> TypeNonNull <$> nonNullType
namedType :: Parser NamedType
namedType = NamedType <$> name
listType :: Parser ListType
listType = ListType <$ "[" <* s <*> type_ <* s <* "]"
nonNullType :: Parser NonNullType
nonNullType = NonNullTypeNamed <$> namedType <* s <* "!"
<|> NonNullTypeList <$> listType <* s <* "!"
-- * Type Definition
-- TODO: what is Const Variable?
-- https://facebook.github.io/graphql/#Value
typeDefinition :: Parser TypeDefinition
typeDefinition =
TypeDefinitionObject <$> objectTypeDefinition
<|> TypeDefinitionInterface <$> interfaceTypeDefinition
<|> TypeDefinitionUnion <$> unionTypeDefinition
<|> TypeDefinitionScalar <$> scalarTypeDefinition
<|> TypeDefinitionEnum <$> enumTypeDefinition
<|> TypeDefinitionInputObject <$> inputObjectTypeDefinition
<|> TypeDefinitionTypeExtension <$> typeExtensionDefinition
<?> "typeDefinition error!"
objectTypeDefinition :: Parser ObjectTypeDefinition
objectTypeDefinition = ObjectTypeDefinition
<$ "type" <* s1
<*> name <* s1
<*> (interfaces <* s <|> empty)
<*> fieldDefinitions
<?> "objectTypeDefinition error!"
interfaces :: Parser Interfaces
interfaces = "implements" *> s1 *> many1_ namedType
fieldDefinitions :: Parser [FieldDefinition]
fieldDefinitions = "{" *> s *> many1_ fieldDefinition <* s <* "}"
fieldDefinition :: Parser FieldDefinition
fieldDefinition = FieldDefinition
<$> name <* s
<*> (argumentsDefinition <* s <|> empty)
<* ":" <* s
<*> type_
argumentsDefinition :: Parser ArgumentsDefinition
argumentsDefinition = inputValueDefinitions
inputValueDefinitions :: Parser [InputValueDefinition]
inputValueDefinitions = "(" *> s *> many1_ inputValueDefinition <* s <* ")"
inputValueDefinition :: Parser InputValueDefinition
inputValueDefinition = InputValueDefinition
<$> name <* s
<* ":" <* s
<*> type_ <* s
<*> (value <|> empty)
interfaceTypeDefinition :: Parser InterfaceTypeDefinition
interfaceTypeDefinition = InterfaceTypeDefinition
<$ "interface" <* s1
<*> name <* s
<*> fieldDefinitions
unionTypeDefinition :: Parser UnionTypeDefinition
unionTypeDefinition = UnionTypeDefinition
<$ "union" <* s1
<*> name <* s
<* "=" <* s
<*> unionMembers
where
-- This should take care of standalone `NamedType`
unionMembers = namedType `sepBy1` (s *> "|" <* s)
scalarTypeDefinition :: Parser ScalarTypeDefinition
scalarTypeDefinition = ScalarTypeDefinition <$ "scalar" <* s1 <*> name
enumTypeDefinition :: Parser EnumTypeDefinition
enumTypeDefinition = EnumTypeDefinition
<$ "enum" <* s1
<*> name <* s
<*> enumValueDefinitions
enumValueDefinitions :: Parser [EnumValueDefinition]
enumValueDefinitions = "{" *> s *> many1_ enumValueDefinition <* s <* "}"
enumValueDefinition :: Parser EnumValueDefinition
enumValueDefinition = EnumValueDefinition <$> name
inputObjectTypeDefinition :: Parser InputObjectTypeDefinition
inputObjectTypeDefinition = InputObjectTypeDefinition
<$ "input" <* s1
<*> name <* s
<*> inputValueDefinitions
typeExtensionDefinition :: Parser TypeExtensionDefinition
typeExtensionDefinition = TypeExtensionDefinition
<$ "extend" <* s1
<*> objectTypeDefinition
-- * Internal
many_ :: Parser a -> Parser [a]
many_ = flip sepBy space'
many1_ :: Parser a -> Parser [a]
many1_ = flip sepBy1 space'
space' :: Parser Char
space' = satisfy isSpace'
s :: Parser ()
s = comments <|> skipWhile isSpace'
s1 :: Parser ()
s1 = comments <|> space' *> s
isSpace' :: Char -> Bool
isSpace' c = isSpace c || ',' == c || isEndOfLine c
comments :: Parser ()
comments = skipMany comment
comment :: Parser ()
comment = () <$ "#" <* manyTill anyChar endOfLine