Accept resolvers given by the user as is

This commit is contained in:
Eugen Wissner 2020-05-13 16:21:48 +02:00
parent 9232e08eb9
commit 4c19c88e98
7 changed files with 166 additions and 70 deletions

View File

@ -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`.

View File

@ -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

View File

@ -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

View 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

View File

@ -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(..))

View File

@ -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
View 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)