Skip to content

Commit ecad3ef

Browse files
authored
Merge pull request #224 from kquick/master
Allow Handlers to return errors.
2 parents 3330f89 + f52d61c commit ecad3ef

File tree

9 files changed

+182
-74
lines changed

9 files changed

+182
-74
lines changed

docs/source/tutorial/Introduction.lhs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import System.Random
1616
1717
import GraphQL
1818
import GraphQL.API (Object, Field, Argument, (:>), Union)
19-
import GraphQL.Resolver (Handler, (:<>)(..), unionValue)
19+
import GraphQL.Resolver (Handler, (:<>)(..), unionValue, returns, handlerError)
2020
```
2121
2222
## A simple GraphQL service
@@ -99,7 +99,7 @@ Here's a `Handler` for `Hello`:
9999
hello :: Handler IO Hello
100100
hello = pure greeting
101101
where
102-
greeting who = pure ("Hello " <> who <> "!")
102+
greeting who = returns ("Hello " <> who <> "!")
103103
```
104104
105105
The type signature, `Handler IO Hello` shows that it's a `Handler` for
@@ -115,7 +115,9 @@ to run actions in the base monad.
115115
116116
The second layer of the handler, the implementation of `greeting`, produces
117117
the value of the `greeting` field. It is monadic so that it will only be
118-
executed when the field was requested.
118+
executed when the field was requested. It uses the 'returns' function to
119+
return the value for the field in the monad (technically, the Applicative
120+
context which is OK because a Monad is Applicative).
119121
120122
Each field handler is a separate monadic action so we only perform the side
121123
effects for fields present in the query.
@@ -124,6 +126,21 @@ This handler is in `Identity` because it doesn't do anything particularly
124126
monadic. It could be in `IO` or `STM` or `ExceptT Text IO` or whatever you
125127
would like.
126128
129+
### Errors in handlers
130+
131+
It's possible that a handler will encounter an error as well (for example, the argument might be looked up in a database and the user might specify a non-existent user). To help support GraphQL-compliant errors, a handler can use the `handlerError` function with the error text.
132+
133+
Here's a modified `Handler` for `Hello`:
134+
135+
```haskell
136+
helloFancy :: Handler IO Hello
137+
helloFancy = pure greeting
138+
where
139+
greeting who = if who == ""
140+
then handlerError "I need to know your name!"
141+
else returns ("Hello " <> who <> "!")
142+
```
143+
127144
### Running queries
128145
129146
Defining a service isn't much point unless you can query. Here's how:
@@ -174,15 +191,15 @@ And its handler:
174191
calculator :: Handler IO Calculator
175192
calculator = pure (add :<> subtract')
176193
where
177-
add a b = pure (a + b)
178-
subtract' a b = pure (a - b)
194+
add a b = returns (a + b)
195+
subtract' a b = returns (a - b)
179196
```
180197
181198
This handler introduces a new operator, `:<>` (pronounced "birdface"), which
182199
is used to compose two existing handlers into a new handler. It's inspired by
183200
the operator for monoids, `<>`.
184201
185-
Note that we still need `pure` for each individual handler.
202+
Note that we use `returns` for each individual handler.
186203
187204
## Nesting Objects
188205
@@ -238,7 +255,7 @@ We write nested handlers the same way we write the top-level handler:
238255
user :: Handler IO User
239256
user = pure name
240257
where
241-
name = pure "Mort"
258+
name = returns "Mort"
242259
243260
query :: Handler IO Query
244261
query = pure user

examples/InputObject.hs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import qualified Data.Aeson as Aeson
1111

1212
import GraphQL
1313
import GraphQL.API
14-
import GraphQL.Resolver (Handler)
14+
import GraphQL.Resolver (Handler, returns)
1515
import GraphQL.Value (FromValue, toValue)
1616

1717
data DogStuff = DogStuff { _toy :: Text, _likesTreats :: Bool } deriving (Show, Generic)
@@ -30,8 +30,8 @@ root = pure description
3030

3131
description :: DogStuff -> Handler IO Text
3232
description (DogStuff toy likesTreats)
33-
| likesTreats = pure $ "likes treats and their favorite toy is a " <> toy
34-
| otherwise = pure $ "their favorite toy is a " <> toy
33+
| likesTreats = returns $ "likes treats and their favorite toy is a " <> toy
34+
| otherwise = returns $ "their favorite toy is a " <> toy
3535

3636
-- | Show input object usage
3737
--

examples/UnionExample.hs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import Protolude
66
import qualified Data.Aeson as Aeson
77
import GraphQL.API (Field, List, Object, Union)
88
import GraphQL (interpretAnonymousQuery)
9-
import GraphQL.Resolver (Handler, (:<>)(..), unionValue)
9+
import GraphQL.Resolver (Handler, (:<>)(..), unionValue, returns)
1010
import GraphQL.Value (ToValue(..))
1111

1212
-- Slightly reduced example from the spec
@@ -17,10 +17,10 @@ type CatOrDog = Object "Me" '[] '[Field "myPet" (Union "CatOrDog" '[MiniCat, Min
1717
type CatOrDogList = Object "CatOrDogList" '[] '[Field "pets" (List (Union "CatOrDog" '[MiniCat, MiniDog]))]
1818

1919
miniCat :: Text -> Handler IO MiniCat
20-
miniCat name = pure (pure name :<> pure 32)
20+
miniCat name = pure (returns name :<> returns 32)
2121

2222
miniDog :: Handler IO MiniDog
23-
miniDog = pure (pure 100)
23+
miniDog = pure (returns 100)
2424

2525
catOrDog :: Handler IO CatOrDog
2626
catOrDog = pure $ do
@@ -29,10 +29,10 @@ catOrDog = pure $ do
2929

3030
catOrDogList :: Handler IO CatOrDogList
3131
catOrDogList = pure $
32-
pure [ unionValue @MiniCat (miniCat "Felix")
33-
, unionValue @MiniCat (miniCat "Mini")
34-
, unionValue @MiniDog miniDog
35-
]
32+
returns [ unionValue @MiniCat (miniCat "Felix")
33+
, unionValue @MiniCat (miniCat "Mini")
34+
, unionValue @MiniDog miniDog
35+
]
3636

3737
main :: IO ()
3838
main = do

graphql-wai/tests/Tests.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import GraphQL.Resolver
1111
type Cat = Object "Cat" '[] '[Field "name" Text]
1212

1313
catHandler :: Handler IO Cat
14-
catHandler = pure (pure "Felix")
14+
catHandler = pure (returns "Felix")
1515

1616
test1 :: Session ()
1717
test1 = do

scripts/hpc-ratchet

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ In a just world, this would be a separate config file, or command-line arguments
3535
Each item represents the number of "things" we are OK with not being covered.
3636
"""
3737
COVERAGE_TOLERANCE = {
38-
ALTERNATIVES: 154,
38+
ALTERNATIVES: 151,
3939
BOOLEANS: 8,
40-
EXPRESSIONS: 1366,
40+
EXPRESSIONS: 1351,
4141
LOCAL_DECLS: 10,
42-
TOP_LEVEL_DECLS: 673,
42+
TOP_LEVEL_DECLS: 670,
4343
}
4444

4545

src/GraphQL/Internal/Resolver.hs

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
{-# LANGUAGE DeriveFunctor #-}
44
{-# LANGUAGE FlexibleContexts #-}
55
{-# LANGUAGE FlexibleInstances #-}
6+
{-# LANGUAGE LambdaCase #-}
67
{-# LANGUAGE MultiParamTypeClasses #-}
78
{-# LANGUAGE PatternSynonyms #-}
89
{-# LANGUAGE RankNTypes #-}
@@ -24,6 +25,8 @@ module GraphQL.Internal.Resolver
2425
, Result(..)
2526
, unionValue
2627
, resolveOperation
28+
, returns
29+
, handlerError
2730
) where
2831

2932
-- TODO (probably incomplete, the spec is large)
@@ -84,6 +87,8 @@ data ResolverError
8487
| SubSelectionOnLeaf (SelectionSetByType Value)
8588
-- | Tried to treat an object as a leaf.
8689
| MissingSelectionSet
90+
-- | Error from handler
91+
| HandlerError Text
8792
deriving (Show, Eq)
8893

8994
instance GraphQLError ResolverError where
@@ -101,6 +106,8 @@ instance GraphQLError ResolverError where
101106
"Tried to get values within leaf field: " <> show ss
102107
formatError MissingSelectionSet =
103108
"Tried to treat object as if it were leaf field."
109+
formatError (HandlerError err) =
110+
"Handler error: " <> err
104111

105112
-- | Object field separation operator.
106113
--
@@ -146,6 +153,22 @@ instance Applicative Result where
146153
ok :: Value -> Result Value
147154
ok = pure
148155

156+
157+
-- | The result of a handler is either text errors generated by the
158+
-- handler or a value.
159+
type HandlerResult a = Either Text a
160+
161+
-- | `returns` is a convenience function for a Handler that is
162+
-- returning the expected value.
163+
returns :: Applicative f => a -> f (HandlerResult a)
164+
returns = pure . Right
165+
166+
-- | `handlerError` is a convenience function for a Handler that has
167+
-- encountered an error and is unable to return the expected value.
168+
handlerError :: Applicative f => Text -> f (HandlerResult a)
169+
handlerError = pure . Left
170+
171+
149172
class HasResolver m a where
150173
type Handler m a
151174
resolve :: Handler m a -> Maybe (SelectionSetByType Value) -> m (Result Value)
@@ -170,45 +193,56 @@ resolveOperation handler ss =
170193
valueMissing :: API.Defaultable a => Name -> Either ResolverError a
171194
valueMissing name = maybe (Left (ValueMissing name)) Right (API.defaultFor name)
172195

196+
gotHandlerErr :: Text -> Result Value
197+
gotHandlerErr err = Result [HandlerError err] GValue.ValueNull
198+
199+
handlerResult :: (Applicative f, ToValue a) => f (HandlerResult a) -> f (Result Value)
200+
handlerResult = fmap (either gotHandlerErr (ok . toValue))
201+
173202
instance forall m. (Applicative m) => HasResolver m Int32 where
174-
type Handler m Int32 = m Int32
175-
resolve handler Nothing = map (ok . toValue) handler
203+
type Handler m Int32 = m (HandlerResult Int32)
204+
resolve handler Nothing = handlerResult @m handler
176205
resolve _ (Just ss) = throwE (SubSelectionOnLeaf ss)
177206

178207
instance forall m. (Applicative m) => HasResolver m Double where
179-
type Handler m Double = m Double
180-
resolve handler Nothing = map (ok . toValue) handler
208+
type Handler m Double = m (HandlerResult Double)
209+
resolve handler Nothing = handlerResult handler
181210
resolve _ (Just ss) = throwE (SubSelectionOnLeaf ss)
182211

183212
instance forall m. (Applicative m) => HasResolver m Text where
184-
type Handler m Text = m Text
185-
resolve handler Nothing = map (ok . toValue) handler
213+
type Handler m Text = m (HandlerResult Text)
214+
resolve handler Nothing = handlerResult handler
186215
resolve _ (Just ss) = throwE (SubSelectionOnLeaf ss)
187216

188217
instance forall m. (Applicative m) => HasResolver m Bool where
189-
type Handler m Bool = m Bool
190-
resolve handler Nothing = map (ok . toValue) handler
218+
type Handler m Bool = m (HandlerResult Bool)
219+
resolve handler Nothing = handlerResult handler
191220
resolve _ (Just ss) = throwE (SubSelectionOnLeaf ss)
192221

193222
instance forall m hg. (Monad m, Applicative m, HasResolver m hg) => HasResolver m (API.List hg) where
194-
type Handler m (API.List hg) = m [Handler m hg]
223+
type Handler m (API.List hg) = m (HandlerResult [Handler m hg])
195224
resolve handler selectionSet = do
196-
h <- handler
197-
let a = traverse (flip (resolve @m @hg) selectionSet) h
198-
map aggregateResults a
225+
handler >>= \case
226+
Right h ->
227+
let a = traverse (flip (resolve @m @hg) selectionSet) h
228+
in map aggregateResults a
229+
Left err -> pure $ gotHandlerErr err
199230

200231
instance forall m ksN enum. (Applicative m, API.GraphQLEnum enum) => HasResolver m (API.Enum ksN enum) where
201-
type Handler m (API.Enum ksN enum) = m enum
202-
resolve handler Nothing = map (ok . GValue.ValueEnum . API.enumToValue) handler
232+
type Handler m (API.Enum ksN enum) = m (HandlerResult enum)
233+
resolve handler Nothing = either gotHandlerErr (ok . GValue.ValueEnum . API.enumToValue) <$> handler
203234
resolve _ (Just ss) = throwE (SubSelectionOnLeaf ss)
204235

205236
instance forall m hg. (HasResolver m hg, Monad m) => HasResolver m (Maybe hg) where
206-
type Handler m (Maybe hg) = m (Maybe (Handler m hg))
237+
type Handler m (Maybe hg) = m (HandlerResult (Maybe (Handler m hg)))
207238
resolve handler selectionSet = do
208239
result <- handler
209240
case result of
210-
Just x -> resolve @m @hg (x :: Handler m hg) selectionSet
211-
Nothing -> (pure . ok) GValue.ValueNull
241+
Right res ->
242+
case res of
243+
Just x -> resolve @m @hg (x :: Handler m hg) selectionSet
244+
Nothing -> (pure . ok) GValue.ValueNull
245+
Left err -> pure $ gotHandlerErr err
212246

213247
-- TODO: A parametrized `Result` is really not a good way to handle the
214248
-- "result" for resolveField, but not sure what to use either. Tom liked the

src/GraphQL/Resolver.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@ import GraphQL.Internal.Resolver as Export
1313
, Result(..)
1414
, unionValue
1515
, resolveOperation
16+
, returns
17+
, handlerError
1618
)

tests/EndToEndSpec.hs

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import qualified Data.Map as Map
1515
import GraphQL (makeSchema, compileQuery, executeQuery, interpretAnonymousQuery, interpretQuery)
1616
import GraphQL.API (Object, Field, List, Argument, (:>), Defaultable(..), HasAnnotatedInputType(..))
1717
import GraphQL.Internal.Syntax.AST (Variable(..))
18-
import GraphQL.Resolver ((:<>)(..), Handler, unionValue)
18+
import GraphQL.Resolver ((:<>)(..), Handler, unionValue, returns)
1919
import GraphQL.Value (ToValue(..), FromValue(..), makeName)
2020
import Test.Hspec
2121
import Text.RawString.QQ (r)
@@ -57,17 +57,17 @@ catOrDog = do
5757

5858
catOrDogList :: Handler IO (List CatOrDog)
5959
catOrDogList =
60-
pure [ unionValue @Cat (catHandler "Felix the Cat" (Just "felix") 42)
61-
, unionValue @Cat (catHandler "Henry" Nothing 10)
62-
, unionValue @Dog (viewServerDog mortgage)
63-
]
60+
returns [ unionValue @Cat (catHandler "Felix the Cat" (Just "felix") 42)
61+
, unionValue @Cat (catHandler "Henry" Nothing 10)
62+
, unionValue @Dog (viewServerDog mortgage)
63+
]
6464

6565
catHandler :: Text -> Maybe Text -> Int32 -> Handler IO Cat
6666
catHandler name nickName meowVolume = pure $
67-
pure name :<>
68-
pure (pure <$> nickName) :<>
69-
pure . const False :<> -- doesn't know any commands
70-
pure meowVolume
67+
returns name :<>
68+
returns (returns <$> nickName) :<>
69+
returns . const False :<> -- doesn't know any commands
70+
returns meowVolume
7171

7272
-- | Our server's internal representation of a 'Dog'.
7373
data ServerDog
@@ -94,17 +94,17 @@ isHouseTrained dog (Just True) = houseTrainedElsewhere dog
9494
-- | Present 'ServerDog' for GraphQL.
9595
viewServerDog :: ServerDog -> Handler IO Dog
9696
viewServerDog dog@ServerDog{..} = pure $
97-
pure name :<>
98-
pure (fmap pure nickname) :<>
99-
pure barkVolume :<>
100-
pure . doesKnowCommand dog :<>
101-
pure . isHouseTrained dog :<>
97+
returns name :<>
98+
returns (fmap returns nickname) :<>
99+
returns barkVolume :<>
100+
returns . doesKnowCommand dog :<>
101+
returns . isHouseTrained dog :<>
102102
viewServerHuman owner
103103

104104
describeDog :: DogStuff -> Handler IO Text
105105
describeDog (DogStuff toy likesTreats)
106-
| likesTreats = pure $ "likes treats and their favorite toy is a " <> toy
107-
| otherwise = pure $ "their favorite toy is a " <> toy
106+
| likesTreats = returns $ "likes treats and their favorite toy is a " <> toy
107+
| otherwise = returns $ "their favorite toy is a " <> toy
108108

109109
rootHandler :: ServerDog -> Handler IO QueryRoot
110110
rootHandler dog = pure $ viewServerDog dog :<> describeDog :<> catOrDog :<> catOrDogList
@@ -127,7 +127,7 @@ newtype ServerHuman = ServerHuman Text deriving (Eq, Ord, Show, Generic)
127127

128128
-- | Present a 'ServerHuman' as a GraphQL 'Human'.
129129
viewServerHuman :: ServerHuman -> Handler IO Human
130-
viewServerHuman (ServerHuman name) = pure (pure name)
130+
viewServerHuman (ServerHuman name) = pure (returns name)
131131

132132
-- | It me.
133133
jml :: ServerHuman

0 commit comments

Comments
 (0)