aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ghci2
-rw-r--r--CHANGELOG.md9
-rw-r--r--Data/GraphQL/AST.hs (renamed from Data/GraphQL.hs)37
-rw-r--r--Data/GraphQL/Parser.hs322
-rw-r--r--README.md7
-rw-r--r--TODO20
-rw-r--r--graphql.cabal32
-rw-r--r--tests/data/kitchen-sink.graphql38
-rw-r--r--tests/data/kitchen-sink.graphql.golden1
-rw-r--r--tests/golden.hs21
10 files changed, 462 insertions, 27 deletions
diff --git a/.ghci b/.ghci
new file mode 100644
index 0000000..ce7490e
--- /dev/null
+++ b/.ghci
@@ -0,0 +1,2 @@
+import Data.Attoparsec.Text
+import qualified Data.Text.IO as TIO
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a5ca3ef..8f8d712 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,15 @@
# Change Log
All notable changes to this project will be documented in this file.
+## [0.2] - 2015-09-14
+### Added
+- Rudimentary parser for `GraphQL` which successfully parses the sample file
+ `kitchen-sink.graphql` from `graphql-js` tests.
+- Golden test for `kitchen-sink.grahql` parsing.
+### Changed
+- Many optional data types in `GraphQl` didn't need to be wrapped in a `Maybe`.
+- Some `newtype`s became type synonyms for easier parsing.
+
## [0.1] - 2015-09-12
### Added
- Data types for the GraphQL language.
diff --git a/Data/GraphQL.hs b/Data/GraphQL/AST.hs
index d878022..0a09671 100644
--- a/Data/GraphQL.hs
+++ b/Data/GraphQL/AST.hs
@@ -1,4 +1,4 @@
-module Data.GraphQL where
+module Data.GraphQL.AST where
import Data.Text (Text)
@@ -16,9 +16,10 @@ data Definition = DefinitionOperation OperationDefinition
deriving (Eq,Show)
data OperationDefinition =
- Query (Maybe [VariableDefinition]) (Maybe [Directive]) SelectionSet
- | Mutation (Maybe [VariableDefinition]) (Maybe [Directive]) SelectionSet
- | Subscription (Maybe [VariableDefinition]) (Maybe [Directive]) SelectionSet
+ Query Name [VariableDefinition] [Directive] SelectionSet
+ | Mutation Name [VariableDefinition] [Directive] SelectionSet
+ -- Not official yet
+ -- -- | Subscription Name [VariableDefinition] [Directive] SelectionSet
deriving (Eq,Show)
data VariableDefinition = VariableDefinition Variable Type (Maybe DefaultValue)
@@ -26,16 +27,16 @@ data VariableDefinition = VariableDefinition Variable Type (Maybe DefaultValue)
newtype Variable = Variable Name deriving (Eq,Show)
-newtype SelectionSet = SelectionSet [Selection] deriving (Eq,Show)
+type SelectionSet = [Selection]
data Selection = SelectionField Field
| SelectionFragmentSpread FragmentSpread
| SelectionInlineFragment InlineFragment
deriving (Eq,Show)
-data Field = Field (Maybe Alias) Name (Maybe [Argument])
- (Maybe [Directive])
- (Maybe SelectionSet)
+data Field = Field Alias Name [Argument]
+ [Directive]
+ SelectionSet
deriving (Eq,Show)
type Alias = Name
@@ -44,15 +45,15 @@ data Argument = Argument Name Value deriving (Eq,Show)
-- * Fragments
-data FragmentSpread = FragmentSpread Name (Maybe [Directive])
+data FragmentSpread = FragmentSpread Name [Directive]
deriving (Eq,Show)
data InlineFragment =
- InlineFragment TypeCondition (Maybe [Directive]) SelectionSet
+ InlineFragment TypeCondition [Directive] SelectionSet
deriving (Eq,Show)
data FragmentDefinition =
- FragmentDefinition Name TypeCondition (Maybe [Directive]) SelectionSet
+ FragmentDefinition Name TypeCondition [Directive] SelectionSet
deriving (Eq,Show)
type TypeCondition = NamedType
@@ -60,10 +61,10 @@ type TypeCondition = NamedType
-- * Values
data Value = ValueVariable Variable
- | ValueInt Int
- | ValueFloat Float
- | ValueString Text
+ | ValueInt Int -- TODO: Should this be `Integer`?
+ | ValueFloat Double -- TODO: Should this be `Scientific`?
| ValueBoolean Bool
+ | ValueString Text
| ValueEnum Name
| ValueList ListValue
| ValueObject ObjectValue
@@ -79,7 +80,7 @@ type DefaultValue = Value
-- * Directives
-data Directive = Directive Name (Maybe [Argument]) deriving (Eq,Show)
+data Directive = Directive Name [Argument] deriving (Eq,Show)
-- * Type Reference
@@ -107,14 +108,16 @@ data TypeDefinition = TypeDefinitionObject ObjectTypeDefinition
| TypeDefinitionTypeExtension TypeExtensionDefinition
deriving (Eq,Show)
-data ObjectTypeDefinition = ObjectTypeDefinition Name (Maybe Interfaces) [FieldDefinition]
+data ObjectTypeDefinition = ObjectTypeDefinition Name Interfaces [FieldDefinition]
deriving (Eq,Show)
type Interfaces = [NamedType]
-data FieldDefinition = FieldDefinition Name [InputValueDefinition]
+data FieldDefinition = FieldDefinition Name ArgumentsDefinition Type
deriving (Eq,Show)
+type ArgumentsDefinition = [InputValueDefinition]
+
data InputValueDefinition = InputValueDefinition Name Type (Maybe DefaultValue)
deriving (Eq,Show)
diff --git a/Data/GraphQL/Parser.hs b/Data/GraphQL/Parser.hs
new file mode 100644
index 0000000..66d913d
--- /dev/null
+++ b/Data/GraphQL/Parser.hs
@@ -0,0 +1,322 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE LambdaCase #-}
+module Data.GraphQL.Parser where
+
+import Prelude hiding (takeWhile)
+import Control.Applicative ((<|>), empty, many, optional)
+import Control.Monad (when)
+import Data.Char
+
+import Data.Text (Text, pack)
+import Data.Attoparsec.Text
+ ( Parser
+ , (<?>)
+ , anyChar
+ , decimal
+ , double
+ , endOfLine
+ , many1
+ , manyTill
+ , option
+ , peekChar
+ , satisfy
+ , sepBy1
+ , signed
+ )
+
+import Data.GraphQL.AST
+
+-- * Name
+
+-- XXX: Handle starting `_` and no number at the beginning:
+-- https://facebook.github.io/graphql/#sec-Names
+-- TODO: Use takeWhile1 instead for efficiency. With takeWhile1 there is no
+-- parsing failure.
+name :: Parser Name
+name = tok $ pack <$> many1 (satisfy isAlphaNum)
+
+-- * Document
+
+document :: Parser Document
+document = whiteSpace
+ *> (Document <$> many1 definition)
+ -- Try SelectionSet when no definition
+ <|> (Document . pure
+ . DefinitionOperation
+ . Query mempty empty empty
+ <$> selectionSet)
+ <?> "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 <$ tok n <*> tok name
+ <*> optempty variableDefinitions
+ <*> optempty directives
+ <*> selectionSet
+
+variableDefinitions :: Parser [VariableDefinition]
+variableDefinitions = parens (many1 variableDefinition)
+
+variableDefinition :: Parser VariableDefinition
+variableDefinition =
+ VariableDefinition <$> variable
+ <* tok ":"
+ <*> type_
+ <*> optional defaultValue
+
+defaultValue :: Parser DefaultValue
+defaultValue = tok "=" *> value
+
+variable :: Parser Variable
+variable = Variable <$ tok "$" <*> name
+
+selectionSet :: Parser SelectionSet
+selectionSet = braces $ many1 selection
+
+selection :: Parser Selection
+selection = SelectionField <$> field
+ -- Inline first to catch `on` case
+ <|> SelectionInlineFragment <$> inlineFragment
+ <|> SelectionFragmentSpread <$> fragmentSpread
+ <?> "selection error!"
+
+field :: Parser Field
+field = Field <$> optempty alias
+ <*> name
+ <*> optempty arguments
+ <*> optempty directives
+ <*> optempty selectionSet
+
+alias :: Parser Alias
+alias = name <* tok ":"
+
+arguments :: Parser [Argument]
+arguments = parens $ many1 argument
+
+argument :: Parser Argument
+argument = Argument <$> name <* tok ":" <*> value
+
+-- * Fragments
+
+fragmentSpread :: Parser FragmentSpread
+-- TODO: Make sure it fails when `... on`.
+-- See https://facebook.github.io/graphql/#FragmentSpread
+fragmentSpread = FragmentSpread
+ <$ tok "..."
+ <*> name
+ <*> optempty directives
+
+-- InlineFragment tried first in order to guard against 'on' keyword
+inlineFragment :: Parser InlineFragment
+inlineFragment = InlineFragment
+ <$ tok "..."
+ <* tok "on"
+ <*> typeCondition
+ <*> optempty directives
+ <*> selectionSet
+
+fragmentDefinition :: Parser FragmentDefinition
+fragmentDefinition = FragmentDefinition
+ <$ tok "fragment"
+ <*> name
+ <* tok "on"
+ <*> typeCondition
+ <*> optempty directives
+ <*> 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 = ValueVariable <$> variable
+ -- TODO: Handle arbitrary precision.
+ <|> ValueInt <$> tok (signed decimal)
+ <|> ValueFloat <$> tok (signed double)
+ <|> ValueBoolean <$> bool
+ -- TODO: Handle escape characters, unicode, etc
+ <|> ValueString <$> quotes name
+ -- `true` and `false` have been tried before
+ <|> ValueEnum <$> name
+ <|> ValueList <$> listValue
+ <|> ValueObject <$> objectValue
+
+-- Notice it can be empty
+listValue :: Parser ListValue
+listValue = ListValue <$> brackets (many value)
+
+-- Notice it can be empty
+objectValue :: Parser ObjectValue
+objectValue = ObjectValue <$> braces (many objectField)
+
+objectField :: Parser ObjectField
+objectField = ObjectField <$> name <* tok ":" <*> value
+
+bool :: Parser Bool
+bool = True <$ tok "true"
+ <|> False <$ tok "false"
+
+-- * Directives
+
+directives :: Parser [Directive]
+directives = many1 directive
+
+directive :: Parser Directive
+directive = Directive
+ <$ tok "@"
+ <*> name
+ <*> optempty arguments
+
+-- * Type Reference
+
+type_ :: Parser Type
+type_ = TypeNamed <$> namedType
+ <|> TypeList <$> listType
+ <|> TypeNonNull <$> nonNullType
+
+namedType :: Parser NamedType
+namedType = NamedType <$> name
+
+listType :: Parser ListType
+listType = ListType <$> brackets type_
+
+nonNullType :: Parser NonNullType
+nonNullType = NonNullTypeNamed <$> namedType <* tok "!"
+ <|> NonNullTypeList <$> listType <* tok "!"
+
+-- * Type Definition
+
+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
+ <$ tok "type"
+ <*> name
+ <*> optempty interfaces
+ <*> fieldDefinitions
+ <?> "objectTypeDefinition error!"
+
+interfaces :: Parser Interfaces
+interfaces = tok "implements" *> many1 namedType
+
+fieldDefinitions :: Parser [FieldDefinition]
+fieldDefinitions = braces $ many1 fieldDefinition
+
+fieldDefinition :: Parser FieldDefinition
+fieldDefinition = FieldDefinition
+ <$> name
+ <*> optempty argumentsDefinition
+ <* tok ":"
+ <*> type_
+
+argumentsDefinition :: Parser ArgumentsDefinition
+argumentsDefinition = inputValueDefinitions
+
+inputValueDefinitions :: Parser [InputValueDefinition]
+inputValueDefinitions = parens $ many1 inputValueDefinition
+
+inputValueDefinition :: Parser InputValueDefinition
+inputValueDefinition = InputValueDefinition
+ <$> name
+ <* tok ":"
+ <*> type_
+ <*> optional defaultValue
+
+interfaceTypeDefinition :: Parser InterfaceTypeDefinition
+interfaceTypeDefinition = InterfaceTypeDefinition
+ <$ tok "interface"
+ <*> name
+ <*> fieldDefinitions
+
+unionTypeDefinition :: Parser UnionTypeDefinition
+unionTypeDefinition = UnionTypeDefinition
+ <$ tok "union"
+ <*> name
+ <* tok "="
+ <*> unionMembers
+
+unionMembers :: Parser [NamedType]
+unionMembers = namedType `sepBy1` tok "|"
+
+scalarTypeDefinition :: Parser ScalarTypeDefinition
+scalarTypeDefinition = ScalarTypeDefinition
+ <$ tok "scalar"
+ <*> name
+
+enumTypeDefinition :: Parser EnumTypeDefinition
+enumTypeDefinition = EnumTypeDefinition
+ <$ tok "enum"
+ <*> name
+ <*> enumValueDefinitions
+
+enumValueDefinitions :: Parser [EnumValueDefinition]
+enumValueDefinitions = braces $ many1 enumValueDefinition
+
+enumValueDefinition :: Parser EnumValueDefinition
+enumValueDefinition = EnumValueDefinition <$> name
+
+inputObjectTypeDefinition :: Parser InputObjectTypeDefinition
+inputObjectTypeDefinition = InputObjectTypeDefinition
+ <$ tok "input"
+ <*> name
+ <*> inputValueDefinitions
+
+typeExtensionDefinition :: Parser TypeExtensionDefinition
+typeExtensionDefinition = TypeExtensionDefinition
+ <$ tok "extend"
+ <*> objectTypeDefinition
+
+-- * 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
+
+-- `empty` /= `pure mempty` for `Parser`.
+optempty :: Monoid a => Parser a -> Parser a
+optempty = option mempty
+
+-- ** WhiteSpace
+--
+whiteSpace :: Parser ()
+whiteSpace = peekChar >>= \case
+ Just c -> if isSpace c || c == ','
+ then anyChar *> whiteSpace
+ else when (c == '#') $ manyTill anyChar endOfLine *> whiteSpace
+ _ -> return ()
diff --git a/README.md b/README.md
index 96408a4..bf5319f 100644
--- a/README.md
+++ b/README.md
@@ -7,12 +7,15 @@ but the idea is to be a Haskell port of
[`graphql-js`](https://github.com/graphql/graphql-js). Next releases
should include:
-- [ ] Parser for the GraphQL language.
-- [ ] Data types for the GraphQL Schema language.
+- [x] GraphQL AST
+- [x] Parser for the GraphQL language. See TODO for caveats.
+- [ ] GraphQL Schema AST.
- [ ] Parser for the GraphQL Schema language.
- [ ] Interpreter of GraphQL requests.
- [ ] Utilities to define GraphQL types and schema.
+See the TODO file for more concrete tasks.
+
## Contact
Suggestions, contributions and bug reports are welcome.
diff --git a/TODO b/TODO
index 6002a0a..9502263 100644
--- a/TODO
+++ b/TODO
@@ -1,3 +1,21 @@
+## AST
+- Simplify unnecessary `newtypes` with type synonyms
- Data type accessors
-- Deal with Location
- Deal with Strictness/unboxing
+- Deal with Location
+
+## Parser
+- Secure Names
+- Optimize `name` and `whiteSpace`: `take...`, `T.fold`, ...
+- Handle escape characters in string literals
+- Guard for `on` in `FragmentSpread`
+- Tests!
+- Handle `[Const]` grammar parameter. Need examples
+- Arbitrary precision for number values?
+- Handle errors. Perhaps port to `parsers` or use a lexer and
+ `regex-applicative`
+
+## Tests
+
+- Golden data within package, `path_graphql` macro.
+- Pretty Print golden result
diff --git a/graphql.cabal b/graphql.cabal
index f653731..2f83aeb 100644
--- a/graphql.cabal
+++ b/graphql.cabal
@@ -1,9 +1,9 @@
name: graphql
-version: 0.1
-synopsis: GraphQL Haskell implementation
+version: 0.2
+synopsis: Haskell GraphQL implementation
description:
- For now this package provides the data types for the GraphQL language.
- Further releases will cover more aspects of the GraphQL specification.
+ This package provides a rudimentary parser for the
+ <https://facebook.github.io/graphql/ GraphQL> language.
homepage: https://github.com/jdnavarro/graphql-haskell
bug-reports: https://github.com/jdnavarro/graphql-haskell/issues
license: BSD3
@@ -13,14 +13,32 @@ maintainer: j@dannynavarro.net
copyright: Copyright (C) 2015 J. Daniel Navarro
category: Web
build-type: Simple
-extra-source-files: README.md CHANGELOG.md stack.yaml
cabal-version: >=1.10
+tested-with: GHC == 7.10
+extra-source-files: README.md CHANGELOG.md stack.yaml
library
- exposed-modules: Data.GraphQL
+ default-language: Haskell2010
+ ghc-options: -Wall
+ exposed-modules: Data.GraphQL.AST
+ Data.GraphQL.Parser
build-depends: base >= 4.7 && < 5,
- text >=0.11.3.1
+ text >=0.11.3.1,
+ attoparsec >=0.10.4.0
+
+test-suite golden
default-language: Haskell2010
+ type: exitcode-stdio-1.0
+ hs-source-dirs: tests
+ main-is: golden.hs
+ ghc-options: -Wall
+ build-depends: base >= 4.6 && <5,
+ bytestring,
+ text,
+ attoparsec,
+ tasty >=0.10,
+ tasty-golden,
+ graphql
source-repository head
type: git
diff --git a/tests/data/kitchen-sink.graphql b/tests/data/kitchen-sink.graphql
new file mode 100644
index 0000000..46fd10e
--- /dev/null
+++ b/tests/data/kitchen-sink.graphql
@@ -0,0 +1,38 @@
+# Copyright (c) 2015, Facebook, Inc.
+# All rights reserved.
+#
+# This source code is licensed under the BSD-style license found in the
+# LICENSE file in the root directory of this source tree. An additional grant
+# of patent rights can be found in the PATENTS file in the same directory.
+
+query queryName($foo: ComplexType, $site: Site = MOBILE) {
+ whoever123is: node(id: [123, 456]) {
+ id , # Inline test comment
+ ... on User @defer {
+ field2 {
+ id ,
+ alias: field1(first:10, after:$foo,) @include(if: $foo) {
+ id,
+ ...frag
+ }
+ }
+ }
+ }
+}
+
+mutation likeStory {
+ like(story: 123) @defer {
+ story {
+ id
+ }
+ }
+}
+
+fragment frag on Friend {
+ foo(size: $size, bar: $b, obj: {key: "value"})
+}
+
+{
+ unnamed(truthy: true, falsey: false),
+ query
+}
diff --git a/tests/data/kitchen-sink.graphql.golden b/tests/data/kitchen-sink.graphql.golden
new file mode 100644
index 0000000..2542cd5
--- /dev/null
+++ b/tests/data/kitchen-sink.graphql.golden
@@ -0,0 +1 @@
+Document [DefinitionOperation (Query "queryName" [VariableDefinition (Variable "foo") (TypeNamed (NamedType "ComplexType")) Nothing,VariableDefinition (Variable "site") (TypeNamed (NamedType "Site")) (Just (ValueEnum "MOBILE"))] [] [SelectionField (Field "whoever123is" "node" [Argument "id" (ValueList (ListValue [ValueInt 123,ValueInt 456]))] [] [SelectionField (Field "" "id" [] [] []),SelectionInlineFragment (InlineFragment (NamedType "User") [Directive "defer" []] [SelectionField (Field "" "field2" [] [] [SelectionField (Field "" "id" [] [] []),SelectionField (Field "alias" "field1" [Argument "first" (ValueInt 10),Argument "after" (ValueVariable (Variable "foo"))] [Directive "include" [Argument "if" (ValueVariable (Variable "foo"))]] [SelectionField (Field "" "id" [] [] []),SelectionFragmentSpread (FragmentSpread "frag" [])])])])])]),DefinitionOperation (Mutation "likeStory" [] [] [SelectionField (Field "" "like" [Argument "story" (ValueInt 123)] [Directive "defer" []] [SelectionField (Field "" "story" [] [] [SelectionField (Field "" "id" [] [] [])])])]),DefinitionFragment (FragmentDefinition "frag" (NamedType "Friend") [] [SelectionField (Field "" "foo" [Argument "size" (ValueVariable (Variable "size")),Argument "bar" (ValueVariable (Variable "b")),Argument "obj" (ValueObject (ObjectValue [ObjectField "key" (ValueString "value")]))] [] [])])]
diff --git a/tests/golden.hs b/tests/golden.hs
new file mode 100644
index 0000000..e7ab70f
--- /dev/null
+++ b/tests/golden.hs
@@ -0,0 +1,21 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import Control.Monad ((>=>))
+import Data.Attoparsec.Text (parseOnly)
+import Data.ByteString.Lazy.Char8 as B8
+import qualified Data.Text.IO as TIO
+import Test.Tasty (defaultMain)
+import Test.Tasty.Golden (goldenVsString)
+
+import Data.GraphQL.Parser (document)
+
+main :: IO ()
+main = defaultMain
+ $ goldenVsString "kitchen-sink.graphql"
+ "./tests/data/kitchen-sink.graphql.golden"
+ (parse "./tests/data/kitchen-sink.graphql")
+ where
+ parse = fmap (parseOnly document) . TIO.readFile
+ >=> pure . either B8.pack (flip B8.snoc '\n' . B8.pack . show)
+