{-# LANGUAGE PatternSynonyms #-} -- | GraphQL output. -- -- How we encode GraphQL responses. module GraphQL.Internal.Output ( Response(..) , Errors , Error(..) , GraphQLError(..) , singleError ) where import Protolude hiding (Location, Map) import Data.Aeson (ToJSON(..)) import Data.List.NonEmpty (NonEmpty(..)) import GraphQL.Value ( Object , objectFromList , Value , pattern ValueObject , pattern ValueNull , NameError(..) ) import GraphQL.Internal.Name (Name) import GraphQL.Value.ToValue (ToValue(..)) -- | GraphQL response. -- -- A GraphQL response must: -- -- * be a map -- * have a "data" key iff the operation executed -- * have an "errors" key iff the operation encountered errors -- * not include "data" if operation failed before execution (e.g. syntax errors, -- validation errors, missing info) -- * not have keys other than "data", "errors", and "extensions" -- -- Other interesting things: -- -- * Doesn't have to be JSON, but does have to have maps, strings, lists, -- and null -- * Can also support bool, int, enum, and float -- * Value of "extensions" must be a map -- -- "data" must be null if an error was encountered during execution that -- prevented a valid response. -- -- "errors" -- -- * must be a non-empty list -- * each error is a map with "message", optionally "locations" key -- with list of locations -- * locations are maps with 1-indexed "line" and "column" keys. data Response = Success Object | PreExecutionFailure Errors | ExecutionFailure Errors | PartialSuccess Object Errors deriving (Eq, Ord, Show) -- | Construct an object from a list of names and values. -- -- Panic if there are duplicate names. unsafeMakeObject :: HasCallStack => [(Name, Value)] -> Value unsafeMakeObject fields = case objectFromList fields of Nothing -> panic $ "Object has duplicate keys: " <> show fields Just object -> ValueObject object instance ToValue Response where toValue (Success x) = unsafeMakeObject [("data", toValue x)] toValue (PreExecutionFailure e) = unsafeMakeObject [("errors", toValue e)] toValue (ExecutionFailure e) = unsafeMakeObject [("data", ValueNull) ,("errors", toValue e)] toValue (PartialSuccess x e) = unsafeMakeObject [("data", toValue x) ,("errors", toValue e) ] instance ToJSON Response where toJSON = toJSON . toValue type Errors = NonEmpty Error data Error = Error Text [Location] deriving (Eq, Ord, Show) instance ToValue Error where toValue (Error message []) = unsafeMakeObject [("message", toValue message)] toValue (Error message locations) = unsafeMakeObject [("message", toValue message) ,("locations", toValue locations) ] -- | Make a list of errors containing a single error. singleError :: GraphQLError e => e -> Errors singleError e = toError e :| [] data Location = Location Line Column deriving (Eq, Ord, Show) type Line = Int32 -- XXX: 1-indexed natural number type Column = Int32 -- XXX: 1-indexed natural number instance ToValue Location where toValue (Location line column) = unsafeMakeObject [("line" , toValue line) ,("column", toValue column) ] -- | An error that arises while processing a GraphQL query. class GraphQLError e where -- | Represent an error as human-readable text, primarily intended for -- developers of GraphQL clients, and secondarily for developers of GraphQL -- servers. formatError :: e -> Text -- | Represent an error as human-readable text, together with reference to a -- series of locations within a GraphQL query document. Default -- implementation calls 'formatError' and provides no locations. toError :: e -> Error toError e = Error (formatError e) [] -- Defined here to avoid circular dependency. instance GraphQLError NameError where formatError (NameError name) = "Not a valid GraphQL name: " <> show name