forked from OSS/graphql
Merge branch 'all-improvements'
This adds general API documentation, a tutorial and error handling.
This commit is contained in:
commit
77853b17ae
@ -1,6 +1,7 @@
|
||||
-- | This module provides the functions to parse and execute @GraphQL@ queries.
|
||||
module Data.GraphQL where
|
||||
|
||||
import Control.Applicative (Alternative, empty)
|
||||
import Control.Applicative (Alternative)
|
||||
|
||||
import Data.Text (Text)
|
||||
|
||||
@ -11,10 +12,23 @@ import Data.GraphQL.Execute
|
||||
import Data.GraphQL.Parser
|
||||
import Data.GraphQL.Schema
|
||||
|
||||
import Data.GraphQL.Error
|
||||
|
||||
-- | Takes a 'Schema' and text representing a @GraphQL@ request document.
|
||||
-- If the text parses correctly as a @GraphQL@ query the query is
|
||||
-- executed according to the given 'Schema'.
|
||||
--
|
||||
-- Returns the response as an @Aeson.@'Aeson.Value'.
|
||||
graphql :: (Alternative m, Monad m) => Schema m -> Text -> m Aeson.Value
|
||||
graphql = flip graphqlSubs $ const Nothing
|
||||
|
||||
-- | Takes a 'Schema', a variable substitution function and text
|
||||
-- representing a @GraphQL@ request document. If the text parses
|
||||
-- correctly as a @GraphQL@ query the substitution is applied to the
|
||||
-- query and the query is then executed according to the given 'Schema'.
|
||||
--
|
||||
-- Returns the response as an @Aeson.@'Aeson.Value'.
|
||||
graphqlSubs :: (Alternative m, Monad m) => Schema m -> Subs -> Text -> m Aeson.Value
|
||||
graphqlSubs schema f =
|
||||
either (const empty) (execute schema f)
|
||||
either parseError (execute schema f)
|
||||
. Attoparsec.parseOnly document
|
||||
|
@ -1,3 +1,6 @@
|
||||
-- | This module defines an abstract syntax tree for the @GraphQL@ language based on
|
||||
-- <https://facebook.github.io/graphql/ Facebook's GraphQL Specification>.
|
||||
|
||||
module Data.GraphQL.AST where
|
||||
|
||||
import Data.Int (Int32)
|
||||
@ -39,11 +42,26 @@ data Selection = SelectionField Field
|
||||
| SelectionInlineFragment InlineFragment
|
||||
deriving (Eq,Show)
|
||||
|
||||
-- | A 'SelectionSet' is primarily composed of 'Field's. A 'Field' describes one
|
||||
-- discrete piece of information available to request within a 'SelectionSet'.
|
||||
--
|
||||
-- Some 'Field's describe complex data or relationships to other data. In
|
||||
-- order to further explore this data, a 'Field' may itself contain a
|
||||
-- 'SelectionSet', allowing for deeply nested requests. All @GraphQL@ operations
|
||||
-- must specify their 'Selection's down to 'Field's which return scalar values to
|
||||
-- ensure an unambiguously shaped response.
|
||||
--
|
||||
-- <https://facebook.github.io/graphql/#sec-Language.Query-Document.Fields Field Specification>
|
||||
data Field = Field Alias Name [Argument] [Directive] SelectionSet
|
||||
deriving (Eq,Show)
|
||||
|
||||
type Alias = Name
|
||||
|
||||
-- | 'Field's are conceptually functions which return values, and occasionally accept
|
||||
-- 'Argument's which alter their behavior. These 'Argument's often map directly to
|
||||
-- function arguments within a @GraphQL@ server’s implementation.
|
||||
--
|
||||
-- <https://facebook.github.io/graphql/#sec-Language.Query-Document.Arguments Argument Specification>
|
||||
data Argument = Argument Name Value deriving (Eq,Show)
|
||||
|
||||
-- * Fragments
|
||||
@ -63,6 +81,15 @@ type TypeCondition = NamedType
|
||||
|
||||
-- * Values
|
||||
|
||||
-- | 'Field' and 'Directive' 'Arguments' accept input values of various literal
|
||||
-- primitives; input values can be scalars, enumeration values, lists, or input
|
||||
-- objects.
|
||||
--
|
||||
-- If not defined as constant (for example, in 'DefaultValue'), input values
|
||||
-- can be specified as a 'Variable'. List and inputs objects may also contain
|
||||
-- 'Variable's (unless defined to be constant).
|
||||
--
|
||||
-- <https://facebook.github.io/graphql/#sec-Input-Values Input Value Specification>
|
||||
data Value = ValueVariable Variable
|
||||
| ValueInt Int32
|
||||
-- GraphQL Float is double precison
|
||||
|
@ -1,5 +1,6 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
-- | This module defines a printer for the @GraphQL@ language.
|
||||
module Data.GraphQL.Encoder where
|
||||
|
||||
#if !MIN_VERSION_base(4,8,0)
|
||||
|
63
Data/GraphQL/Error.hs
Normal file
63
Data/GraphQL/Error.hs
Normal file
@ -0,0 +1,63 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
module Data.GraphQL.Error (
|
||||
parseError,
|
||||
CollectErrsT,
|
||||
addErr,
|
||||
addErrMsg,
|
||||
runCollectErrs,
|
||||
joinErrs,
|
||||
errWrap
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as Aeson
|
||||
import Data.Text (Text, pack)
|
||||
|
||||
import Control.Arrow ((&&&))
|
||||
|
||||
#if !MIN_VERSION_base(4,8,0)
|
||||
import Control.Applicative (Applicative, pure)
|
||||
import Data.Foldable (Foldable, concatMap)
|
||||
import Prelude hiding (concatMap)
|
||||
#endif
|
||||
|
||||
-- | Wraps a parse error into a list of errors.
|
||||
parseError :: Applicative f => String -> f Aeson.Value
|
||||
parseError s =
|
||||
pure $ Aeson.object [("errors", Aeson.toJSON [makeErrorMsg $ pack s])]
|
||||
|
||||
-- | A wrapper for an 'Applicative' to pass error messages around.
|
||||
type CollectErrsT f a = f (a,[Aeson.Value])
|
||||
|
||||
-- | Takes a (wrapped) list (foldable functor) of values and errors,
|
||||
-- joins the values into a list and concatenates the errors.
|
||||
joinErrs
|
||||
:: (Functor m, Functor f, Foldable f)
|
||||
=> m (f (a,[Aeson.Value])) -> CollectErrsT m (f a)
|
||||
joinErrs = fmap $ fmap fst &&& concatMap snd
|
||||
|
||||
-- | Wraps the given 'Applicative' to handle errors
|
||||
errWrap :: Functor f => f a -> f (a, [Aeson.Value])
|
||||
errWrap = fmap (flip (,) [])
|
||||
|
||||
-- | Adds an error to the list of errors.
|
||||
addErr :: Functor f => Aeson.Value -> CollectErrsT f a -> CollectErrsT f a
|
||||
addErr v = (fmap . fmap) (v :)
|
||||
|
||||
makeErrorMsg :: Text -> Aeson.Value
|
||||
makeErrorMsg s = Aeson.object [("message",Aeson.toJSON s)]
|
||||
|
||||
-- | Convenience function for just wrapping an error message.
|
||||
addErrMsg :: Functor f => Text -> CollectErrsT f a -> CollectErrsT f a
|
||||
addErrMsg = addErr . makeErrorMsg
|
||||
|
||||
-- | Runs the given query, but collects the errors into an error
|
||||
-- list which is then sent back with the data.
|
||||
runCollectErrs :: Functor f => CollectErrsT f Aeson.Value -> f Aeson.Value
|
||||
runCollectErrs = fmap finalD
|
||||
where
|
||||
finalD (dat,errs) =
|
||||
Aeson.object
|
||||
$ if null errs
|
||||
then [("data",dat)]
|
||||
else [("data",dat),("errors",Aeson.toJSON $ reverse errs)]
|
@ -1,4 +1,6 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
-- | This module provides the function to execute a @GraphQL@ request --
|
||||
-- according to a 'Schema'.
|
||||
module Data.GraphQL.Execute (execute) where
|
||||
|
||||
#if !MIN_VERSION_base(4,8,0)
|
||||
@ -13,16 +15,32 @@ import Data.GraphQL.AST
|
||||
import Data.GraphQL.Schema (Schema(..))
|
||||
import qualified Data.GraphQL.Schema as Schema
|
||||
|
||||
execute
|
||||
:: Alternative f
|
||||
=> Schema.Schema f -> Schema.Subs -> Document -> f Aeson.Value
|
||||
execute (Schema resolvs) subs = Schema.resolvers resolvs . rootFields subs
|
||||
import Data.GraphQL.Error
|
||||
|
||||
-- | Takes a 'Schema', a variable substitution function ('Schema.Subs'), and a
|
||||
-- @GraphQL@ 'document'. The substitution is applied to the document using
|
||||
-- 'rootFields', and the 'Schema''s resolvers are applied to the resulting fields.
|
||||
--
|
||||
-- Returns the result of the query against the 'Schema' wrapped in a /data/ field, or
|
||||
-- errors wrapped in an /errors/ field.
|
||||
execute :: Alternative f
|
||||
=> Schema.Schema f -> Schema.Subs -> Document -> f Aeson.Value
|
||||
execute (Schema resolvs) subs doc = runCollectErrs res
|
||||
where res = Schema.resolvers resolvs $ rootFields subs doc
|
||||
|
||||
-- | Takes a variable substitution function and a @GraphQL@ document.
|
||||
-- If the document contains one query (and no other definitions)
|
||||
-- it applies the substitution to the query's set of selections
|
||||
-- and then returns their fields.
|
||||
rootFields :: Schema.Subs -> Document -> [Field]
|
||||
rootFields subs (Document [DefinitionOperation (Query (Node _varDefs _ _ sels))]) =
|
||||
Schema.fields $ substitute subs <$> sels
|
||||
rootFields _ _ = []
|
||||
|
||||
-- | Takes a variable substitution function and a selection. If the
|
||||
-- selection is a field it applies the substitution to the field's
|
||||
-- arguments using 'subsArg', and recursively applies the substitution to
|
||||
-- the arguments of fields nested in the primary field.
|
||||
substitute :: Schema.Subs -> Selection -> Selection
|
||||
substitute subs (SelectionField (Field alias name args directives sels)) =
|
||||
SelectionField $ Field
|
||||
@ -35,6 +53,9 @@ substitute subs (SelectionField (Field alias name args directives sels)) =
|
||||
substitute _ sel = sel
|
||||
|
||||
-- TODO: Support different value types
|
||||
-- | Takes a variable substitution function and an argument. If the
|
||||
-- argument's value is a variable the substitution is applied to the
|
||||
-- variable's name.
|
||||
subsArg :: Schema.Subs -> Argument -> Maybe Argument
|
||||
subsArg subs (Argument n (ValueVariable (Variable v))) =
|
||||
Argument n . ValueString <$> subs v
|
||||
|
@ -1,5 +1,6 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
-- | This module defines a parser for @GraphQL@ request documents.
|
||||
module Data.GraphQL.Parser where
|
||||
|
||||
import Prelude hiding (takeWhile)
|
||||
|
@ -1,5 +1,8 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
-- | This module provides a representation of a @GraphQL@ Schema in addition to
|
||||
-- functions for defining and manipulating Schemas.
|
||||
module Data.GraphQL.Schema
|
||||
( Schema(..)
|
||||
, Resolver
|
||||
@ -21,14 +24,16 @@ module Data.GraphQL.Schema
|
||||
) where
|
||||
|
||||
#if !MIN_VERSION_base(4,8,0)
|
||||
import Control.Applicative (pure, (<|>))
|
||||
import Control.Applicative (pure)
|
||||
import Control.Arrow (first)
|
||||
import Data.Foldable (foldMap)
|
||||
import Data.Traversable (traverse)
|
||||
import Data.Monoid (Monoid(mempty,mappend))
|
||||
#else
|
||||
import Data.Bifunctor (first)
|
||||
import Data.Monoid (Alt(Alt,getAlt))
|
||||
#endif
|
||||
import Control.Applicative (Alternative, empty)
|
||||
import Control.Applicative (Alternative((<|>), empty))
|
||||
import Data.Maybe (catMaybes)
|
||||
import Data.Foldable (fold)
|
||||
|
||||
@ -36,77 +41,105 @@ import qualified Data.Aeson as Aeson
|
||||
import Data.HashMap.Strict (HashMap)
|
||||
import qualified Data.HashMap.Strict as HashMap
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T (null)
|
||||
import qualified Data.Text as T (null, unwords)
|
||||
|
||||
import Data.GraphQL.AST
|
||||
import Data.GraphQL.Error
|
||||
|
||||
-- | A GraphQL schema.
|
||||
-- @f@ is usually expected to be an instance of 'Alternative'.
|
||||
data Schema f = Schema [Resolver f]
|
||||
|
||||
type Resolver f = Field -> f Aeson.Object
|
||||
-- | Resolves a 'Field' into an @Aeson.@'Aeson.Object' with error information
|
||||
-- (or 'empty'). @f@ is usually expected to be an instance of 'Alternative'.
|
||||
type Resolver f = Field -> CollectErrsT f Aeson.Object
|
||||
|
||||
-- | Variable substitution function.
|
||||
type Subs = Text -> Maybe Text
|
||||
|
||||
-- | Create a named 'Resolver' from a list of 'Resolver's.
|
||||
object :: Alternative f => Text -> [Resolver f] -> Resolver f
|
||||
object name resolvs = objectA name $ \case
|
||||
[] -> resolvs
|
||||
_ -> empty
|
||||
|
||||
-- | Like 'object' but also taking 'Argument's.
|
||||
objectA
|
||||
:: Alternative f
|
||||
=> Text -> ([Argument] -> [Resolver f]) -> Resolver f
|
||||
objectA name f fld@(Field _ _ args _ sels) =
|
||||
withField name (resolvers (f args) $ fields sels) fld
|
||||
|
||||
-- | A scalar represents a primitive value, like a string or an integer.
|
||||
scalar :: (Alternative f, Aeson.ToJSON a) => Text -> a -> Resolver f
|
||||
scalar name s = scalarA name $ \case
|
||||
[] -> pure s
|
||||
_ -> empty
|
||||
|
||||
-- | Like 'scalar' but also taking 'Argument's.
|
||||
scalarA
|
||||
:: (Alternative f, Aeson.ToJSON a)
|
||||
=> Text -> ([Argument] -> f a) -> Resolver f
|
||||
scalarA name f fld@(Field _ _ args _ []) = withField name (f args) fld
|
||||
scalarA name f fld@(Field _ _ args _ []) = withField name (errWrap $ f args) fld
|
||||
scalarA _ _ _ = empty
|
||||
|
||||
-- | Like 'object' but taking lists of 'Resolver's instead of a single list.
|
||||
array :: Alternative f => Text -> [[Resolver f]] -> Resolver f
|
||||
array name resolvs = arrayA name $ \case
|
||||
[] -> resolvs
|
||||
_ -> empty
|
||||
|
||||
-- | Like 'array' but also taking 'Argument's.
|
||||
arrayA
|
||||
:: Alternative f
|
||||
=> Text -> ([Argument] -> [[Resolver f]]) -> Resolver f
|
||||
arrayA name f fld@(Field _ _ args _ sels) =
|
||||
withField name (traverse (flip resolvers $ fields sels) $ f args) fld
|
||||
withField name (joinErrs $ traverse (flip resolvers $ fields sels) $ f args) fld
|
||||
|
||||
-- | Represents one of a finite set of possible values.
|
||||
-- Used in place of a 'scalar' when the possible responses are easily enumerable.
|
||||
enum :: Alternative f => Text -> f [Text] -> Resolver f
|
||||
enum name enums = enumA name $ \case
|
||||
[] -> enums
|
||||
_ -> empty
|
||||
|
||||
-- | Like 'enum' but also taking 'Argument's.
|
||||
enumA :: Alternative f => Text -> ([Argument] -> f [Text]) -> Resolver f
|
||||
enumA name f fld@(Field _ _ args _ []) = withField name (f args) fld
|
||||
enumA name f fld@(Field _ _ args _ []) = withField name (errWrap $ f args) fld
|
||||
enumA _ _ _ = empty
|
||||
|
||||
-- | Helper function to facilitate 'Argument' handling.
|
||||
withField
|
||||
:: (Alternative f, Aeson.ToJSON a)
|
||||
=> Text -> f a -> Field -> f (HashMap Text Aeson.Value)
|
||||
=> Text -> CollectErrsT f a -> Field -> CollectErrsT f (HashMap Text Aeson.Value)
|
||||
withField name f (Field alias name' _ _ _) =
|
||||
if name == name'
|
||||
then fmap (HashMap.singleton aliasOrName . Aeson.toJSON) f
|
||||
then fmap (first $ HashMap.singleton aliasOrName . Aeson.toJSON) f
|
||||
else empty
|
||||
where
|
||||
aliasOrName = if T.null alias then name' else alias
|
||||
|
||||
resolvers :: Alternative f => [Resolver f] -> [Field] -> f Aeson.Value
|
||||
-- | Takes a list of 'Resolver's and a list of 'Field's and applies each
|
||||
-- 'Resolver' to each 'Field'. Resolves into a value containing the
|
||||
-- resolved 'Field', or a null value and error information.
|
||||
resolvers :: Alternative f => [Resolver f] -> [Field] -> CollectErrsT f Aeson.Value
|
||||
resolvers resolvs =
|
||||
fmap (Aeson.toJSON . fold)
|
||||
. traverse (\fld -> getAlt $ foldMap (Alt . ($ fld)) resolvs)
|
||||
fmap (first Aeson.toJSON . fold)
|
||||
. traverse (\fld -> getAlt (foldMap (Alt . ($ fld)) resolvs) <|> errmsg fld)
|
||||
where
|
||||
errmsg (Field alias name _ _ _) = addErrMsg msg $ (errWrap . pure) val
|
||||
where
|
||||
val = HashMap.singleton aliasOrName Aeson.Null
|
||||
msg = T.unwords ["field", name, "not resolved."]
|
||||
aliasOrName = if T.null alias then name else alias
|
||||
|
||||
-- | Checks whether the given 'Selection' contains a 'Field' and
|
||||
-- returns the 'Field' if so, else returns 'Nothing'.
|
||||
field :: Selection -> Maybe Field
|
||||
field (SelectionField x) = Just x
|
||||
field _ = Nothing
|
||||
|
||||
-- | Returns a list of the 'Field's contained in the given 'SelectionSet'.
|
||||
fields :: SelectionSet -> [Field]
|
||||
fields = catMaybes . fmap field
|
||||
|
||||
|
4
docs/tutorial/Makefile
Normal file
4
docs/tutorial/Makefile
Normal file
@ -0,0 +1,4 @@
|
||||
default:
|
||||
pandoc -f markdown+lhs+yaml_metadata_block --highlight-style=haddock -S -c "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" --section-divs -c tutorial.css --toc --standalone -t html5 -o tutorial.html tutorial.lhs
|
||||
pandoc -f markdown+lhs+yaml_metadata_block --highlight-style=haddock --toc --standalone -t rst -o tutorial.rst tutorial.lhs
|
||||
pandoc -f markdown+lhs+yaml_metadata_block --highlight-style=haddock --toc --standalone -t latex -o tutorial.pdf tutorial.lhs
|
3
docs/tutorial/tutorial.css
Normal file
3
docs/tutorial/tutorial.css
Normal file
@ -0,0 +1,3 @@
|
||||
body {
|
||||
padding: 0 20px;
|
||||
}
|
165
docs/tutorial/tutorial.html
Normal file
165
docs/tutorial/tutorial.html
Normal file
@ -0,0 +1,165 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="generator" content="pandoc">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
|
||||
<title>GraphQL Haskell Tutorial</title>
|
||||
<style type="text/css">code{white-space: pre;}</style>
|
||||
<style type="text/css">
|
||||
div.sourceCode { overflow-x: auto; }
|
||||
table.sourceCode, tr.sourceCode, td.lineNumbers, td.sourceCode {
|
||||
margin: 0; padding: 0; vertical-align: baseline; border: none; }
|
||||
table.sourceCode { width: 100%; line-height: 100%; }
|
||||
td.lineNumbers { text-align: right; padding-right: 4px; padding-left: 4px; color: #aaaaaa; border-right: 1px solid #aaaaaa; }
|
||||
td.sourceCode { padding-left: 5px; }
|
||||
code > span.kw { color: #0000ff; } /* Keyword */
|
||||
code > span.ch { color: #008080; } /* Char */
|
||||
code > span.st { color: #008080; } /* String */
|
||||
code > span.co { color: #008000; } /* Comment */
|
||||
code > span.ot { color: #ff4000; } /* Other */
|
||||
code > span.al { color: #ff0000; } /* Alert */
|
||||
code > span.er { color: #ff0000; font-weight: bold; } /* Error */
|
||||
code > span.wa { color: #008000; font-weight: bold; } /* Warning */
|
||||
code > span.cn { } /* Constant */
|
||||
code > span.sc { color: #008080; } /* SpecialChar */
|
||||
code > span.vs { color: #008080; } /* VerbatimString */
|
||||
code > span.ss { color: #008080; } /* SpecialString */
|
||||
code > span.im { } /* Import */
|
||||
code > span.va { } /* Variable */
|
||||
code > span.cf { color: #0000ff; } /* ControlFlow */
|
||||
code > span.op { } /* Operator */
|
||||
code > span.bu { } /* BuiltIn */
|
||||
code > span.ex { } /* Extension */
|
||||
code > span.pp { color: #ff4000; } /* Preprocessor */
|
||||
code > span.do { color: #008000; } /* Documentation */
|
||||
code > span.an { color: #008000; } /* Annotation */
|
||||
code > span.cv { color: #008000; } /* CommentVar */
|
||||
code > span.at { } /* Attribute */
|
||||
code > span.in { color: #008000; } /* Information */
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="tutorial.css">
|
||||
<!--[if lt IE 9]>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1 class="title">GraphQL Haskell Tutorial</h1>
|
||||
</header>
|
||||
<nav id="TOC">
|
||||
<ul>
|
||||
<li><a href="#getting-started">Getting started</a><ul>
|
||||
<li><a href="#first-example">First example</a></li>
|
||||
<li><a href="#monadic-actions">Monadic actions</a></li>
|
||||
<li><a href="#errors">Errors</a></li>
|
||||
<li><a href="#combining-resolvers">Combining resolvers</a></li>
|
||||
</ul></li>
|
||||
<li><a href="#further-examples">Further examples</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<section id="getting-started" class="level2">
|
||||
<h2>Getting started</h2>
|
||||
<p>Welcome to graphql-haskell!</p>
|
||||
<p>We have written a small tutorial to help you (and ourselves) understand the graphql package.</p>
|
||||
<p>Since this file is a literate haskell file, we start by importing some dependencies.</p>
|
||||
<div class="sourceCode"><pre class="sourceCode literate haskell"><code class="sourceCode haskell"><span class="ot">{-# LANGUAGE OverloadedStrings #-}</span>
|
||||
<span class="ot">{-# LANGUAGE LambdaCase #-}</span>
|
||||
<span class="kw">module</span> <span class="dt">Main</span> <span class="kw">where</span>
|
||||
|
||||
<span class="kw">import </span><span class="dt">Prelude</span> <span class="kw">hiding</span> (empty, putStrLn)
|
||||
<span class="kw">import </span><span class="dt">Data.GraphQL</span>
|
||||
<span class="kw">import </span><span class="dt">Data.GraphQL.Schema</span>
|
||||
<span class="kw">import qualified</span> <span class="dt">Data.GraphQL.Schema</span> <span class="kw">as</span> <span class="dt">Schema</span>
|
||||
|
||||
<span class="kw">import </span><span class="dt">Control.Applicative</span>
|
||||
<span class="kw">import </span><span class="dt">Data.Text</span> <span class="kw">hiding</span> (empty)
|
||||
<span class="kw">import </span><span class="dt">Data.Aeson</span>
|
||||
<span class="kw">import </span><span class="dt">Data.ByteString.Lazy.Char8</span> (putStrLn)
|
||||
|
||||
<span class="kw">import </span><span class="dt">Data.Time</span>
|
||||
|
||||
<span class="kw">import </span><span class="dt">Debug.Trace</span></code></pre></div>
|
||||
<section id="first-example" class="level3">
|
||||
<h3>First example</h3>
|
||||
<p>Now, as our first example, we are going to look at the example from <a href="https://github.com/graphql/graphql-js">graphql.js</a>.</p>
|
||||
<p>First we build a GraphQL schema.</p>
|
||||
<div class="sourceCode"><pre class="sourceCode literate haskell"><code class="sourceCode haskell"><span class="ot">schema1 ::</span> <span class="dt">Alternative</span> f <span class="ot">=></span> <span class="dt">Schema</span> f
|
||||
schema1 <span class="fu">=</span> <span class="dt">Schema</span> [hello]
|
||||
|
||||
<span class="ot">hello ::</span> <span class="dt">Alternative</span> f <span class="ot">=></span> <span class="dt">Resolver</span> f
|
||||
hello <span class="fu">=</span> Schema.scalar <span class="st">"hello"</span> (<span class="st">"it's me"</span><span class="ot"> ::</span> <span class="dt">Text</span>)</code></pre></div>
|
||||
<p>This defines a simple schema with one type and one field, that resolves to a fixed value.</p>
|
||||
<p>Next we define our query.</p>
|
||||
<div class="sourceCode"><pre class="sourceCode literate haskell"><code class="sourceCode haskell"><span class="ot">query1 ::</span> <span class="dt">Text</span>
|
||||
query1 <span class="fu">=</span> <span class="st">"{ hello }"</span></code></pre></div>
|
||||
<p>To run the query, we call the <code>graphql</code> with the schema and the query.</p>
|
||||
<div class="sourceCode"><pre class="sourceCode literate haskell"><code class="sourceCode haskell"><span class="ot">main1 ::</span> <span class="dt">IO</span> ()
|
||||
main1 <span class="fu">=</span> putStrLn <span class="fu">=<<</span> encode <span class="fu"><$></span> graphql schema1 query1</code></pre></div>
|
||||
<p>This runs the query by fetching the one field defined, returning</p>
|
||||
<p><code>{"data" : {"hello":"it's me"}}</code></p>
|
||||
</section>
|
||||
<section id="monadic-actions" class="level3">
|
||||
<h3>Monadic actions</h3>
|
||||
<p>For this example, we’re going to be using time.</p>
|
||||
<div class="sourceCode"><pre class="sourceCode literate haskell"><code class="sourceCode haskell"><span class="ot">schema2 ::</span> <span class="dt">Schema</span> <span class="dt">IO</span>
|
||||
schema2 <span class="fu">=</span> <span class="dt">Schema</span> [time]
|
||||
|
||||
<span class="ot">time ::</span> <span class="dt">Resolver</span> <span class="dt">IO</span>
|
||||
time <span class="fu">=</span> Schema.scalarA <span class="st">"time"</span> <span class="fu">$</span> \<span class="kw">case</span>
|
||||
[] <span class="ot">-></span> <span class="kw">do</span> t <span class="ot"><-</span> getCurrentTime
|
||||
return <span class="fu">$</span> show t
|
||||
_ <span class="ot">-></span> empty</code></pre></div>
|
||||
<p>This defines a simple schema with one type and one field, which resolves to the current time.</p>
|
||||
<p>Next we define our query.</p>
|
||||
<div class="sourceCode"><pre class="sourceCode literate haskell"><code class="sourceCode haskell"><span class="ot">query2 ::</span> <span class="dt">Text</span>
|
||||
query2 <span class="fu">=</span> <span class="st">"{ time }"</span>
|
||||
|
||||
<span class="ot">main2 ::</span> <span class="dt">IO</span> ()
|
||||
main2 <span class="fu">=</span> putStrLn <span class="fu">=<<</span> encode <span class="fu"><$></span> graphql schema2 query2</code></pre></div>
|
||||
<p>This runs the query, returning the current time</p>
|
||||
<p><code>{"data": {"time":"2016-03-08 23:28:14.546899 UTC"}}</code></p>
|
||||
</section>
|
||||
<section id="errors" class="level3">
|
||||
<h3>Errors</h3>
|
||||
<p>Errors are handled according to the spec, with fields that cause erros being resolved to <code>null</code>, and an error being added to the error list.</p>
|
||||
<p>An example of this is the following query:</p>
|
||||
<div class="sourceCode"><pre class="sourceCode literate haskell"><code class="sourceCode haskell"><span class="ot">queryShouldFail ::</span> <span class="dt">Text</span>
|
||||
queryShouldFail <span class="fu">=</span> <span class="st">"{ boyhowdy }"</span></code></pre></div>
|
||||
<p>Since there is no <code>boyhowdy</code> field in our schema, it will not resolve, and the query will fail, as we can see in the following example.</p>
|
||||
<div class="sourceCode"><pre class="sourceCode literate haskell"><code class="sourceCode haskell"><span class="ot">mainShouldFail ::</span> <span class="dt">IO</span> ()
|
||||
mainShouldFail <span class="fu">=</span> <span class="kw">do</span>
|
||||
r <span class="ot"><-</span> graphql schema1 query1
|
||||
putStrLn <span class="fu">$</span> encode r
|
||||
putStrLn <span class="st">"This will fail"</span>
|
||||
r <span class="ot"><-</span> graphql schema1 queryShouldFail
|
||||
putStrLn <span class="fu">$</span> encode r</code></pre></div>
|
||||
<p>This outputs:</p>
|
||||
<pre><code>{"data": {"hello": "it's me"}}
|
||||
This will fail
|
||||
{"data": {"boyhowdy": null}, "errors":[{"message": "the field boyhowdy did not resolve."}]}</code></pre>
|
||||
</section>
|
||||
<section id="combining-resolvers" class="level3">
|
||||
<h3>Combining resolvers</h3>
|
||||
<p>Now that we have two resolvers, we can define a schema which uses them both.</p>
|
||||
<div class="sourceCode"><pre class="sourceCode literate haskell"><code class="sourceCode haskell"><span class="ot">schema3 ::</span> <span class="dt">Schema</span> <span class="dt">IO</span>
|
||||
schema3 <span class="fu">=</span> <span class="dt">Schema</span> [hello, time]
|
||||
|
||||
<span class="ot">query3 ::</span> <span class="dt">Text</span>
|
||||
query3 <span class="fu">=</span> <span class="st">"query timeAndHello { time hello }"</span>
|
||||
|
||||
<span class="ot">main3 ::</span> <span class="dt">IO</span> ()
|
||||
main3 <span class="fu">=</span> putStrLn <span class="fu">=<<</span> encode <span class="fu"><$></span> graphql schema3 query3</code></pre></div>
|
||||
<p>This queries for both time and hello, returning</p>
|
||||
<p><code>{ "data": {"hello":"it's me","time":"2016-03-08 23:29:11.62108 UTC"}}</code></p>
|
||||
<p>Notice that we can name our queries, as we did with <code>timeAndHello</code>. Since we have only been using single queries, we can use the shorthand <code>{ time hello}</code>, as we have been doing in the previous examples.</p>
|
||||
<p>In GraphQL there can only be one operation per query.</p>
|
||||
</section>
|
||||
</section>
|
||||
<section id="further-examples" class="level2">
|
||||
<h2>Further examples</h2>
|
||||
<p>More examples on queries and a more complex schema can be found in the test directory, in the <a href="../../tests/Test/StarWars">Test.StarWars</a> module. This includes a more complex schema, and more complex queries.</p>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
150
docs/tutorial/tutorial.lhs
Normal file
150
docs/tutorial/tutorial.lhs
Normal file
@ -0,0 +1,150 @@
|
||||
---
|
||||
title: GraphQL Haskell Tutorial
|
||||
---
|
||||
|
||||
|
||||
== Getting started ==
|
||||
|
||||
Welcome to graphql-haskell!
|
||||
|
||||
We have written a small tutorial to help you (and ourselves) understand the graphql package.
|
||||
|
||||
Since this file is a literate haskell file, we start by importing some dependencies.
|
||||
|
||||
> {-# LANGUAGE OverloadedStrings #-}
|
||||
> {-# LANGUAGE LambdaCase #-}
|
||||
> module Main where
|
||||
>
|
||||
> import Prelude hiding (empty, putStrLn)
|
||||
> import Data.GraphQL
|
||||
> import Data.GraphQL.Schema
|
||||
> import qualified Data.GraphQL.Schema as Schema
|
||||
>
|
||||
> import Control.Applicative
|
||||
> import Data.Text hiding (empty)
|
||||
> import Data.Aeson
|
||||
> import Data.ByteString.Lazy.Char8 (putStrLn)
|
||||
>
|
||||
> import Data.Time
|
||||
>
|
||||
> import Debug.Trace
|
||||
|
||||
=== First example ===
|
||||
|
||||
Now, as our first example, we are going to look at the
|
||||
example from [graphql.js](https://github.com/graphql/graphql-js).
|
||||
|
||||
First we build a GraphQL schema.
|
||||
|
||||
> schema1 :: Alternative f => Schema f
|
||||
> schema1 = Schema [hello]
|
||||
>
|
||||
> hello :: Alternative f => Resolver f
|
||||
> hello = Schema.scalar "hello" ("it's me" :: Text)
|
||||
|
||||
This defines a simple schema with one type and one field, that resolves to a fixed value.
|
||||
|
||||
Next we define our query.
|
||||
|
||||
> query1 :: Text
|
||||
> query1 = "{ hello }"
|
||||
|
||||
|
||||
To run the query, we call the `graphql` with the schema and the query.
|
||||
|
||||
> main1 :: IO ()
|
||||
> main1 = putStrLn =<< encode <$> graphql schema1 query1
|
||||
|
||||
This runs the query by fetching the one field defined,
|
||||
returning
|
||||
|
||||
```{"data" : {"hello":"it's me"}}```
|
||||
|
||||
|
||||
|
||||
=== Monadic actions ===
|
||||
|
||||
For this example, we're going to be using time.
|
||||
|
||||
> schema2 :: Schema IO
|
||||
> schema2 = Schema [time]
|
||||
>
|
||||
> time :: Resolver IO
|
||||
> time = Schema.scalarA "time" $ \case
|
||||
> [] -> do t <- getCurrentTime
|
||||
> return $ show t
|
||||
> _ -> empty
|
||||
|
||||
This defines a simple schema with one type and one field,
|
||||
which resolves to the current time.
|
||||
|
||||
Next we define our query.
|
||||
|
||||
> query2 :: Text
|
||||
> query2 = "{ time }"
|
||||
>
|
||||
> main2 :: IO ()
|
||||
> main2 = putStrLn =<< encode <$> graphql schema2 query2
|
||||
|
||||
This runs the query, returning the current time
|
||||
|
||||
```{"data": {"time":"2016-03-08 23:28:14.546899 UTC"}}```
|
||||
|
||||
|
||||
=== Errors ===
|
||||
|
||||
Errors are handled according to the spec,
|
||||
with fields that cause erros being resolved to `null`,
|
||||
and an error being added to the error list.
|
||||
|
||||
An example of this is the following query:
|
||||
|
||||
> queryShouldFail :: Text
|
||||
> queryShouldFail = "{ boyhowdy }"
|
||||
|
||||
Since there is no `boyhowdy` field in our schema, it will not resolve,
|
||||
and the query will fail, as we can see in the following example.
|
||||
|
||||
> mainShouldFail :: IO ()
|
||||
> mainShouldFail = do
|
||||
> r <- graphql schema1 query1
|
||||
> putStrLn $ encode r
|
||||
> putStrLn "This will fail"
|
||||
> r <- graphql schema1 queryShouldFail
|
||||
> putStrLn $ encode r
|
||||
>
|
||||
|
||||
This outputs:
|
||||
|
||||
```
|
||||
{"data": {"hello": "it's me"}}
|
||||
This will fail
|
||||
{"data": {"boyhowdy": null}, "errors":[{"message": "the field boyhowdy did not resolve."}]}
|
||||
```
|
||||
|
||||
=== Combining resolvers ===
|
||||
|
||||
Now that we have two resolvers, we can define a schema which uses them both.
|
||||
|
||||
> schema3 :: Schema IO
|
||||
> schema3 = Schema [hello, time]
|
||||
>
|
||||
> query3 :: Text
|
||||
> query3 = "query timeAndHello { time hello }"
|
||||
>
|
||||
> main3 :: IO ()
|
||||
> main3 = putStrLn =<< encode <$> graphql schema3 query3
|
||||
|
||||
This queries for both time and hello, returning
|
||||
|
||||
```{ "data": {"hello":"it's me","time":"2016-03-08 23:29:11.62108 UTC"}}```
|
||||
|
||||
Notice that we can name our queries, as we did with `timeAndHello`. Since we have only been using single queries, we can use the shorthand `{ time hello}`, as we have been doing in the previous examples.
|
||||
|
||||
In GraphQL there can only be one operation per query.
|
||||
|
||||
|
||||
== Further examples ==
|
||||
|
||||
More examples on queries and a more complex schema can be found in the test directory,
|
||||
in the [Test.StarWars](../../tests/Test/StarWars) module. This includes a more complex schema, and more complex queries.
|
BIN
docs/tutorial/tutorial.pdf
Normal file
BIN
docs/tutorial/tutorial.pdf
Normal file
Binary file not shown.
176
docs/tutorial/tutorial.rst
Normal file
176
docs/tutorial/tutorial.rst
Normal file
@ -0,0 +1,176 @@
|
||||
========================
|
||||
GraphQL Haskell Tutorial
|
||||
========================
|
||||
|
||||
.. contents::
|
||||
:depth: 3
|
||||
..
|
||||
|
||||
Getting started
|
||||
===============
|
||||
|
||||
Welcome to graphql-haskell!
|
||||
|
||||
We have written a small tutorial to help you (and ourselves) understand
|
||||
the graphql package.
|
||||
|
||||
Since this file is a literate haskell file, we start by importing some
|
||||
dependencies.
|
||||
|
||||
.. code:: haskell
|
||||
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
module Main where
|
||||
|
||||
import Prelude hiding (empty, putStrLn)
|
||||
import Data.GraphQL
|
||||
import Data.GraphQL.Schema
|
||||
import qualified Data.GraphQL.Schema as Schema
|
||||
|
||||
import Control.Applicative
|
||||
import Data.Text hiding (empty)
|
||||
import Data.Aeson
|
||||
import Data.ByteString.Lazy.Char8 (putStrLn)
|
||||
|
||||
import Data.Time
|
||||
|
||||
import Debug.Trace
|
||||
|
||||
First example
|
||||
-------------
|
||||
|
||||
Now, as our first example, we are going to look at the example from
|
||||
`graphql.js <https://github.com/graphql/graphql-js>`__.
|
||||
|
||||
First we build a GraphQL schema.
|
||||
|
||||
.. code:: haskell
|
||||
|
||||
schema1 :: Alternative f => Schema f
|
||||
schema1 = Schema [hello]
|
||||
|
||||
hello :: Alternative f => Resolver f
|
||||
hello = Schema.scalar "hello" ("it's me" :: Text)
|
||||
|
||||
This defines a simple schema with one type and one field, that resolves
|
||||
to a fixed value.
|
||||
|
||||
Next we define our query.
|
||||
|
||||
.. code:: haskell
|
||||
|
||||
query1 :: Text
|
||||
query1 = "{ hello }"
|
||||
|
||||
To run the query, we call the ``graphql`` with the schema and the query.
|
||||
|
||||
.. code:: haskell
|
||||
|
||||
main1 :: IO ()
|
||||
main1 = putStrLn =<< encode <$> graphql schema1 query1
|
||||
|
||||
This runs the query by fetching the one field defined, returning
|
||||
|
||||
``{"data" : {"hello":"it's me"}}``
|
||||
|
||||
Monadic actions
|
||||
---------------
|
||||
|
||||
For this example, we're going to be using time.
|
||||
|
||||
.. code:: haskell
|
||||
|
||||
schema2 :: Schema IO
|
||||
schema2 = Schema [time]
|
||||
|
||||
time :: Resolver IO
|
||||
time = Schema.scalarA "time" $ \case
|
||||
[] -> do t <- getCurrentTime
|
||||
return $ show t
|
||||
_ -> empty
|
||||
|
||||
This defines a simple schema with one type and one field, which resolves
|
||||
to the current time.
|
||||
|
||||
Next we define our query.
|
||||
|
||||
.. code:: haskell
|
||||
|
||||
query2 :: Text
|
||||
query2 = "{ time }"
|
||||
|
||||
main2 :: IO ()
|
||||
main2 = putStrLn =<< encode <$> graphql schema2 query2
|
||||
|
||||
This runs the query, returning the current time
|
||||
|
||||
``{"data": {"time":"2016-03-08 23:28:14.546899 UTC"}}``
|
||||
|
||||
Errors
|
||||
------
|
||||
|
||||
Errors are handled according to the spec, with fields that cause erros
|
||||
being resolved to ``null``, and an error being added to the error list.
|
||||
|
||||
An example of this is the following query:
|
||||
|
||||
.. code:: haskell
|
||||
|
||||
queryShouldFail :: Text
|
||||
queryShouldFail = "{ boyhowdy }"
|
||||
|
||||
Since there is no ``boyhowdy`` field in our schema, it will not resolve,
|
||||
and the query will fail, as we can see in the following example.
|
||||
|
||||
.. code:: haskell
|
||||
|
||||
mainShouldFail :: IO ()
|
||||
mainShouldFail = do
|
||||
r <- graphql schema1 query1
|
||||
putStrLn $ encode r
|
||||
putStrLn "This will fail"
|
||||
r <- graphql schema1 queryShouldFail
|
||||
putStrLn $ encode r
|
||||
|
||||
This outputs:
|
||||
|
||||
::
|
||||
|
||||
{"data": {"hello": "it's me"}}
|
||||
This will fail
|
||||
{"data": {"boyhowdy": null}, "errors":[{"message": "the field boyhowdy did not resolve."}]}
|
||||
|
||||
Combining resolvers
|
||||
-------------------
|
||||
|
||||
Now that we have two resolvers, we can define a schema which uses them
|
||||
both.
|
||||
|
||||
.. code:: haskell
|
||||
|
||||
schema3 :: Schema IO
|
||||
schema3 = Schema [hello, time]
|
||||
|
||||
query3 :: Text
|
||||
query3 = "query timeAndHello { time hello }"
|
||||
|
||||
main3 :: IO ()
|
||||
main3 = putStrLn =<< encode <$> graphql schema3 query3
|
||||
|
||||
This queries for both time and hello, returning
|
||||
|
||||
``{ "data": {"hello":"it's me","time":"2016-03-08 23:29:11.62108 UTC"}}``
|
||||
|
||||
Notice that we can name our queries, as we did with ``timeAndHello``.
|
||||
Since we have only been using single queries, we can use the shorthand
|
||||
``{ time hello}``, as we have been doing in the previous examples.
|
||||
|
||||
In GraphQL there can only be one operation per query.
|
||||
|
||||
Further examples
|
||||
================
|
||||
|
||||
More examples on queries and a more complex schema can be found in the
|
||||
test directory, in the `Test.StarWars <../../tests/Test/StarWars>`__
|
||||
module. This includes a more complex schema, and more complex queries.
|
@ -16,6 +16,7 @@ build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC == 7.8.4, GHC == 7.10.3
|
||||
extra-source-files: README.md CHANGELOG.md stack.yaml
|
||||
docs/tutorial/tutorial.lhs
|
||||
data-files: tests/data/*.graphql
|
||||
tests/data/*.min.graphql
|
||||
|
||||
@ -28,10 +29,11 @@ library
|
||||
Data.GraphQL.Execute
|
||||
Data.GraphQL.Schema
|
||||
Data.GraphQL.Parser
|
||||
build-depends: base >= 4.7 && < 5,
|
||||
text >= 0.11.3.1,
|
||||
aeson >= 0.7.0.3,
|
||||
Data.GraphQL.Error
|
||||
build-depends: aeson >= 0.7.0.3,
|
||||
attoparsec >= 0.10.4.0,
|
||||
base >= 4.7 && < 5,
|
||||
text >= 0.11.3.1,
|
||||
unordered-containers >= 0.2.5.0
|
||||
|
||||
test-suite tasty
|
||||
@ -44,15 +46,15 @@ test-suite tasty
|
||||
Test.StarWars.Data
|
||||
Test.StarWars.Schema
|
||||
Test.StarWars.QueryTests
|
||||
build-depends: base >= 4.6 && <5,
|
||||
aeson >= 0.7.0.3,
|
||||
text >= 0.11.3.1,
|
||||
build-depends: aeson >= 0.7.0.3,
|
||||
attoparsec >= 0.10.4.0,
|
||||
base >= 4.6 && <5,
|
||||
graphql,
|
||||
raw-strings-qq >= 1.1,
|
||||
tasty >= 0.10,
|
||||
tasty-hunit >= 0.9,
|
||||
unordered-containers >= 0.2.5.0,
|
||||
graphql
|
||||
text >= 0.11.3.1,
|
||||
unordered-containers >= 0.2.5.0
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
|
@ -2,7 +2,7 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
module Test.StarWars.QueryTests (test) where
|
||||
|
||||
import qualified Data.Aeson as Aeson (Value)
|
||||
import qualified Data.Aeson as Aeson (Value(Null), toJSON)
|
||||
import Data.Aeson (object, (.=))
|
||||
import Data.Text (Text)
|
||||
import Text.RawString.QQ (r)
|
||||
@ -28,7 +28,7 @@ test = testGroup "Star Wars Query Tests"
|
||||
}
|
||||
}
|
||||
|]
|
||||
$ object ["hero" .= object ["id" .= ("2001" :: Text)]]
|
||||
$ object [ "data" .= object ["hero" .= object ["id" .= ("2001" :: Text)]]]
|
||||
, testCase "R2-D2 ID and friends" . testQuery
|
||||
[r| query HeroNameAndFriendsQuery {
|
||||
hero {
|
||||
@ -40,7 +40,7 @@ test = testGroup "Star Wars Query Tests"
|
||||
}
|
||||
}
|
||||
|]
|
||||
$ object [
|
||||
$ object [ "data" .= object [
|
||||
"hero" .= object [
|
||||
"id" .= ("2001" :: Text)
|
||||
, "name" .= ("R2-D2" :: Text)
|
||||
@ -50,7 +50,7 @@ test = testGroup "Star Wars Query Tests"
|
||||
, object ["name" .= ("Leia Organa" :: Text)]
|
||||
]
|
||||
]
|
||||
]
|
||||
]]
|
||||
]
|
||||
, testGroup "Nested Queries"
|
||||
[ testCase "R2-D2 friends" . testQuery
|
||||
@ -67,7 +67,7 @@ test = testGroup "Star Wars Query Tests"
|
||||
}
|
||||
}
|
||||
|]
|
||||
$ object [
|
||||
$ object [ "data" .= object [
|
||||
"hero" .= object [
|
||||
"name" .= ("R2-D2" :: Text)
|
||||
, "friends" .= [
|
||||
@ -102,7 +102,7 @@ test = testGroup "Star Wars Query Tests"
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]]
|
||||
, testCase "Luke ID" . testQuery
|
||||
[r| query FetchLukeQuery {
|
||||
human(id: "1000") {
|
||||
@ -110,12 +110,12 @@ test = testGroup "Star Wars Query Tests"
|
||||
}
|
||||
}
|
||||
|]
|
||||
$ object [
|
||||
$ object [ "data" .= object [
|
||||
"human" .= object [
|
||||
"name" .= ("Luke Skywalker" :: Text)
|
||||
]
|
||||
]
|
||||
]
|
||||
]]
|
||||
, testCase "Luke ID with variable" . testQueryParams
|
||||
(\v -> if v == "someId"
|
||||
then Just "1000"
|
||||
@ -126,9 +126,9 @@ test = testGroup "Star Wars Query Tests"
|
||||
}
|
||||
}
|
||||
|]
|
||||
$ object [
|
||||
$ object [ "data" .= object [
|
||||
"human" .= object ["name" .= ("Luke Skywalker" :: Text)]
|
||||
]
|
||||
]]
|
||||
, testCase "Han ID with variable" . testQueryParams
|
||||
(\v -> if v == "someId"
|
||||
then Just "1002"
|
||||
@ -139,10 +139,10 @@ test = testGroup "Star Wars Query Tests"
|
||||
}
|
||||
}
|
||||
|]
|
||||
$ object [
|
||||
$ object [ "data" .= object [
|
||||
"human" .= object ["name" .= ("Han Solo" :: Text)]
|
||||
]
|
||||
, testCase "Invalid ID" $ testFailParams
|
||||
]]
|
||||
, testCase "Invalid ID" . testQueryParams
|
||||
(\v -> if v == "id"
|
||||
then Just "Not a valid ID"
|
||||
else Nothing)
|
||||
@ -151,13 +151,14 @@ test = testGroup "Star Wars Query Tests"
|
||||
name
|
||||
}
|
||||
}
|
||||
|]
|
||||
|] $ object ["data" .= object ["human" .= object ["name" .= Aeson.Null]],
|
||||
"errors" .= (Aeson.toJSON [object ["message" .= ("field name not resolved." :: Text)]])]
|
||||
-- TODO: This test is directly ported from `graphql-js`, however do we want
|
||||
-- to mimic the same behavior? Is this part of the spec? Once proper
|
||||
-- exceptions are implemented this test might no longer be meaningful.
|
||||
-- If the same behavior needs to be replicated, should it be implemented
|
||||
-- when defining the `Schema` or when executing?
|
||||
-- $ object ["human" .= Aeson.Null]
|
||||
-- $ object [ "data" .= object ["human" .= Aeson.Null] ]
|
||||
, testCase "Luke aliased" . testQuery
|
||||
[r| query FetchLukeAliased {
|
||||
luke: human(id: "1000") {
|
||||
@ -165,11 +166,11 @@ test = testGroup "Star Wars Query Tests"
|
||||
}
|
||||
}
|
||||
|]
|
||||
$ object [
|
||||
$ object [ "data" .= object [
|
||||
"luke" .= object [
|
||||
"name" .= ("Luke Skywalker" :: Text)
|
||||
]
|
||||
]
|
||||
]]
|
||||
, testCase "R2-D2 ID and friends aliased" . testQuery
|
||||
[r| query HeroNameAndFriendsQuery {
|
||||
hero {
|
||||
@ -181,7 +182,7 @@ test = testGroup "Star Wars Query Tests"
|
||||
}
|
||||
}
|
||||
|]
|
||||
$ object [
|
||||
$ object [ "data" .= object [
|
||||
"hero" .= object [
|
||||
"id" .= ("2001" :: Text)
|
||||
, "name" .= ("R2-D2" :: Text)
|
||||
@ -191,7 +192,7 @@ test = testGroup "Star Wars Query Tests"
|
||||
, object ["friendName" .= ("Leia Organa" :: Text)]
|
||||
]
|
||||
]
|
||||
]
|
||||
]]
|
||||
, testCase "Luke and Leia aliased" . testQuery
|
||||
[r| query FetchLukeAndLeiaAliased {
|
||||
luke: human(id: "1000") {
|
||||
@ -202,14 +203,14 @@ test = testGroup "Star Wars Query Tests"
|
||||
}
|
||||
}
|
||||
|]
|
||||
$ object [
|
||||
$ object [ "data" .= object [
|
||||
"luke" .= object [
|
||||
"name" .= ("Luke Skywalker" :: Text)
|
||||
]
|
||||
, "leia" .= object [
|
||||
"name" .= ("Leia Organa" :: Text)
|
||||
]
|
||||
]
|
||||
]]
|
||||
]
|
||||
|
||||
testQuery :: Text -> Aeson.Value -> Assertion
|
||||
@ -221,5 +222,5 @@ testQuery q expected = graphql schema q @?= Just expected
|
||||
testQueryParams :: Subs -> Text -> Aeson.Value -> Assertion
|
||||
testQueryParams f q expected = graphqlSubs schema f q @?= Just expected
|
||||
|
||||
testFailParams :: Subs -> Text -> Assertion
|
||||
testFailParams f q = graphqlSubs schema f q @?= Nothing
|
||||
-- testFailParams :: Subs -> Text -> Assertion
|
||||
-- testFailParams f q = graphqlSubs schema f q @?= Nothing
|
||||
|
Loading…
Reference in New Issue
Block a user