diff --git a/docs/source/tutorial/Introduction.lhs b/docs/source/tutorial/Introduction.lhs index 5de6775..1b609cd 100644 --- a/docs/source/tutorial/Introduction.lhs +++ b/docs/source/tutorial/Introduction.lhs @@ -6,58 +6,304 @@ First some imports: {-# LANGUAGE DataKinds #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeApplications #-} + module Introduction where import Protolude -import GraphQL.API (Object, Field, Argument, (:>)) -import GraphQL.Resolver (Handler, (:<>)(..)) +import System.Random + +import GraphQL +import GraphQL.API (Object, Field, Argument, (:>), Union) +import GraphQL.Resolver (Handler, (:<>)(..), unionValue) +``` + +## A simple GraphQL service + +A [GraphQL](http://graphql.org/) service is made up of two things: + + 1. A schema that defines the service + 2. Some code that implements the service's behavior + +We're going to build a very simple service that says hello to +people. Our GraphQL schema for this looks like: + +```graphql +type Hello { + greeting(who: String!): String! +} +``` + +Which means we have base type, an _object_ called `Hello`, which has a single +_field_ `greeting`, which takes a non-nullable `String` called `who` and +returns a `String`. + +Note that all the types here are GraphQL types, not Haskell types. `String` +here is a GraphQL `String`, not a Haskell one. + +And we want to be able to send queries that look like: + +```graphql +{ + greeting(who: "world") +} +``` + +And get responses like: + +```json +{ + "data": { + "greeting": "Hello world!" + } +} +``` + +### Defining the schema + +Here's how we would define the schema in Haskell: + +```haskell +type Hello = Object "Hello" '[] + '[ Argument "who" Text :> Field "greeting" Text + ] +``` + +Breaking this down, we define a new Haskell type `Hello`, which is a GraphQL +object (also named `"Hello"`) that implements no interfaces (hence `'[]`). It +has one field, called `"greeting"` which returns some `Text` and takes a +single named argument `"who"`, which is also `Text`. + +Note that the GraphQL `String` from above got translated into a Haskell +`Text`. + +There are some noteworthy differences between this schema and the GraphQL +schema: + +* The GraphQL schema requires a special annotation to say that a value cannot + be null, `!`. In Haskell, we instead assume that nothing can be null. +* In the GraphQL schema, the argument appears *after* the field name. In + Haskell, it appears *before*. +* In Haskell, we name the top-level type twice, once on left hand side of the + type definition and once on the right. + +### Implementing the handlers + +Once we have the schema, we need to define the corresponding handlers, which +are `Handler` values. + +Here's a `Handler` for `Hello`: + +```haskell +hello :: Handler IO Hello +hello = pure greeting + where + greeting who = pure ("Hello " <> who <> "!") +``` + +The type signature, `Handler IO Hello` shows that it's a `Handler` for +`Hello`, and that it runs in the `IO` monad. (Note: nothing about this example +code requires the `IO` monad, it's just a monad that lots of people has heard +of.) + +The implementation looks slightly weird, but it's weird for good reasons. + +The first layer of the handler, `pure greeting`, produces the `Hello` object. +The `pure` might seem redundant here, but making this step monadic allows us +to run actions in the base monad. + +The second layer of the handler, the implementation of `greeting`, produces +the value of the `greeting` field. It is monadic so that it will only be +executed when the field was requested. + +Each field handler is a separate monadic action so we only perform the side +effects for fields present in the query. + +This handler is in `Identity` because it doesn't do anything particularly +monadic. It could be in `IO` or `STM` or `ExceptT Text IO` or whatever you +would like. + +### Running queries + +Defining a service isn't much point unless you can query. Here's how: + +```haskell +queryHello :: IO Response +queryHello = interpretAnonymousQuery @Hello hello "{ greeting(who: \"mort\") }" +``` + +The actual `Response` type is fairly verbose, so we're most likely to turn it +into JSON: + +``` +λ Aeson.encode <$> queryHello +"{\"greeting\":\"Hello mort!\"}" ``` -The core idea for this library is that we define a composite type that -specifies the whole API, and then implement a matching handler. +## Combining field handlers with :<> -The main GraphQL entities we care about are Objects and Fields. Each -Field can have arguments. +How do we define an object with more than one field? + +Let's implement a simple calculator that can add and subtract integers. First, +the schema: + +```graphql +type Calculator { + add(a: Int!, b: Int!): Int!, + sub(a: Int!, b: Int!): Int!, +} +``` + +Here, `Calculator` is an object with two fields: `add` and `sub`. + +And now the Haskell version: ``` haskell -type HelloWorld = Object "HelloWorld" '[] - '[ Argument "greeting" Text :> Field "me" Text +type Calculator = Object "Calculator" '[] + '[ Argument "a" Int32 :> Argument "b" Int32 :> Field "add" Int32 + , Argument "a" Int32 :> Argument "b" Int32 :> Field "subtract" Int32 ] ``` -The example above is equivalent to the following GraphQL type: +So far, this is the same as our `Hello` example. + +And its handler: +```haskell +calculator :: Handler IO Calculator +calculator = pure (add :<> subtract') + where + add a b = pure (a + b) + subtract' a b = pure (a - b) ``` -type HelloWorld { - me(greeting: String!): String! + +This handler introduces a new operator, `:<>` (pronounced "birdface"), which +is used to compose two existing handlers into a new handler. It's inspired by +the operator for monoids, `<>`. + +Note that we still need `pure` for each individual handler. + +## Nesting Objects + +How do we define objects made up other objects? + +One of the great things in GraphQL is that objects can be used as types for +fields. Take this classic GraphQL schema as an example: + +```graphql +type Query { + me: User! +} + +type User { + name: Text! +} +``` + +We would query this schema with something like: + +```graphql +{ + me { + name + } } ``` -And if we had a code to handle that type (more later) we could query it like this: +Which would produce output like: +```json +{ + "data": { + "me": { + "name": "Mort" + } + } +} ``` -{ me(greeting: "hello") } + +The Haskell type for this schema looks like: + +```haskell +type User = Object "User" '[] '[Field "name" Text] +type Query = Object "Query" '[] '[Field "me" User] ``` -## The handler +Note that `Query` refers to the type `User` when it defines the field `me`. -We defined a corresponding handler via the `Handler m a` which takes -the monad to run in (`IO` in this case) and the actual API definition -(`HelloWorld`). +We write nested handlers the same way we write the top-level handler: ```haskell -handler :: Handler IO HelloWorld -handler = pure (\greeting -> pure (greeting <> " to me")) +user :: Handler IO User +user = pure name + where + name = pure "Mort" + +query :: Handler IO Query +query = pure user ``` -The implementation looks slightly weird, but it's weird for good -reasons. In order: +And that's it. -* The first `pure` allows us to run actions in the base monad (`IO` -here) before returning anything. This is useful to allocate a resource -like a database connection. -* The `pure` in the function call allows us to **avoid running -actions** when the field hasn't been requested: Each handler is a -separate monadic action so we only perform the side effects for fields -present in the query. +## Unions + +GraphQL has [support for union +types](http://graphql.org/learn/schema/#union-types). These require special +treatment in Haskell. + +Let's define a union, first in GraphQL: + +```graphql +union UserOrCalculator = User | Calculator +``` + +And now in Haskell: + +```haskell +type UserOrCalculator = Union "UserOrCalculator" '[User, Calculator] +``` + +And let's define a very simple top-level object that uses `UserOrCalculator`: + +```haskell +type UnionQuery = Object "UnionQuery" '[] '[Field "union" UserOrCalculator] +``` + +and a handler that randomly returns either a user or a calculator: + +```haskell +unionQuery :: Handler IO UnionQuery +unionQuery = do + returnUser <- randomIO + if returnUser + then pure (unionValue @User user) + else pure (unionValue @Calculator calculator) +``` + +The important thing here is that we have to wrap the actual objects we return +using `unionValue`. + +Note that while `unionValue` looks a bit like `unsafeCoerce` by forcing one +type to become another type, it's actually type-safe because we use a +*type-index* to pick the correct type from the union. Using e.g. `unionValue +@HelloWorld handler` will not compile because `HelloWorld` is not in the +union. + +## Where next? + +We have an +[examples](https://github.com/jml/graphql-api/tree/master/tests/Examples) +directory showing full code examples. + +We also have a fair number of [end-to-end +tests](https://github.com/jml/graphql-api/tree/master/tests/EndToEndTests.hs) +based on an [example +schema](https://github.com/jml/graphql-api/tree/master/tests/ExampleSchema.hs) +that you might find interesting. + +If you want to try the examples in this tutorial you can run: + +```bash +stack repl tutorial +``` diff --git a/docs/source/tutorial/package.yaml b/docs/source/tutorial/package.yaml index ef0b642..c11f65f 100644 --- a/docs/source/tutorial/package.yaml +++ b/docs/source/tutorial/package.yaml @@ -7,6 +7,9 @@ maintainer: tehunger@gmail.com, Jonathan M. Lange ghc-options: -Wall -pgmL markdown-unlit +default-extensions: + - NoImplicitPrelude + library: exposed-modules: - Introduction @@ -14,4 +17,6 @@ library: - base >= 4.9 && < 5 - protolude - graphql-api + - random - markdown-unlit >= 0.4 + - aeson diff --git a/docs/source/tutorial/tutorial.cabal b/docs/source/tutorial/tutorial.cabal index 9038e94..6f3ded8 100644 --- a/docs/source/tutorial/tutorial.cabal +++ b/docs/source/tutorial/tutorial.cabal @@ -1,4 +1,4 @@ --- This file has been generated from package.yaml by hpack version 0.14.1. +-- This file has been generated from package.yaml by hpack version 0.15.0. -- -- see: https://github.com/sol/hpack @@ -12,6 +12,7 @@ build-type: Simple cabal-version: >= 1.10 library + default-extensions: NoImplicitPrelude exposed-modules: Introduction other-modules: @@ -20,6 +21,8 @@ library base >= 4.9 && < 5 , protolude , graphql-api + , random , markdown-unlit >= 0.4 + , aeson default-language: Haskell2010 ghc-options: -Wall -pgmL markdown-unlit diff --git a/src/GraphQL/Internal/Output.hs b/src/GraphQL/Internal/Output.hs index 59734fc..c49eded 100644 --- a/src/GraphQL/Internal/Output.hs +++ b/src/GraphQL/Internal/Output.hs @@ -11,6 +11,7 @@ module GraphQL.Internal.Output ) where import Protolude hiding (Location, Map) +import Data.Aeson (ToJSON(..)) import Data.List.NonEmpty (NonEmpty(..)) import GraphQL.Value ( Object @@ -75,6 +76,9 @@ instance ToValue Response where ,("errors", toValue e) ] +instance ToJSON Response where + toJSON = toJSON . toValue + type Errors = NonEmpty Error data Error = Error Text [Location] deriving (Eq, Ord, Show)