summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEugen Wissner <belka@caraus.de>2020-07-14 19:37:56 +0200
committerEugen Wissner <belka@caraus.de>2020-07-14 19:37:56 +0200
commitae2210f6598f166116abebc1163e1523d3bc627c (patch)
tree02b18f29d0b025a702366e70405dbcc203092c96
parent840e129c4496b4e8145480d2b3c3cb34f505702e (diff)
downloadgraphql-ae2210f6598f166116abebc1163e1523d3bc627c.tar.gz
Support subscriptions
This is experimental support. The implementation is based on conduit and is boring. There is a new resolver data constructor that should create a source event stream. The executor receives the events, pipes them through the normal execution and puts them into the response stream which is returned to the user. - Tests are missing. - The executor should check field value resolver on subscription types. - The graphql function should probably return (Either ResponseEventStream Response), but I'm not sure about this. It will make the usage more complicated if no subscriptions are involved, but with the current API implementing subscriptions is more difficult than it should be.
-rw-r--r--CHANGELOG.md15
-rw-r--r--graphql.cabal7
-rw-r--r--package.yaml3
-rw-r--r--src/Language/GraphQL.hs12
-rw-r--r--src/Language/GraphQL/Error.hs8
-rw-r--r--src/Language/GraphQL/Execute.hs22
-rw-r--r--src/Language/GraphQL/Execute/Execution.hs19
-rw-r--r--src/Language/GraphQL/Execute/Subscribe.hs92
-rw-r--r--src/Language/GraphQL/Type.hs9
-rw-r--r--src/Language/GraphQL/Type/Internal.hs10
-rw-r--r--src/Language/GraphQL/Type/Out.hs76
-rw-r--r--stack.yaml2
-rw-r--r--tests/Language/GraphQL/ExecuteSpec.hs32
-rw-r--r--tests/Test/DirectiveSpec.hs4
-rw-r--r--tests/Test/FragmentSpec.hs16
-rw-r--r--tests/Test/RootOperationSpec.hs18
-rw-r--r--tests/Test/StarWars/Data.hs12
-rw-r--r--tests/Test/StarWars/Schema.hs87
18 files changed, 287 insertions, 157 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 862667c..224f936 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,16 +22,21 @@ and this project adheres to
- `Error.Response` represents a result of running a GraphQL query.
- `Type.Schema` exports `Type` which lists all types possible in the schema.
- Parsing subscriptions (the execution always fails yet).
+- `Error.ResponseEventStream`, `Type.Out.Resolve`, `Type.Out.Subscribe` and
+ `Type.Out.SourceEventStream` define subscription resolvers.
## Changed
-- `Trans.ActionT` has become to `Type.Out.ResolverT`. Since `Type.Out.Resolver`
- has gone it is a better name for GraphQL resolvers.
+- `Type.Out.Resolver`: Interface fields don't have resolvers, object fields
+ have value resolvers, root subscription type resolvers need an additional
+ resolver that creates an event stream. `Resolver` represents these differences
+ now and pairs a field with the function(s).
- All code from `Trans` is moved to `Type.Out` and exported by `Type` and
`Type.Out`.
- `AST.Core` contained only `Arguments` which was moved to `Type.Definition`.
`AST` provides now only functionality related to parsing and encoding, as it
should be.
-- `Execute.execute` takes an additional argument, a possible operation name.
+- `Execute.execute` takes an additional argument, a possible operation name
+ and returns either a stream or the response.
- `Error` module was changed to work with dedicated types for errors and the
response instead of JSON.
- `graphqlSubs` takes an additional argument, the operation name. The type of
@@ -40,7 +45,9 @@ and this project adheres to
underlying functions (in the `Execute` module).
## Removed
-- `Type.Out.Resolver`: It is an unneeded layer of complexity. Resolvers are a
+- `Trans.ActionT` is an unneeded layer of complexity. `Type.Out.Resolver`
+ represents possible resolver configurations.
+- `Type.Out.Resolver`: It . Resolvers are a
part of the fields and are called `Trans.ResolverT`.
- `Execute.executeWithName`. `Execute.execute` takes the operation name and
completely replaces `executeWithName`.
diff --git a/graphql.cabal b/graphql.cabal
index e7ac249..5f3c8ea 100644
--- a/graphql.cabal
+++ b/graphql.cabal
@@ -4,7 +4,7 @@ cabal-version: 1.12
--
-- see: https://github.com/sol/hpack
--
--- hash: c06170c5fd3d1c3e42fb8c8fde8afd88bf3dd142f6cee1f83128e8d00d443f2d
+-- hash: 38e16611476c6163a049a4ddbaef34cf3fdef8f85d25f7bcaed839372c9fdf75
name: graphql
version: 0.8.0.0
@@ -53,6 +53,7 @@ library
Language.GraphQL.Type.Schema
other-modules:
Language.GraphQL.Execute.Execution
+ Language.GraphQL.Execute.Subscribe
Language.GraphQL.Execute.Transform
Language.GraphQL.Type.Definition
Language.GraphQL.Type.Internal
@@ -61,6 +62,7 @@ library
build-depends:
aeson
, base >=4.7 && <5
+ , conduit
, containers
, megaparsec
, parser-combinators
@@ -68,7 +70,6 @@ library
, text
, transformers
, unordered-containers
- , vector
default-language: Haskell2010
test-suite tasty
@@ -97,6 +98,7 @@ test-suite tasty
QuickCheck
, aeson
, base >=4.7 && <5
+ , conduit
, containers
, graphql
, hspec
@@ -109,5 +111,4 @@ test-suite tasty
, text
, transformers
, unordered-containers
- , vector
default-language: Haskell2010
diff --git a/package.yaml b/package.yaml
index 88b238c..1c855e1 100644
--- a/package.yaml
+++ b/package.yaml
@@ -28,6 +28,7 @@ data-files:
dependencies:
- aeson
- base >= 4.7 && < 5
+- conduit
- containers
- megaparsec
- parser-combinators
@@ -35,12 +36,12 @@ dependencies:
- text
- transformers
- unordered-containers
-- vector
library:
source-dirs: src
other-modules:
- Language.GraphQL.Execute.Execution
+ - Language.GraphQL.Execute.Subscribe
- Language.GraphQL.Execute.Transform
- Language.GraphQL.Type.Definition
- Language.GraphQL.Type.Internal
diff --git a/src/Language/GraphQL.hs b/src/Language/GraphQL.hs
index 845d5cf..6ee2dd7 100644
--- a/src/Language/GraphQL.hs
+++ b/src/Language/GraphQL.hs
@@ -9,6 +9,7 @@ module Language.GraphQL
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Types as Aeson
+import Data.Either (fromRight)
import qualified Data.Sequence as Seq
import Data.Text (Text)
import Language.GraphQL.AST
@@ -34,10 +35,14 @@ graphqlSubs :: Monad m
-> Aeson.Object -- ^ Variable substitution function.
-> Text -- ^ Text representing a @GraphQL@ request document.
-> m Aeson.Value -- ^ Response.
-graphqlSubs schema operationName variableValues document' =
- either parseError executeRequest parsed >>= formatResponse
+graphqlSubs schema operationName variableValues document'
+ = either parseError executeRequest (parse document "" document')
+ >>= formatResponse
where
- parsed = parse document "" document'
+ executeRequest parsed
+ = fromRight streamReturned
+ <$> execute schema operationName variableValues parsed
+ streamReturned = singleError "This service does not support subscriptions."
formatResponse (Response data'' Seq.Empty) =
pure $ Aeson.object [("data", data'')]
formatResponse (Response data'' errors') = pure $ Aeson.object
@@ -54,4 +59,3 @@ graphqlSubs schema operationName variableValues document' =
[ ("line", Aeson.toJSON line)
, ("column", Aeson.toJSON column)
]
- executeRequest = execute schema operationName variableValues
diff --git a/src/Language/GraphQL/Error.hs b/src/Language/GraphQL/Error.hs
index 3dbc696..474ddc7 100644
--- a/src/Language/GraphQL/Error.hs
+++ b/src/Language/GraphQL/Error.hs
@@ -9,19 +9,20 @@ module Language.GraphQL.Error
, Error(..)
, Resolution(..)
, Response(..)
+ , ResponseEventStream
, addErr
, addErrMsg
, runCollectErrs
, singleError
) where
+import Conduit
import Control.Monad.Trans.State (StateT, modify, runStateT)
import Data.HashMap.Strict (HashMap)
import Data.Sequence (Seq(..), (|>))
import qualified Data.Sequence as Seq
import Data.Text (Text)
import qualified Data.Text as Text
-import Data.Void (Void)
import Language.GraphQL.AST (Location(..), Name)
import Language.GraphQL.Execute.Coerce
import Language.GraphQL.Type.Schema
@@ -96,6 +97,11 @@ data Response a = Response
, errors :: Seq Error
} deriving (Eq, Show)
+-- | Each event in the underlying Source Stream triggers execution of the
+-- subscription selection set. The results of the execution generate a Response
+-- Stream.
+type ResponseEventStream m a = ConduitT () (Response a) m ()
+
-- | Runs the given query computation, but collects the errors into an error
-- list, which is then sent back with the data.
runCollectErrs :: (Monad m, Serialize a)
diff --git a/src/Language/GraphQL/Execute.hs b/src/Language/GraphQL/Execute.hs
index 471cd00..08aa5ab 100644
--- a/src/Language/GraphQL/Execute.hs
+++ b/src/Language/GraphQL/Execute.hs
@@ -7,13 +7,13 @@ module Language.GraphQL.Execute
) where
import Data.HashMap.Strict (HashMap)
-import qualified Data.HashMap.Strict as HashMap
import Data.Sequence (Seq(..))
import Data.Text (Text)
import Language.GraphQL.AST.Document (Document, Name)
import Language.GraphQL.Execute.Coerce
import Language.GraphQL.Execute.Execution
import qualified Language.GraphQL.Execute.Transform as Transform
+import qualified Language.GraphQL.Execute.Subscribe as Subscribe
import Language.GraphQL.Error
import qualified Language.GraphQL.Type.Definition as Definition
import qualified Language.GraphQL.Type.Out as Out
@@ -28,24 +28,28 @@ import Language.GraphQL.Type.Schema
execute :: (Monad m, VariableValue a, Serialize b)
=> Schema m -- ^ Resolvers.
-> Maybe Text -- ^ Operation name.
- -> HashMap.HashMap Name a -- ^ Variable substitution function.
+ -> HashMap Name a -- ^ Variable substitution function.
-> Document -- @GraphQL@ document.
- -> m (Response b)
+ -> m (Either (ResponseEventStream m b) (Response b))
execute schema operationName subs document =
case Transform.document schema operationName subs document of
- Left queryError -> pure $ singleError $ Transform.queryError queryError
+ Left queryError -> pure
+ $ Right
+ $ singleError
+ $ Transform.queryError queryError
Right transformed -> executeRequest transformed
executeRequest :: (Monad m, Serialize a)
=> Transform.Document m
- -> m (Response a)
+ -> m (Either (ResponseEventStream m a) (Response a))
executeRequest (Transform.Document types' rootObjectType operation)
| (Transform.Query _ fields) <- operation =
- executeOperation types' rootObjectType fields
+ Right <$> executeOperation types' rootObjectType fields
| (Transform.Mutation _ fields) <- operation =
- executeOperation types' rootObjectType fields
- | otherwise =
- pure $ singleError "This service does not support subscriptions."
+ Right <$> executeOperation types' rootObjectType fields
+ | (Transform.Subscription _ fields) <- operation
+ = either (Right . singleError) Left
+ <$> Subscribe.subscribe types' rootObjectType fields
-- This is actually executeMutation, but we don't distinguish between queries
-- and mutations yet.
diff --git a/src/Language/GraphQL/Execute/Execution.hs b/src/Language/GraphQL/Execute/Execution.hs
index e9ba4a7..fe4ad82 100644
--- a/src/Language/GraphQL/Execute/Execution.hs
+++ b/src/Language/GraphQL/Execute/Execution.hs
@@ -3,7 +3,9 @@
{-# LANGUAGE ViewPatterns #-}
module Language.GraphQL.Execute.Execution
- ( executeSelectionSet
+ ( coerceArgumentValues
+ , collectFields
+ , executeSelectionSet
) where
import Control.Monad.Trans.Class (lift)
@@ -32,10 +34,10 @@ import Prelude hiding (null)
resolveFieldValue :: Monad m
=> Type.Value
-> Type.Subs
- -> Type.ResolverT m a
- -> m (Either Text a)
-resolveFieldValue result args =
- flip runReaderT context . runExceptT . Type.runResolverT
+ -> Type.Resolve m
+ -> m (Either Text Type.Value)
+resolveFieldValue result args resolver =
+ flip runReaderT context $ runExceptT resolver
where
context = Type.Context
{ Type.arguments = Type.Arguments args
@@ -101,12 +103,12 @@ instanceOf objectType (AbstractUnionType unionType) =
go unionMemberType acc = acc || objectType == unionMemberType
executeField :: (Monad m, Serialize a)
- => Out.Field m
+ => Out.Resolver m
-> Type.Value
-> NonEmpty (Transform.Field m)
-> CollectErrsT m a
-executeField fieldDefinition prev fields = do
- let Out.Field _ fieldType argumentDefinitions resolver = fieldDefinition
+executeField (Out.ValueResolver fieldDefinition resolver) prev fields = do
+ let Out.Field _ fieldType argumentDefinitions = fieldDefinition
let (Transform.Field _ _ arguments' _ :| []) = fields
case coerceArgumentValues argumentDefinitions arguments' of
Nothing -> addErrMsg "Argument coercing failed."
@@ -115,6 +117,7 @@ executeField fieldDefinition prev fields = do
case answer of
Right result -> completeValue fieldType fields result
Left errorMessage -> addErrMsg errorMessage
+executeField _ _ _ = addErrMsg "No field value resolver specified."
completeValue :: (Monad m, Serialize a)
=> Out.Type m
diff --git a/src/Language/GraphQL/Execute/Subscribe.hs b/src/Language/GraphQL/Execute/Subscribe.hs
new file mode 100644
index 0000000..ee9b116
--- /dev/null
+++ b/src/Language/GraphQL/Execute/Subscribe.hs
@@ -0,0 +1,92 @@
+{- This Source Code Form is subject to the terms of the Mozilla Public License,
+ v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ obtain one at https://mozilla.org/MPL/2.0/. -}
+
+{-# LANGUAGE ExplicitForAll #-}
+{-# LANGUAGE OverloadedStrings #-}
+module Language.GraphQL.Execute.Subscribe
+ ( subscribe
+ ) where
+
+import Conduit
+import Control.Monad.Trans.Except (ExceptT(..), runExceptT)
+import Control.Monad.Trans.Reader (ReaderT(..), runReaderT)
+import Data.HashMap.Strict (HashMap)
+import qualified Data.HashMap.Strict as HashMap
+import qualified Data.Map.Strict as Map
+import qualified Data.List.NonEmpty as NonEmpty
+import Data.Sequence (Seq(..))
+import Data.Text (Text)
+import Language.GraphQL.AST (Name)
+import Language.GraphQL.Execute.Coerce
+import Language.GraphQL.Execute.Execution
+import qualified Language.GraphQL.Execute.Transform as Transform
+import Language.GraphQL.Error
+import qualified Language.GraphQL.Type.Definition as Definition
+import qualified Language.GraphQL.Type as Type
+import qualified Language.GraphQL.Type.Out as Out
+import Language.GraphQL.Type.Schema
+
+-- This is actually executeMutation, but we don't distinguish between queries
+-- and mutations yet.
+subscribe :: (Monad m, Serialize a)
+ => HashMap Name (Type m)
+ -> Out.ObjectType m
+ -> Seq (Transform.Selection m)
+ -> m (Either Text (ResponseEventStream m a))
+subscribe types' objectType fields = do
+ sourceStream <- createSourceEventStream types' objectType fields
+ traverse (mapSourceToResponseEvent types' objectType fields) sourceStream
+
+mapSourceToResponseEvent :: (Monad m, Serialize a)
+ => HashMap Name (Type m)
+ -> Out.ObjectType m
+ -> Seq (Transform.Selection m)
+ -> Out.SourceEventStream m
+ -> m (ResponseEventStream m a)
+mapSourceToResponseEvent types' subscriptionType fields sourceStream = pure
+ $ sourceStream
+ .| mapMC (executeSubscriptionEvent types' subscriptionType fields)
+
+createSourceEventStream :: Monad m
+ => HashMap Name (Type m)
+ -> Out.ObjectType m
+ -> Seq (Transform.Selection m)
+ -> m (Either Text (Out.SourceEventStream m))
+createSourceEventStream _types subscriptionType@(Out.ObjectType _ _ _ fieldTypes) fields
+ | [fieldGroup] <- Map.elems groupedFieldSet
+ , Transform.Field _ fieldName arguments' _ <- NonEmpty.head fieldGroup
+ , resolverT <- fieldTypes HashMap.! fieldName
+ , Out.EventStreamResolver fieldDefinition _ resolver <- resolverT
+ , Out.Field _ _fieldType argumentDefinitions <- fieldDefinition =
+ case coerceArgumentValues argumentDefinitions arguments' of
+ Nothing -> pure $ Left "Argument coercion failed."
+ Just argumentValues ->
+ resolveFieldEventStream Type.Null argumentValues resolver
+ | otherwise = pure $ Left "Subscription contains more than one field."
+ where
+ groupedFieldSet = collectFields subscriptionType fields
+
+resolveFieldEventStream :: Monad m
+ => Type.Value
+ -> Type.Subs
+ -> ExceptT Text (ReaderT Out.Context m) (Out.SourceEventStream m)
+ -> m (Either Text (Out.SourceEventStream m))
+resolveFieldEventStream result args resolver =
+ flip runReaderT context $ runExceptT resolver
+ where
+ context = Type.Context
+ { Type.arguments = Type.Arguments args
+ , Type.values = result
+ }
+
+-- This is actually executeMutation, but we don't distinguish between queries
+-- and mutations yet.
+executeSubscriptionEvent :: (Monad m, Serialize a)
+ => HashMap Name (Type m)
+ -> Out.ObjectType m
+ -> Seq (Transform.Selection m)
+ -> Definition.Value
+ -> m (Response a)
+executeSubscriptionEvent types' objectType fields initialValue =
+ runCollectErrs types' $ executeSelectionSet initialValue objectType fields
diff --git a/src/Language/GraphQL/Type.hs b/src/Language/GraphQL/Type.hs
index 0a30924..e84fc03 100644
--- a/src/Language/GraphQL/Type.hs
+++ b/src/Language/GraphQL/Type.hs
@@ -1,3 +1,7 @@
+{- This Source Code Form is subject to the terms of the Mozilla Public License,
+ v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ obtain one at https://mozilla.org/MPL/2.0/. -}
+
-- | Reexports non-conflicting type system and schema definitions.
module Language.GraphQL.Type
( In.InputField(..)
@@ -6,7 +10,10 @@ module Language.GraphQL.Type
, Out.Field(..)
, Out.InterfaceType(..)
, Out.ObjectType(..)
- , Out.ResolverT(..)
+ , Out.Resolve
+ , Out.Resolver(..)
+ , Out.SourceEventStream
+ , Out.Subscribe
, Out.UnionType(..)
, Out.argument
, module Language.GraphQL.Type.Definition
diff --git a/src/Language/GraphQL/Type/Internal.hs b/src/Language/GraphQL/Type/Internal.hs
index 07dabe6..9121d13 100644
--- a/src/Language/GraphQL/Type/Internal.hs
+++ b/src/Language/GraphQL/Type/Internal.hs
@@ -1,3 +1,7 @@
+{- This Source Code Form is subject to the terms of the Mozilla Public License,
+ v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ obtain one at https://mozilla.org/MPL/2.0/. -}
+
{-# LANGUAGE ExplicitForAll #-}
module Language.GraphQL.Type.Internal
@@ -36,11 +40,13 @@ collectReferencedTypes schema =
collect traverser typeName element foundTypes
| HashMap.member typeName foundTypes = foundTypes
| otherwise = traverser $ HashMap.insert typeName element foundTypes
- visitFields (Out.Field _ outputType arguments _) foundTypes
+ visitFields (Out.Field _ outputType arguments) foundTypes
= traverseOutputType outputType
$ foldr visitArguments foundTypes arguments
visitArguments (In.Argument _ inputType _) = traverseInputType inputType
visitInputFields (In.InputField _ inputType _) = traverseInputType inputType
+ getField (Out.ValueResolver field _) = field
+ getField (Out.EventStreamResolver field _ _) = field
traverseInputType (In.InputObjectBaseType objectType) =
let (In.InputObjectType typeName _ inputFields) = objectType
element = InputObjectType objectType
@@ -73,7 +79,7 @@ collectReferencedTypes schema =
traverseObjectType objectType foundTypes =
let (Out.ObjectType typeName _ interfaces fields) = objectType
element = ObjectType objectType
- traverser = polymorphicTraverser interfaces fields
+ traverser = polymorphicTraverser interfaces (getField <$> fields)
in collect traverser typeName element foundTypes
traverseInterfaceType interfaceType foundTypes =
let (Out.InterfaceType typeName _ interfaces fields) = interfaceType
diff --git a/src/Language/GraphQL/Type/Out.hs b/src/Language/GraphQL/Type/Out.hs
index 97107ca..d094b4d 100644
--- a/src/Language/GraphQL/Type/Out.hs
+++ b/src/Language/GraphQL/Type/Out.hs
@@ -1,3 +1,7 @@
+{- This Source Code Form is subject to the terms of the Mozilla Public License,
+ v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ obtain one at https://mozilla.org/MPL/2.0/. -}
+
{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ViewPatterns #-}
@@ -12,7 +16,10 @@ module Language.GraphQL.Type.Out
, Field(..)
, InterfaceType(..)
, ObjectType(..)
- , ResolverT(..)
+ , Resolve
+ , Subscribe
+ , Resolver(..)
+ , SourceEventStream
, Type(..)
, UnionType(..)
, argument
@@ -25,10 +32,7 @@ module Language.GraphQL.Type.Out
, pattern UnionBaseType
) where
-import Control.Applicative (Alternative(..))
-import Control.Monad (MonadPlus(..))
-import Control.Monad.IO.Class (MonadIO(..))
-import Control.Monad.Trans.Class (MonadTrans(..))
+import Conduit
import Control.Monad.Trans.Except (ExceptT)
import Control.Monad.Trans.Reader (ReaderT, asks)
import Data.HashMap.Strict (HashMap)
@@ -44,7 +48,7 @@ import qualified Language.GraphQL.Type.In as In
-- Almost all of the GraphQL types you define will be object types. Object
-- types have a name, but most importantly describe their fields.
data ObjectType m = ObjectType
- Name (Maybe Text) [InterfaceType m] (HashMap Name (Field m))
+ Name (Maybe Text) [InterfaceType m] (HashMap Name (Resolver m))
instance forall a. Eq (ObjectType a) where
(ObjectType this _ _ _) == (ObjectType that _ _ _) = this == that
@@ -74,7 +78,6 @@ data Field m = Field
(Maybe Text) -- ^ Description.
(Type m) -- ^ Field type.
(HashMap Name In.Argument) -- ^ Arguments.
- (ResolverT m Value) -- ^ Resolver.
-- | These types may be used as output types as the result of fields.
--
@@ -169,56 +172,41 @@ isNonNullType (NonNullUnionType _) = True
isNonNullType (NonNullListType _) = True
isNonNullType _ = False
--- | Resolution context holds resolver arguments.
+-- | Resolution context holds resolver arguments and the root value.
data Context = Context
{ arguments :: Arguments
, values :: Value
}
--- | Monad transformer stack used by the resolvers to provide error handling
--- and resolution context (resolver arguments).
---
--- Resolves a 'Field' into a 'Value' with error information (if an error has
--- occurred). @m@ is an arbitrary monad, usually 'IO'.
---
--- Resolving a field can result in a leaf value or an object, which is
--- represented as a list of nested resolvers, used to resolve the fields of that
--- object.
-newtype ResolverT m a = ResolverT
- { runResolverT :: ExceptT Text (ReaderT Context m) a
- }
-
-instance Functor m => Functor (ResolverT m) where
- fmap f = ResolverT . fmap f . runResolverT
-
-instance Monad m => Applicative (ResolverT m) where
- pure = ResolverT . pure
- (ResolverT f) <*> (ResolverT x) = ResolverT $ f <*> x
-
-instance Monad m => Monad (ResolverT m) where
- return = pure
- (ResolverT action) >>= f = ResolverT $ action >>= runResolverT . f
+-- | Monad transformer stack used by the resolvers for determining the resolved
+-- value of a field.
+type Resolve m = ExceptT Text (ReaderT Context m) Value
-instance MonadTrans ResolverT where
- lift = ResolverT . lift . lift
+-- | Monad transformer stack used by the resolvers for determining the resolved
+-- event stream of a subscription field.
+type Subscribe m = ExceptT Text (ReaderT Context m) (SourceEventStream m)
-instance MonadIO m => MonadIO (ResolverT m) where
- liftIO = lift . liftIO
+-- | A source stream represents the sequence of events, each of which will
+-- trigger a GraphQL execution corresponding to that event.
+type SourceEventStream m = ConduitT () Value m ()
-instance Monad m => Alternative (ResolverT m) where
- empty = ResolverT empty
- (ResolverT x) <|> (ResolverT y) = ResolverT $ x <|> y
-
-instance Monad m => MonadPlus (ResolverT m) where
- mzero = empty
- mplus = (<|>)
+-- | 'Resolver' associates some function(s) with each 'Field'. 'ValueResolver'
+-- resolves a 'Field' into a 'Value'. 'EventStreamResolver' resolves
+-- additionally a 'Field' into a 'SourceEventStream' if it is the field of a
+-- root subscription type.
+--
+-- The resolvers aren't part of the 'Field' itself because not all fields
+-- have resolvers (interface fields don't have an implementation).
+data Resolver m
+ = ValueResolver (Field m) (Resolve m)
+ | EventStreamResolver (Field m) (Resolve m) (Subscribe m)
-- | Retrieves an argument by its name. If the argument with this name couldn't
-- be found, returns 'Null' (i.e. the argument is assumed to
-- be optional then).
-argument :: Monad m => Name -> ResolverT m Value
+argument :: Monad m => Name -> Resolve m
argument argumentName = do
- argumentValue <- ResolverT $ lift $ asks $ lookupArgument . arguments
+ argumentValue <- lift $ asks $ lookupArgument . arguments
pure $ fromMaybe Null argumentValue
where
lookupArgument (Arguments argumentMap) =
diff --git a/stack.yaml b/stack.yaml
index 65e78cc..513705a 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -1,4 +1,4 @@
-resolver: lts-16.4
+resolver: lts-16.5
packages:
- .
diff --git a/tests/Language/GraphQL/ExecuteSpec.hs b/tests/Language/GraphQL/ExecuteSpec.hs
index e7ab9f8..f994482 100644
--- a/tests/Language/GraphQL/ExecuteSpec.hs
+++ b/tests/Language/GraphQL/ExecuteSpec.hs
@@ -5,6 +5,7 @@ module Language.GraphQL.ExecuteSpec
import Data.Aeson ((.=))
import qualified Data.Aeson as Aeson
+import Data.Either (fromRight)
import Data.Functor.Identity (Identity(..))
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
@@ -22,26 +23,27 @@ schema = Schema {query = queryType, mutation = Nothing}
queryType :: Out.ObjectType Identity
queryType = Out.ObjectType "Query" Nothing []
- $ HashMap.singleton "philosopher" philosopherField
+ $ HashMap.singleton "philosopher"
+ $ ValueResolver philosopherField
+ $ pure $ Type.Object mempty
where
- philosopherField
- = Out.Field Nothing (Out.NonNullObjectType philosopherType) HashMap.empty
- $ pure $ Type.Object mempty
+ philosopherField =
+ Out.Field Nothing (Out.NonNullObjectType philosopherType) HashMap.empty
philosopherType :: Out.ObjectType Identity
philosopherType = Out.ObjectType "Philosopher" Nothing []
$ HashMap.fromList resolvers
where
resolvers =
- [ ("firstName", firstNameField)
- , ("lastName", lastNameField)
+ [ ("firstName", ValueResolver firstNameField firstNameResolver)
+ , ("lastName", ValueResolver lastNameField lastNameResolver)
]
- firstNameField
- = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
- $ pure $ Type.String "Friedrich"
+ firstNameField =
+ Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
+ firstNameResolver = pure $ Type.String "Friedrich"
lastNameField
= Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
- $ pure $ Type.String "Nietzsche"
+ lastNameResolver = pure $ Type.String "Nietzsche"
spec :: Spec
spec =
@@ -54,8 +56,9 @@ spec =
]
expected = Response data'' mempty
execute' = execute schema Nothing (mempty :: HashMap Name Aeson.Value)
- actual = runIdentity
- $ either parseError execute'
+ actual = fromRight (singleError "")
+ $ runIdentity
+ $ either (pure . parseError) execute'
$ parse document "" "{ philosopher { firstName surname } }"
in actual `shouldBe` expected
it "merges selections" $
@@ -67,7 +70,8 @@ spec =
]
expected = Response data'' mempty
execute' = execute schema Nothing (mempty :: HashMap Name Aeson.Value)
- actual = runIdentity
- $ either parseError execute'
+ actual = fromRight (singleError "")
+ $ runIdentity
+ $ either (pure . parseError) execute'
$ parse document "" "{ philosopher { firstName } philosopher { lastName } }"
in actual `shouldBe` expected
diff --git a/tests/Test/DirectiveSpec.hs b/tests/Test/DirectiveSpec.hs
index 3243d2a..4d31cb9 100644
--- a/tests/Test/DirectiveSpec.hs
+++ b/tests/Test/DirectiveSpec.hs
@@ -16,10 +16,10 @@ import Text.RawString.QQ (r)
experimentalResolver :: Schema IO
experimentalResolver = Schema { query = queryType, mutation = Nothing }
where
- resolver = pure $ Int 5
queryType = Out.ObjectType "Query" Nothing []
$ HashMap.singleton "experimentalField"
- $ Out.Field Nothing (Out.NamedScalarType int) mempty resolver
+ $ Out.ValueResolver (Out.Field Nothing (Out.NamedScalarType int) mempty)
+ $ pure $ Int 5
emptyObject :: Aeson.Value
emptyObject = object
diff --git a/tests/Test/FragmentSpec.hs b/tests/Test/FragmentSpec.hs
index 94cc76c..af1812c 100644
--- a/tests/Test/FragmentSpec.hs
+++ b/tests/Test/FragmentSpec.hs
@@ -64,12 +64,14 @@ hatType = Out.ObjectType "Hat" Nothing []
, ("circumference", circumferenceFieldType)
]
-circumferenceFieldType :: Out.Field IO
-circumferenceFieldType = Out.Field Nothing (Out.NamedScalarType int) mempty
+circumferenceFieldType :: Out.Resolver IO
+circumferenceFieldType
+ = Out.ValueResolver (Out.Field Nothing (Out.NamedScalarType int) mempty)
$ pure $ snd circumference
-sizeFieldType :: Out.Field IO
-sizeFieldType = Out.Field Nothing (Out.NamedScalarType string) mempty
+sizeFieldType :: Out.Resolver IO
+sizeFieldType
+ = Out.ValueResolver (Out.Field Nothing (Out.NamedScalarType string) mempty)
$ pure $ snd size
toSchema :: Text -> (Text, Value) -> Schema IO
@@ -78,17 +80,15 @@ toSchema t (_, resolve) = Schema
where
unionMember = if t == "Hat" then hatType else shirtType
typeNameField = Out.Field Nothing (Out.NamedScalarType string) mempty
- $ pure $ String "Shirt"
garmentField = Out.Field Nothing (Out.NamedObjectType unionMember) mempty
- $ pure resolve
queryType =
case t of
"circumference" -> hatType
"size" -> shirtType
_ -> Out.ObjectType "Query" Nothing []
$ HashMap.fromList
- [ ("garment", garmentField)
- , ("__typename", typeNameField)
+ [ ("garment", ValueResolver garmentField (pure resolve))
+ , ("__typename", ValueResolver typeNameField (pure $ String "Shirt"))
]
spec :: Spec
diff --git a/tests/Test/RootOperationSpec.hs b/tests/Test/RootOperationSpec.hs
index 44b19a6..7202104 100644
--- a/tests/Test/RootOperationSpec.hs
+++ b/tests/Test/RootOperationSpec.hs
@@ -15,23 +15,23 @@ import qualified Language.GraphQL.Type.Out as Out
hatType :: Out.ObjectType IO
hatType = Out.ObjectType "Hat" Nothing []
$ HashMap.singleton "circumference"
- $ Out.Field Nothing (Out.NamedScalarType int) mempty
- $ pure
- $ Int 60
+ $ ValueResolver (Out.Field Nothing (Out.NamedScalarType int) mempty)
+ $ pure $ Int 60
schema :: Schema IO
schema = Schema
- (Out.ObjectType "Query" Nothing [] hatField)
- (Just $ Out.ObjectType "Mutation" Nothing [] incrementField)
+ (Out.ObjectType "Query" Nothing [] hatFieldResolver)
+ (Just $ Out.ObjectType "Mutation" Nothing [] incrementFieldResolver)
where
garment = pure $ Object $ HashMap.fromList
[ ("circumference", Int 60)
]
- incrementField = HashMap.singleton "incrementCircumference"
- $ Out.Field Nothing (Out.NamedScalarType int) mempty
+ incrementFieldResolver = HashMap.singleton "incrementCircumference"
+ $ ValueResolver (Out.Field Nothing (Out.NamedScalarType int) mempty)
$ pure $ Int 61
- hatField = HashMap.singleton "garment"
- $ Out.Field Nothing (Out.NamedObjectType hatType) mempty garment
+ hatField = Out.Field Nothing (Out.NamedObjectType hatType) mempty
+ hatFieldResolver =
+ HashMap.singleton "garment" $ ValueResolver hatField garment
spec :: Spec
spec =
diff --git a/tests/Test/StarWars/Data.hs b/tests/Test/StarWars/Data.hs
index dacd0cd..00a89d9 100644
--- a/tests/Test/StarWars/Data.hs
+++ b/tests/Test/StarWars/Data.hs
@@ -66,8 +66,8 @@ appearsIn :: Character -> [Int]
appearsIn (Left x) = _appearsIn . _droidChar $ x
appearsIn (Right x) = _appearsIn . _humanChar $ x
-secretBackstory :: ResolverT Identity Text
-secretBackstory = ResolverT $ throwE "secretBackstory is secret."
+secretBackstory :: Resolve Identity
+secretBackstory = throwE "secretBackstory is secret."
typeName :: Character -> Text
typeName = either (const "Droid") (const "Human")
@@ -161,10 +161,10 @@ getHero :: Int -> Character
getHero 5 = luke
getHero _ = artoo
-getHuman :: Alternative f => ID -> f Character
+getHuman :: ID -> Maybe Character
getHuman = fmap Right . getHuman'
-getHuman' :: Alternative f => ID -> f Human
+getHuman' :: ID -> Maybe Human
getHuman' "1000" = pure luke'
getHuman' "1001" = pure vader
getHuman' "1002" = pure han
@@ -172,10 +172,10 @@ getHuman' "1003" = pure leia
getHuman' "1004" = pure tarkin
getHuman' _ = empty
-getDroid :: Alternative f => ID -> f Character
+getDroid :: ID -> Maybe Character
getDroid = fmap Left . getDroid'
-getDroid' :: Alternative f => ID -> f Droid
+getDroid' :: ID -> Maybe Droid
getDroid' "2000" = pure threepio
getDroid' "2001" = pure artoo'
getDroid' _ = empty
diff --git a/tests/Test/StarWars/Schema.hs b/tests/Test/StarWars/Schema.hs
index cf18eca..ed3c32c 100644
--- a/tests/Test/StarWars/Schema.hs
+++ b/tests/Test/StarWars/Schema.hs
@@ -23,19 +23,20 @@ schema :: Schema Identity
schema = Schema { query = queryType, mutation = Nothing }
where
queryType = Out.ObjectType "Query" Nothing [] $ HashMap.fromList
- [ ("hero", heroField)
- , ("human", humanField)
- , ("droid", droidField)
+ [ ("hero", heroFieldResolver)
+ , ("human", humanFieldResolver)
+ , ("droid", droidFieldResolver)
]
- heroArguments = HashMap.singleton "episode"
+ heroField = Out.Field Nothing (Out.NamedObjectType heroObject)
+ $ HashMap.singleton "episode"
$ In.Argument Nothing (In.NamedEnumType episodeEnum) Nothing
- heroField =
- Out.Field Nothing (Out.NamedObjectType heroObject) heroArguments hero
- humanArguments = HashMap.singleton "id"
+ heroFieldResolver = ValueResolver heroField hero
+ humanField = Out.Field Nothing (Out.NamedObjectType heroObject)
+ $ HashMap.singleton "id"
$ In.Argument Nothing (In.NonNullScalarType string) Nothing
- humanField =
- Out.Field Nothing (Out.NamedObjectType heroObject) humanArguments human
- droidField = Out.Field Nothing (Out.NamedObjectType droidObject) mempty droid
+ humanFieldResolver = ValueResolver humanField human
+ droidField = Out.Field Nothing (Out.NamedObjectType droidObject) mempty
+ droidFieldResolver = ValueResolver droidField droid
heroObject :: Out.ObjectType Identity
heroObject = Out.ObjectType "Human" Nothing [] $ HashMap.fromList
@@ -48,8 +49,9 @@ heroObject = Out.ObjectType "Human" Nothing [] $ HashMap.fromList
, ("__typename", typenameFieldType)
]
where
- homePlanetFieldType = Out.Field Nothing (Out.NamedScalarType string) mempty
- $ idField "homePlanet"
+ homePlanetFieldType
+ = ValueResolver (Out.Field Nothing (Out.NamedScalarType string) mempty)
+ $ idField "homePlanet"
droidObject :: Out.ObjectType Identity
droidObject = Out.ObjectType "Droid" Nothing [] $ HashMap.fromList
@@ -62,39 +64,48 @@ droidObject = Out.ObjectType "Droid" Nothing [] $ HashMap.fromList
, ("__typename", typenameFieldType)
]
where
- primaryFunctionFieldType = Out.Field Nothing (Out.NamedScalarType string) mempty
+ primaryFunctionFieldType
+ = ValueResolver (Out.Field Nothing (Out.NamedScalarType string) mempty)
$ idField "primaryFunction"
-typenameFieldType :: Out.Field Identity
-typenameFieldType = Out.Field Nothing (Out.NamedScalarType string) mempty
+typenameFieldType :: Resolver Identity
+typenameFieldType
+ = ValueResolver (Out.Field Nothing (Out.NamedScalarType string) mempty)
$ idField "__typename"
-idFieldType :: Out.Field Identity
-idFieldType = Out.Field Nothing (Out.NamedScalarType id) mempty
+idFieldType :: Resolver Identity
+idFieldType
+ = ValueResolver (Out.Field Nothing (Out.NamedScalarType id) mempty)
$ idField "id"
-nameFieldType :: Out.Field Identity
-nameFieldType = Out.Field Nothing (Out.NamedScalarType string) mempty
+nameFieldType :: Resolver Identity
+nameFieldType
+ = ValueResolver (Out.Field Nothing (Out.NamedScalarType string) mempty)
$ idField "name"
-friendsFieldType :: Out.Field Identity
-friendsFieldType = Out.Field Nothing (Out.ListType $ Out.NamedObjectType droidObject) mempty
+friendsFieldType :: Resolver Identity
+friendsFieldType
+ = ValueResolver (Out.Field Nothing fieldType mempty)
$ idField "friends"
+ where
+ fieldType = Out.ListType $ Out.NamedObjectType droidObject
-appearsInField :: Out.Field Identity
-appearsInField = Out.Field (Just description) fieldType mempty
+appearsInField :: Resolver Identity
+appearsInField
+ = ValueResolver (Out.Field (Just description) fieldType mempty)
$ idField "appearsIn"
where
fieldType = Out.ListType $ Out.NamedEnumType episodeEnum
description = "Which movies they appear in."
-secretBackstoryFieldType :: Out.Field Identity
-secretBackstoryFieldType = Out.Field Nothing (Out.NamedScalarType string) mempty
- $ String <$> secretBackstory
+secretBackstoryFieldType :: Resolver Identity
+secretBackstoryFieldType = ValueResolver field secretBackstory
+ where
+ field = Out.Field Nothing (Out.NamedScalarType string) mempty
-idField :: Text -> ResolverT Identity Value
+idField :: Text -> Resolve Identity
idField f = do
- v <- ResolverT $ lift $ asks values
+ v <- lift $ asks values
let (Object v') = v
pure $ v' HashMap.! f
@@ -107,7 +118,7 @@ episodeEnum = EnumType "Episode" (Just description)
empire = ("EMPIRE", EnumValue $ Just "Released in 1980.")
jedi = ("JEDI", EnumValue $ Just "Released in 1983.")
-hero :: ResolverT Identity Value
+hero :: Resolve Identity
hero = do
episode <- argument "episode"
pure $ character $ case episode of
@@ -116,23 +127,19 @@ hero = do
Enum "JEDI" -> getHero 6
_ -> artoo
-human :: ResolverT Identity Value
+human :: Resolve Identity
human = do
id' <- argument "id"
case id' of
- String i -> do
- humanCharacter <- lift $ return $ getHuman i >>= Just
- case humanCharacter of
- Nothing -> pure Null
- Just e -> pure $ character e
- _ -> ResolverT $ throwE "Invalid arguments."
-
-droid :: ResolverT Identity Value
+ String i -> pure $ maybe Null character $ getHuman i >>= Just
+ _ -> throwE "Invalid arguments."
+
+droid :: Resolve Identity
droid = do
id' <- argument "id"
case id' of
- String i -> character <$> getDroid i
- _ -> ResolverT $ throwE "Invalid arguments."
+ String i -> pure $ maybe Null character $ getDroid i >>= Just
+ _ -> throwE "Invalid arguments."
character :: Character -> Value
character char = Object $ HashMap.fromList