Accept resolvers given by the user as is
This commit is contained in:
parent
9232e08eb9
commit
4c19c88e98
@ -7,6 +7,11 @@ and this project adheres to
|
|||||||
[Haskell Package Versioning Policy](https://pvp.haskell.org/).
|
[Haskell Package Versioning Policy](https://pvp.haskell.org/).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
### Changed
|
||||||
|
- `Resolver` is now `Resolver Name FieldResolver` where `FieldResolver` can
|
||||||
|
contain a JSON value or another resolver, which is invoked during the
|
||||||
|
execution. `FieldResolver` is executed in `ActionT` and the current `Field` is
|
||||||
|
passed in the reader and not as an explicit argument.
|
||||||
|
|
||||||
## [0.7.0.0] - 2020-05-11
|
## [0.7.0.0] - 2020-05-11
|
||||||
### Fixed
|
### Fixed
|
||||||
@ -20,7 +25,7 @@ and this project adheres to
|
|||||||
- `Trans.argument`.
|
- `Trans.argument`.
|
||||||
- Schema extension parser.
|
- Schema extension parser.
|
||||||
- Contributing guidelines.
|
- Contributing guidelines.
|
||||||
- `Schema.resolversToMap` (intended for to be used internally).
|
- `Schema.resolversToMap` (intended to be used internally).
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Rename `AST.Definition` into `AST.Document.ExecutableDefinition`.
|
- Rename `AST.Definition` into `AST.Document.ExecutableDefinition`.
|
||||||
|
@ -35,18 +35,19 @@ import qualified Language.GraphQL.Type as Type
|
|||||||
-- | Resolves a 'Field' into an @Aeson.@'Data.Aeson.Types.Object' with error
|
-- | Resolves a 'Field' into an @Aeson.@'Data.Aeson.Types.Object' with error
|
||||||
-- information (if an error has occurred). @m@ is an arbitrary monad, usually
|
-- information (if an error has occurred). @m@ is an arbitrary monad, usually
|
||||||
-- 'IO'.
|
-- 'IO'.
|
||||||
data Resolver m = Resolver
|
data Resolver m = Resolver Name (FieldResolver m)
|
||||||
Text -- ^ Name
|
|
||||||
(Field -> CollectErrsT m Aeson.Object) -- ^ Resolver
|
data FieldResolver m
|
||||||
|
= ValueResolver (ActionT m Aeson.Value)
|
||||||
|
| NestingResolver (ActionT m (Type.Wrapping (HashMap Name (FieldResolver m))))
|
||||||
|
|
||||||
-- | Converts resolvers to a map.
|
-- | Converts resolvers to a map.
|
||||||
resolversToMap
|
resolversToMap :: (Foldable f, Functor f)
|
||||||
:: (Foldable f, Functor f)
|
|
||||||
=> f (Resolver m)
|
=> f (Resolver m)
|
||||||
-> HashMap Text (Field -> CollectErrsT m Aeson.Object)
|
-> HashMap Text (FieldResolver m)
|
||||||
resolversToMap = HashMap.fromList . toList . fmap toKV
|
resolversToMap = HashMap.fromList . toList . fmap toKV
|
||||||
where
|
where
|
||||||
toKV (Resolver name f) = (name, f)
|
toKV (Resolver name r) = (name, r)
|
||||||
|
|
||||||
-- | Contains variables for the query. The key of the map is a variable name,
|
-- | Contains variables for the query. The key of the map is a variable name,
|
||||||
-- and the value is the variable value.
|
-- and the value is the variable value.
|
||||||
@ -54,85 +55,79 @@ type Subs = HashMap Name Value
|
|||||||
|
|
||||||
-- | Create a new 'Resolver' with the given 'Name' from the given 'Resolver's.
|
-- | Create a new 'Resolver' with the given 'Name' from the given 'Resolver's.
|
||||||
object :: Monad m => Name -> ActionT m [Resolver m] -> Resolver m
|
object :: Monad m => Name -> ActionT m [Resolver m] -> Resolver m
|
||||||
object name f = Resolver name $ resolveFieldValue f resolveRight
|
object name = Resolver name
|
||||||
where
|
. NestingResolver
|
||||||
resolveRight fld@(Field _ _ _ flds) resolver
|
. fmap (Type.Named . resolversToMap)
|
||||||
= withField (resolve (resolversToMap resolver) flds) fld
|
|
||||||
|
|
||||||
-- | Like 'object' but can be null or a list of objects.
|
-- | Like 'object' but can be null or a list of objects.
|
||||||
wrappedObject ::
|
wrappedObject :: Monad m
|
||||||
Monad m =>
|
=> Name
|
||||||
Name ->
|
-> ActionT m (Type.Wrapping [Resolver m])
|
||||||
ActionT m (Type.Wrapping [Resolver m]) ->
|
-> Resolver m
|
||||||
Resolver m
|
wrappedObject name = Resolver name
|
||||||
wrappedObject name f = Resolver name $ resolveFieldValue f resolveRight
|
. NestingResolver
|
||||||
where
|
. (fmap . fmap) resolversToMap
|
||||||
resolveRight fld@(Field _ _ _ sels) resolver
|
|
||||||
= withField (traverse (resolveMap sels) resolver) fld
|
|
||||||
resolveMap = flip (resolve . resolversToMap)
|
|
||||||
|
|
||||||
-- | A scalar represents a primitive value, like a string or an integer.
|
-- | A scalar represents a primitive value, like a string or an integer.
|
||||||
scalar :: (Monad m, Aeson.ToJSON a) => Name -> ActionT m a -> Resolver m
|
scalar :: (Monad m, Aeson.ToJSON a) => Name -> ActionT m a -> Resolver m
|
||||||
scalar name f = Resolver name $ resolveFieldValue f resolveRight
|
scalar name = Resolver name . ValueResolver . fmap Aeson.toJSON
|
||||||
where
|
|
||||||
resolveRight fld result = withField (return result) fld
|
|
||||||
|
|
||||||
-- | Like 'scalar' but can be null or a list of scalars.
|
-- | Like 'scalar' but can be null or a list of scalars.
|
||||||
wrappedScalar ::
|
wrappedScalar :: (Monad m, Aeson.ToJSON a)
|
||||||
(Monad m, Aeson.ToJSON a) =>
|
=> Name
|
||||||
Name ->
|
-> ActionT m (Type.Wrapping a)
|
||||||
ActionT m (Type.Wrapping a) ->
|
-> Resolver m
|
||||||
Resolver m
|
wrappedScalar name = Resolver name . ValueResolver . fmap Aeson.toJSON
|
||||||
wrappedScalar name f = Resolver name $ resolveFieldValue f resolveRight
|
|
||||||
where
|
|
||||||
resolveRight fld (Type.Named result) = withField (return result) fld
|
|
||||||
resolveRight fld Type.Null
|
|
||||||
= return $ HashMap.singleton (aliasOrName fld) Aeson.Null
|
|
||||||
resolveRight fld (Type.List result) = withField (return result) fld
|
|
||||||
|
|
||||||
resolveFieldValue ::
|
resolveFieldValue :: Monad m => Field -> ActionT m a -> m (Either Text a)
|
||||||
Monad m =>
|
resolveFieldValue field@(Field _ _ args _) =
|
||||||
ActionT m a ->
|
flip runReaderT (Context {arguments=args, info=field})
|
||||||
(Field -> a -> CollectErrsT m Aeson.Object) ->
|
. runExceptT
|
||||||
Field ->
|
. runActionT
|
||||||
CollectErrsT m (HashMap Text Aeson.Value)
|
|
||||||
resolveFieldValue f resolveRight fld@(Field _ _ args _) = do
|
|
||||||
result <- lift $ reader . runExceptT . runActionT $ f
|
|
||||||
either resolveLeft (resolveRight fld) result
|
|
||||||
where
|
|
||||||
reader = flip runReaderT $ Context {arguments=args}
|
|
||||||
resolveLeft err = do
|
|
||||||
_ <- addErrMsg err
|
|
||||||
return $ HashMap.singleton (aliasOrName fld) Aeson.Null
|
|
||||||
|
|
||||||
-- | Helper function to facilitate error handling and result emitting.
|
convert :: Type.Wrapping Aeson.Value -> Aeson.Value
|
||||||
withField :: (Monad m, Aeson.ToJSON a)
|
convert Type.Null = Aeson.Null
|
||||||
=> CollectErrsT m a -> Field -> CollectErrsT m (HashMap Text Aeson.Value)
|
convert (Type.Named value) = value
|
||||||
withField v fld
|
convert (Type.List value) = Aeson.toJSON value
|
||||||
= HashMap.singleton (aliasOrName fld) . Aeson.toJSON <$> runAppendErrs v
|
|
||||||
|
withField :: Monad m => Field -> FieldResolver m -> CollectErrsT m Aeson.Object
|
||||||
|
withField field (ValueResolver resolver) = do
|
||||||
|
answer <- lift $ resolveFieldValue field resolver
|
||||||
|
either (errmsg field) (pure . HashMap.singleton (aliasOrName field)) answer
|
||||||
|
withField field@(Field _ _ _ seqSelection) (NestingResolver resolver) = do
|
||||||
|
answer <- lift $ resolveFieldValue field resolver
|
||||||
|
case answer of
|
||||||
|
Right result -> do
|
||||||
|
nestedFields <- traverse (`resolve` seqSelection) result
|
||||||
|
pure $ HashMap.singleton (aliasOrName field) $ convert nestedFields
|
||||||
|
Left errorMessage -> errmsg field errorMessage
|
||||||
|
|
||||||
|
errmsg :: Monad m => Field -> Text -> CollectErrsT m (HashMap Text Aeson.Value)
|
||||||
|
errmsg field errorMessage = do
|
||||||
|
addErrMsg errorMessage
|
||||||
|
pure $ HashMap.singleton (aliasOrName field) Aeson.Null
|
||||||
|
|
||||||
-- | Takes a list of 'Resolver's and a list of 'Field's and applies each
|
-- | 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
|
-- 'Resolver' to each 'Field'. Resolves into a value containing the
|
||||||
-- resolved 'Field', or a null value and error information.
|
-- resolved 'Field', or a null value and error information.
|
||||||
resolve :: Monad m
|
resolve :: Monad m
|
||||||
=> HashMap Text (Field -> CollectErrsT m Aeson.Object)
|
=> HashMap Text (FieldResolver m)
|
||||||
-> Seq Selection
|
-> Seq Selection
|
||||||
-> CollectErrsT m Aeson.Value
|
-> CollectErrsT m Aeson.Value
|
||||||
resolve resolvers = fmap (Aeson.toJSON . fold) . traverse tryResolvers
|
resolve resolvers = fmap (Aeson.toJSON . fold) . traverse tryResolvers
|
||||||
where
|
where
|
||||||
resolveTypeName f = do
|
lookupResolver = flip HashMap.lookup resolvers
|
||||||
value <- f $ Field Nothing "__typename" mempty mempty
|
|
||||||
return $ HashMap.lookupDefault "" "__typename" value
|
|
||||||
tryResolvers (SelectionField fld@(Field _ name _ _))
|
tryResolvers (SelectionField fld@(Field _ name _ _))
|
||||||
= fromMaybe (errmsg fld) $ HashMap.lookup name resolvers <*> Just fld
|
| (Just resolver) <- lookupResolver name = withField fld resolver
|
||||||
tryResolvers (SelectionFragment (Fragment typeCondition selections')) = do
|
| otherwise = errmsg fld $ T.unwords ["field", name, "not resolved."]
|
||||||
that <- traverse resolveTypeName $ HashMap.lookup "__typename" resolvers
|
tryResolvers (SelectionFragment (Fragment typeCondition selections'))
|
||||||
if maybe True (Aeson.String typeCondition ==) that
|
| Just (ValueResolver resolver) <- lookupResolver "__typename" = do
|
||||||
then fmap fold . traverse tryResolvers $ selections'
|
let fakeField = Field Nothing "__typename" mempty mempty
|
||||||
else return mempty
|
that <- lift $ resolveFieldValue fakeField resolver
|
||||||
errmsg fld@(Field _ name _ _) = do
|
if Right (Aeson.String typeCondition) == that
|
||||||
addErrMsg $ T.unwords ["field", name, "not resolved."]
|
then fmap fold . traverse tryResolvers $ selections'
|
||||||
return $ HashMap.singleton (aliasOrName fld) Aeson.Null
|
else pure mempty
|
||||||
|
| otherwise = fmap fold . traverse tryResolvers $ selections'
|
||||||
|
|
||||||
aliasOrName :: Field -> Text
|
aliasOrName :: Field -> Text
|
||||||
aliasOrName (Field alias name _ _) = fromMaybe name alias
|
aliasOrName (Field alias name _ _) = fromMaybe name alias
|
||||||
|
@ -18,8 +18,9 @@ import Language.GraphQL.AST.Core
|
|||||||
import Prelude hiding (lookup)
|
import Prelude hiding (lookup)
|
||||||
|
|
||||||
-- | Resolution context holds resolver arguments.
|
-- | Resolution context holds resolver arguments.
|
||||||
newtype Context = Context
|
data Context = Context
|
||||||
{ arguments :: Arguments
|
{ arguments :: Arguments
|
||||||
|
, info :: Field
|
||||||
}
|
}
|
||||||
|
|
||||||
-- | Monad transformer stack used by the resolvers to provide error handling
|
-- | Monad transformer stack used by the resolvers to provide error handling
|
||||||
|
31
tests/Language/GraphQL/SchemaSpec.hs
Normal file
31
tests/Language/GraphQL/SchemaSpec.hs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
module Language.GraphQL.SchemaSpec
|
||||||
|
( spec
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.Aeson as Aeson
|
||||||
|
import qualified Data.Aeson.Types as Aeson
|
||||||
|
import qualified Data.Sequence as Sequence
|
||||||
|
import Data.Text (Text)
|
||||||
|
import Language.GraphQL.AST.Core
|
||||||
|
import Language.GraphQL.Error
|
||||||
|
import Language.GraphQL.Schema
|
||||||
|
import Test.Hspec (Spec, describe, it, shouldBe)
|
||||||
|
|
||||||
|
spec :: Spec
|
||||||
|
spec =
|
||||||
|
describe "resolve" $
|
||||||
|
it "ignores invalid __typename" $ do
|
||||||
|
let resolver = object "__typename" $ pure
|
||||||
|
[ scalar "field" $ pure ("T" :: Text)
|
||||||
|
]
|
||||||
|
schema = resolversToMap [resolver]
|
||||||
|
fields = Sequence.singleton
|
||||||
|
$ SelectionFragment
|
||||||
|
$ Fragment "T" Sequence.empty
|
||||||
|
expected = Aeson.object
|
||||||
|
[ ("data" , Aeson.emptyObject)
|
||||||
|
]
|
||||||
|
|
||||||
|
actual <- runCollectErrs (resolve schema fields)
|
||||||
|
actual `shouldBe` expected
|
@ -4,7 +4,7 @@ module Test.DirectiveSpec
|
|||||||
( spec
|
( spec
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Data.Aeson (Value, object, (.=))
|
import Data.Aeson (Value(..), object, (.=))
|
||||||
import Data.HashMap.Strict (HashMap)
|
import Data.HashMap.Strict (HashMap)
|
||||||
import qualified Data.HashMap.Strict as HashMap
|
import qualified Data.HashMap.Strict as HashMap
|
||||||
import Data.List.NonEmpty (NonEmpty(..))
|
import Data.List.NonEmpty (NonEmpty(..))
|
||||||
|
@ -189,3 +189,27 @@ spec = do
|
|||||||
]
|
]
|
||||||
actual <- graphql (HashMap.singleton "Query" $ garment "Hat" :| []) query
|
actual <- graphql (HashMap.singleton "Query" $ garment "Hat" :| []) query
|
||||||
actual `shouldBe` expected
|
actual `shouldBe` expected
|
||||||
|
|
||||||
|
it "test1" $ do
|
||||||
|
let query = [r|
|
||||||
|
{
|
||||||
|
garment {
|
||||||
|
circumference
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
expected = object
|
||||||
|
[ "data" .= object
|
||||||
|
[ "garment" .= object
|
||||||
|
[ "circumference" .= (60 :: Int)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
actual <- graphql schema query
|
||||||
|
actual `shouldBe` expected
|
||||||
|
where
|
||||||
|
schema = HashMap.singleton "Query" $ garment' :| []
|
||||||
|
garment' = Schema.object "garment" $ return
|
||||||
|
[ circumference'
|
||||||
|
]
|
||||||
|
circumference' = Schema.scalar "circumference" $ pure (60 :: Int)
|
||||||
|
40
tests/Test/QuerySpec.hs
Normal file
40
tests/Test/QuerySpec.hs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
module Test.QuerySpec
|
||||||
|
( spec
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Data.Aeson ((.=), object)
|
||||||
|
import qualified Data.HashMap.Strict as HashMap
|
||||||
|
import Data.List.NonEmpty (NonEmpty(..))
|
||||||
|
import Language.GraphQL
|
||||||
|
import qualified Language.GraphQL.Schema as Schema
|
||||||
|
import Test.Hspec (Spec, describe, it, shouldBe)
|
||||||
|
import Text.RawString.QQ (r)
|
||||||
|
|
||||||
|
spec :: Spec
|
||||||
|
spec =
|
||||||
|
describe "Query executor" $
|
||||||
|
it "returns objects from the root resolvers" $ do
|
||||||
|
let query = [r|
|
||||||
|
{
|
||||||
|
garment {
|
||||||
|
circumference
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
expected = object
|
||||||
|
[ "data" .= object
|
||||||
|
[ "garment" .= object
|
||||||
|
[ "circumference" .= (60 :: Int)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
actual <- graphql schema query
|
||||||
|
actual `shouldBe` expected
|
||||||
|
where
|
||||||
|
schema = HashMap.singleton "Query" $ garment' :| []
|
||||||
|
garment' = Schema.object "garment" $ return
|
||||||
|
[ circumference'
|
||||||
|
]
|
||||||
|
circumference' = Schema.scalar "circumference" $ pure (60 :: Int)
|
Loading…
Reference in New Issue
Block a user