summaryrefslogtreecommitdiff
path: root/Data/GraphQL/Parser.hs
blob: 35f42d5311c73deb3466a2919a07827463f44cb8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}
-- | This module defines a parser for @GraphQL@ request documents.
module Data.GraphQL.Parser where

import Prelude hiding (takeWhile)

import Control.Applicative ((<|>), Alternative, empty, many, optional)
import Control.Monad (when)
import Data.Char (isDigit, isSpace)
import Data.Foldable (traverse_)
import Data.Monoid ((<>))
import Data.List.NonEmpty (NonEmpty((:|)))
import Data.Scientific (floatingOrInteger, scientific, toBoundedInteger)

import Data.Text (Text, append)
import Data.Attoparsec.Combinator (lookAhead)
import Data.Attoparsec.Text
  ( Parser
  , (<?>)
  , anyChar
  , endOfLine
  , inClass
  , many1
  , manyTill
  , option
  , peekChar
  , takeWhile
  , takeWhile1
  )
import qualified Data.Attoparsec.Text as Attoparsec (scientific)

import Data.GraphQL.AST

-- * Name

name :: Parser Name
name = tok $ append <$> takeWhile1 isA_z
                    <*> takeWhile ((||) <$> isDigit <*> isA_z)
  where
    -- `isAlpha` handles many more Unicode Chars
    isA_z =  inClass $ '_' : ['A'..'Z'] <> ['a'..'z']

-- * Document

document :: Parser Document
document = whiteSpace *> manyNE definition

definition :: Parser Definition
definition = DefinitionOperation <$> operationDefinition
         <|> DefinitionFragment  <$> fragmentDefinition
         <?> "definition error!"

operationDefinition :: Parser OperationDefinition
operationDefinition = OperationSelectionSet <$> selectionSet
                  <|> OperationDefinition   <$> operationType
                                            <*> optional name
                                            <*> opt variableDefinitions
                                            <*> opt directives
                                            <*> selectionSet
                  <?> "operationDefinition error"

operationType :: Parser OperationType
operationType = Query    <$ tok "query"
            <|> Mutation <$ tok "mutation"
            <?> "operationType error"

-- * SelectionSet

selectionSet :: Parser SelectionSet
selectionSet = braces $ manyNE selection

selectionSetOpt :: Parser SelectionSetOpt
selectionSetOpt = braces $ many1 selection

selection :: Parser Selection
selection = SelectionField          <$> field
        <|> SelectionFragmentSpread <$> fragmentSpread
        <|> SelectionInlineFragment <$> inlineFragment
        <?> "selection error!"

-- * Field

field :: Parser Field
field = Field <$> optional alias
              <*> name
              <*> opt arguments
              <*> opt directives
              <*> opt selectionSetOpt

alias :: Parser Alias
alias = name <* tok ":"

-- * Arguments

arguments :: Parser Arguments
arguments = parens $ many1 argument

argument :: Parser Argument
argument = Argument <$> name <* tok ":" <*> value

-- * Fragments

fragmentSpread :: Parser FragmentSpread
fragmentSpread = FragmentSpread <$  tok "..."
                                <*> fragmentName
                                <*> opt directives

inlineFragment :: Parser InlineFragment
inlineFragment = InlineFragment <$  tok "..."
                                <*> optional typeCondition
                                <*> opt directives
                                <*> selectionSet

fragmentDefinition :: Parser FragmentDefinition
fragmentDefinition = FragmentDefinition
                 <$  tok "fragment"
                 <*> name
                 <*> typeCondition
                 <*> opt directives
                 <*> selectionSet

fragmentName :: Parser FragmentName
fragmentName = but (tok "on") *> name

typeCondition :: Parser TypeCondition
typeCondition = tok "on" *> name

-- * Input Values

value :: Parser Value
value = ValueVariable <$> variable
    <|> tok floatOrInt32Value
    <|> ValueBoolean  <$> booleanValue
    <|> ValueNull     <$  tok "null"
    <|> ValueString   <$> stringValue
    <|> ValueEnum     <$> enumValue
    <|> ValueList     <$> listValue
    <|> ValueObject   <$> objectValue
    <?> "value error!"
  where
    booleanValue :: Parser Bool
    booleanValue = True  <$ tok "true"
               <|> False <$ tok "false"

    floatOrInt32Value :: Parser Value
    floatOrInt32Value =
      Attoparsec.scientific >>=
      either (pure . ValueFloat)
             (maybe (fail "Integer value is out of range.")
                    (pure . ValueInt)
                    . toBoundedInteger . (`scientific` 0))
             . floatingOrInteger

    -- TODO: Escape characters. Look at `jsstring_` in aeson package.
    stringValue :: Parser Text
    stringValue = quotes (takeWhile (/= '"'))

    enumValue :: Parser Name
    enumValue = but (tok "true") *> but (tok "false") *> but (tok "null") *> name

    listValue :: Parser [Value]
    listValue = brackets $ many1 value

    objectValue :: Parser [ObjectField]
    objectValue = braces $ many1 objectField

objectField :: Parser ObjectField
objectField = ObjectField <$> name <* tok ":" <*> value

-- * Variables

variableDefinitions :: Parser VariableDefinitions
variableDefinitions = parens $ many1 variableDefinition

variableDefinition :: Parser VariableDefinition
variableDefinition = VariableDefinition <$> variable
                                        <*  tok ":"
                                        <*> type_
                                        <*> optional defaultValue

variable :: Parser Variable
variable = tok "$" *> name

defaultValue :: Parser DefaultValue
defaultValue = tok "=" *> value

-- * Input Types

type_ :: Parser Type
type_ = TypeNamed   <$> name <* but "!"
    <|> TypeList    <$> brackets type_
    <|> TypeNonNull <$> nonNullType
    <?> "type_ error!"

nonNullType :: Parser NonNullType
nonNullType = NonNullTypeNamed <$> name <* tok "!"
          <|> NonNullTypeList  <$> brackets type_  <* tok "!"
          <?> "nonNullType error!"

-- * Directives

directives :: Parser Directives
directives = many1 directive

directive :: Parser Directive
directive = Directive
        <$  tok "@"
        <*> name
        <*> opt arguments

-- * Internal

tok :: Parser a -> Parser a
tok p = p <* whiteSpace

parens :: Parser a -> Parser a
parens = between "(" ")"

braces :: Parser a -> Parser a
braces = between "{" "}"

quotes :: Parser a -> Parser a
quotes = between "\"" "\""

brackets :: Parser a -> Parser a
brackets = between "[" "]"

between :: Parser Text -> Parser Text -> Parser a -> Parser a
between open close p = tok open *> p <* tok close

opt :: Monoid a => Parser a -> Parser a
opt = option mempty

-- Hack to reverse parser success
but :: Parser a -> Parser ()
but pn = False <$ lookAhead pn <|> pure True >>= \case
  False -> empty
  True  -> pure ()

manyNE :: Alternative f => f a -> f (NonEmpty a)
manyNE p = (:|) <$> p <*> many p

whiteSpace :: Parser ()
whiteSpace = peekChar >>= traverse_ (\c ->
    if isSpace c || c == ','
       then anyChar *> whiteSpace
       else when (c == '#') $ manyTill anyChar endOfLine *> whiteSpace)