These functions are from Language.GraphQL.Schema. There are actually only two generic types in GraphQL: Scalars and objects. Enum is a scalar value. According to the specification enums may be serailized to strings. And in the current implementation they used untyped strings anyway, so there is no point to have differently named functions with the same implementation as their scalar counterparts.
173 lines
6.4 KiB
Haskell
173 lines
6.4 KiB
Haskell
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
-- | This module provides a representation of a @GraphQL@ Schema in addition to
|
|
-- functions for defining and manipulating schemas.
|
|
module Language.GraphQL.Schema
|
|
( Resolver
|
|
, Schema
|
|
, Subs
|
|
, object
|
|
, objectA
|
|
, scalar
|
|
, scalarA
|
|
, enum
|
|
, enumA
|
|
, resolve
|
|
, wrappedEnum
|
|
, wrappedEnumA
|
|
, wrappedObject
|
|
, wrappedObjectA
|
|
, wrappedScalar
|
|
, wrappedScalarA
|
|
-- * AST Reexports
|
|
, Field
|
|
, Argument(..)
|
|
, Value(..)
|
|
) where
|
|
|
|
import Control.Monad.IO.Class (MonadIO(..))
|
|
import Control.Monad.Trans.Class (lift)
|
|
import Control.Monad.Trans.Except (runExceptT)
|
|
import Data.Foldable ( find
|
|
, fold
|
|
)
|
|
import Data.List.NonEmpty (NonEmpty)
|
|
import Data.Maybe (fromMaybe)
|
|
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
|
|
import Language.GraphQL.Error
|
|
import Language.GraphQL.Trans
|
|
import Language.GraphQL.Type
|
|
import Language.GraphQL.AST.Core
|
|
|
|
{-# DEPRECATED Schema "Use NonEmpty (Resolver m) instead" #-}
|
|
-- | A GraphQL schema.
|
|
-- @m@ is usually expected to be an instance of 'MonadIO'.
|
|
type Schema m = NonEmpty (Resolver m)
|
|
|
|
-- | Resolves a 'Field' into an @Aeson.@'Aeson.Object' with error information
|
|
-- (or 'empty'). @m@ is usually expected to be an instance of 'MonadIO.
|
|
data Resolver m = Resolver
|
|
Text -- ^ Name
|
|
(Field -> CollectErrsT m Aeson.Object) -- ^ Resolver
|
|
|
|
type Fields = [Field]
|
|
|
|
type Arguments = [Argument]
|
|
|
|
-- | Variable substitution function.
|
|
type Subs = Name -> Maybe Value
|
|
|
|
-- | Create a new 'Resolver' with the given 'Name' from the given 'Resolver's.
|
|
object :: MonadIO m => Name -> ActionT m [Resolver m] -> Resolver m
|
|
object name = objectA name . const
|
|
|
|
-- | Like 'object' but also taking 'Argument's.
|
|
objectA :: MonadIO m
|
|
=> Name -> (Arguments -> ActionT m [Resolver m]) -> Resolver m
|
|
objectA name f = Resolver name $ resolveFieldValue f resolveRight
|
|
where
|
|
resolveRight fld@(Field _ _ _ flds) resolver = withField (resolve resolver flds) fld
|
|
|
|
-- | Like 'object' but also taking 'Argument's and can be null or a list of objects.
|
|
wrappedObjectA :: MonadIO m
|
|
=> Name -> (Arguments -> ActionT m (Wrapping [Resolver m])) -> Resolver m
|
|
wrappedObjectA name f = Resolver name $ resolveFieldValue f resolveRight
|
|
where
|
|
resolveRight fld@(Field _ _ _ sels) resolver
|
|
= withField (traverse (`resolve` sels) resolver) fld
|
|
|
|
-- | Like 'object' but can be null or a list of objects.
|
|
wrappedObject :: MonadIO m
|
|
=> Name -> ActionT m (Wrapping [Resolver m]) -> Resolver m
|
|
wrappedObject name = wrappedObjectA name . const
|
|
|
|
-- | A scalar represents a primitive value, like a string or an integer.
|
|
scalar :: (MonadIO m, Aeson.ToJSON a) => Name -> ActionT m a -> Resolver m
|
|
scalar name = scalarA name . const
|
|
|
|
-- | Like 'scalar' but also taking 'Argument's.
|
|
scalarA :: (MonadIO m, Aeson.ToJSON a)
|
|
=> Name -> (Arguments -> ActionT m a) -> Resolver m
|
|
scalarA name f = Resolver name $ resolveFieldValue f resolveRight
|
|
where
|
|
resolveRight fld result = withField (return result) fld
|
|
|
|
-- | Lika 'scalar' but also taking 'Argument's and can be null or a list of scalars.
|
|
wrappedScalarA :: (MonadIO m, Aeson.ToJSON a)
|
|
=> Name -> (Arguments -> ActionT m (Wrapping a)) -> Resolver m
|
|
wrappedScalarA name f = Resolver name $ resolveFieldValue f resolveRight
|
|
where
|
|
resolveRight fld (Named result) = withField (return result) fld
|
|
resolveRight fld Null
|
|
= return $ HashMap.singleton (aliasOrName fld) Aeson.Null
|
|
resolveRight fld (List result) = withField (return result) fld
|
|
|
|
-- | Like 'scalar' but can be null or a list of scalars.
|
|
wrappedScalar :: (MonadIO m, Aeson.ToJSON a)
|
|
=> Name -> ActionT m (Wrapping a) -> Resolver m
|
|
wrappedScalar name = wrappedScalarA name . const
|
|
|
|
{-# DEPRECATED enum "Use scalar instead" #-}
|
|
enum :: MonadIO m => Name -> ActionT m [Text] -> Resolver m
|
|
enum name = enumA name . const
|
|
|
|
{-# DEPRECATED enumA "Use scalarA instead" #-}
|
|
enumA :: MonadIO m => Name -> (Arguments -> ActionT m [Text]) -> Resolver m
|
|
enumA name f = Resolver name $ resolveFieldValue f resolveRight
|
|
where
|
|
resolveRight fld resolver = withField (return resolver) fld
|
|
|
|
{-# DEPRECATED wrappedEnumA "Use wrappedScalarA instead" #-}
|
|
wrappedEnumA :: MonadIO m
|
|
=> Name -> (Arguments -> ActionT m (Wrapping [Text])) -> Resolver m
|
|
wrappedEnumA name f = Resolver name $ resolveFieldValue f resolveRight
|
|
where
|
|
resolveRight fld (Named resolver) = withField (return resolver) fld
|
|
resolveRight fld Null
|
|
= return $ HashMap.singleton (aliasOrName fld) Aeson.Null
|
|
resolveRight fld (List resolver) = withField (return resolver) fld
|
|
|
|
{-# DEPRECATED wrappedEnum "Use wrappedScalar instead" #-}
|
|
wrappedEnum :: MonadIO m => Name -> ActionT m (Wrapping [Text]) -> Resolver m
|
|
wrappedEnum name = wrappedEnumA name . const
|
|
|
|
resolveFieldValue :: MonadIO m
|
|
=> ([Argument] -> ActionT m a)
|
|
-> (Field -> a -> CollectErrsT m (HashMap Text Aeson.Value))
|
|
-> Field
|
|
-> CollectErrsT m (HashMap Text Aeson.Value)
|
|
resolveFieldValue f resolveRight fld@(Field _ _ args _) = do
|
|
result <- lift $ runExceptT . runActionT $ f args
|
|
either resolveLeft (resolveRight fld) result
|
|
where
|
|
resolveLeft err = do
|
|
_ <- addErrMsg err
|
|
return $ HashMap.singleton (aliasOrName fld) Aeson.Null
|
|
|
|
-- | Helper function to facilitate 'Argument' handling.
|
|
withField :: (MonadIO m, Aeson.ToJSON a)
|
|
=> CollectErrsT m a -> Field -> CollectErrsT m (HashMap Text Aeson.Value)
|
|
withField v fld
|
|
= HashMap.singleton (aliasOrName fld) . Aeson.toJSON <$> runAppendErrs v
|
|
|
|
-- | 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.
|
|
resolve :: MonadIO m
|
|
=> [Resolver m] -> Fields -> CollectErrsT m Aeson.Value
|
|
resolve resolvers = fmap (Aeson.toJSON . fold) . traverse tryResolvers
|
|
where
|
|
tryResolvers fld = maybe (errmsg fld) (tryResolver fld) $ find (compareResolvers fld) resolvers
|
|
compareResolvers (Field _ name _ _) (Resolver name' _) = name == name'
|
|
tryResolver fld (Resolver _ resolver) = resolver fld
|
|
errmsg fld@(Field _ name _ _) = do
|
|
addErrMsg $ T.unwords ["field", name, "not resolved."]
|
|
return $ HashMap.singleton (aliasOrName fld) Aeson.Null
|
|
|
|
aliasOrName :: Field -> Text
|
|
aliasOrName (Field alias name _ _) = fromMaybe name alias
|