Write documentation out of the source tree
In a Wiki.
This commit is contained in:
parent
6e8d8a34a1
commit
afcf9aaa14
@ -1,31 +0,0 @@
|
|||||||
# Contributing guidelines
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
To ensure all code changes adhere to existing code quality standards, some
|
|
||||||
automatic checks can be run locally.
|
|
||||||
|
|
||||||
Ensure that the code builds without warnings and passes the tests:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
stack test --pedantic
|
|
||||||
```
|
|
||||||
|
|
||||||
And also run the linter on your code:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
stack build hlint
|
|
||||||
stack exec hlint -- src tests
|
|
||||||
```
|
|
||||||
|
|
||||||
Build the documentation and check if you get any warnings:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
stack haddock
|
|
||||||
```
|
|
||||||
|
|
||||||
Validate that literate Haskell (tutorials) files compile without any warnings:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
stack ghc -- -Wall -fno-code docs/tutorial/*.lhs
|
|
||||||
```
|
|
103
README.md
103
README.md
@ -1,5 +1,8 @@
|
|||||||
# GraphQL implementation in Haskell
|
# GraphQL implementation in Haskell
|
||||||
|
|
||||||
|
[![Simple Haskell](https://www.simplehaskell.org/badges/badge.svg)](https://www.simplehaskell.org)
|
||||||
|
[![CI/CD](https://img.shields.io/badge/CI-CD-brightgreen)](https://build.caraus.tech/go/pipelines)
|
||||||
|
|
||||||
This implementation is relatively low-level by design, it doesn't provide any
|
This implementation is relatively low-level by design, it doesn't provide any
|
||||||
mappings between the GraphQL types and Haskell's type system and avoids
|
mappings between the GraphQL types and Haskell's type system and avoids
|
||||||
compile-time magic. It focuses on flexibility instead, so other solutions can
|
compile-time magic. It focuses on flexibility instead, so other solutions can
|
||||||
@ -29,103 +32,3 @@ API documentation is available through
|
|||||||
|
|
||||||
Further documentation will be made available in the
|
Further documentation will be made available in the
|
||||||
[Wiki](https://www.caraus.tech/projects/pub-graphql/wiki).
|
[Wiki](https://www.caraus.tech/projects/pub-graphql/wiki).
|
||||||
|
|
||||||
### Getting started
|
|
||||||
|
|
||||||
We start with a simple GraphQL API that provides us with some famous and less
|
|
||||||
famous cites.
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
"""
|
|
||||||
Root Query type.
|
|
||||||
"""
|
|
||||||
type Query {
|
|
||||||
"""
|
|
||||||
Provides a cite.
|
|
||||||
"""
|
|
||||||
cite: String!
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This is called a GraphQL schema, it defines all queries supported by the API.
|
|
||||||
`Query` is the root query type. Every GraphQL API should define a query type.
|
|
||||||
|
|
||||||
`Query` has a single field `cite` that returns a `String`. The `!` after the
|
|
||||||
type denotes that the returned value cannot be `Null`. GraphQL fields are
|
|
||||||
nullable by default.
|
|
||||||
|
|
||||||
To be able to work with this schema, we are going to implement it in Haskell.
|
|
||||||
|
|
||||||
```haskell
|
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
|
|
||||||
import qualified Data.Aeson as Aeson
|
|
||||||
import qualified Data.ByteString.Lazy.Char8 as ByteString.Lazy.Char8
|
|
||||||
import qualified Data.HashMap.Strict as HashMap
|
|
||||||
import Language.GraphQL
|
|
||||||
import Language.GraphQL.Type
|
|
||||||
import qualified Language.GraphQL.Type.Out as Out
|
|
||||||
|
|
||||||
-- GraphQL supports 3 kinds of operations: queries, mutations and subscriptions.
|
|
||||||
-- Our first schema supports only queries.
|
|
||||||
citeSchema :: Schema IO
|
|
||||||
citeSchema = schema queryType Nothing Nothing mempty
|
|
||||||
|
|
||||||
-- GraphQL distinguishes between input and output types. Input types are field
|
|
||||||
-- argument types and they are defined in Language.GraphQL.Type.In. Output types
|
|
||||||
-- are result types, they are defined in Language.GraphQL.Type.Out. Root types
|
|
||||||
-- are always object types.
|
|
||||||
--
|
|
||||||
-- Here we define a type "Query". The second argument is an optional
|
|
||||||
-- description, the third one is the list of interfaces implemented by the
|
|
||||||
-- object type. The last argument is a field map. Keys are field names, values
|
|
||||||
-- are field definitions and resolvers. Resolvers are the functions, where the
|
|
||||||
-- actual logic lives, they return values for the respective fields.
|
|
||||||
queryType :: Out.ObjectType IO
|
|
||||||
queryType = Out.ObjectType "Query" (Just "Root Query type.") []
|
|
||||||
$ HashMap.singleton "cite" citeResolver
|
|
||||||
where
|
|
||||||
-- 'ValueResolver' is a 'Resolver' data constructor, it combines a field
|
|
||||||
-- definition with its resolver function. This function resolves a value for
|
|
||||||
-- a field (as opposed to the 'EventStreamResolver' used by subscriptions).
|
|
||||||
-- Our resolver just returns a constant value.
|
|
||||||
citeResolver = ValueResolver citeField
|
|
||||||
$ pure "Piscis primum a capite foetat"
|
|
||||||
|
|
||||||
-- The first argument is an optional field description. The second one is
|
|
||||||
-- the field type and the third one is for arguments (we have none in this
|
|
||||||
-- example).
|
|
||||||
--
|
|
||||||
-- GraphQL has named and wrapping types. String is a scalar, named type.
|
|
||||||
-- Named types are nullable by default. To make our "cite" field
|
|
||||||
-- non-nullable, we wrap it in the wrapping type, Non-Null.
|
|
||||||
citeField = Out.Field
|
|
||||||
(Just "Provides a cite.") (Out.NonNullScalarType string) HashMap.empty
|
|
||||||
|
|
||||||
-- Now we can execute a query. Since our schema defines only one field,
|
|
||||||
-- everything we can do is to ask to resolve it and give back the result.
|
|
||||||
-- Since subscriptions don't return plain values, the 'graphql' function returns
|
|
||||||
-- an 'Either'. 'Left' is for subscriptions, 'Right' is for queries and
|
|
||||||
-- mutations.
|
|
||||||
main :: IO ()
|
|
||||||
main = do
|
|
||||||
Right result <- graphql citeSchema "{ cite }"
|
|
||||||
ByteString.Lazy.Char8.putStrLn $ Aeson.encode result
|
|
||||||
```
|
|
||||||
|
|
||||||
Executing this query produces the following JSON:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"cite": "Piscis primum a capite foetat"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Contact
|
|
||||||
|
|
||||||
Suggestions, patches and bug reports are welcome.
|
|
||||||
|
|
||||||
Should you have questions on usage, please open an issue and ask – this helps
|
|
||||||
to write useful documentation.
|
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
|
|
||||||
import qualified Data.Aeson as Aeson
|
|
||||||
import qualified Data.ByteString.Lazy.Char8 as ByteString.Lazy.Char8
|
|
||||||
import qualified Data.HashMap.Strict as HashMap
|
|
||||||
import Language.GraphQL
|
|
||||||
import Language.GraphQL.Type
|
|
||||||
import qualified Language.GraphQL.Type.Out as Out
|
|
||||||
|
|
||||||
-- GraphQL supports 3 kinds of operations: queries, mutations and subscriptions.
|
|
||||||
-- Our first schema supports only queries.
|
|
||||||
citeSchema :: Schema IO
|
|
||||||
citeSchema = schema queryType
|
|
||||||
|
|
||||||
-- GraphQL distinguishes between input and output types. Input types are field
|
|
||||||
-- argument types and they are defined in Language.GraphQL.Type.In. Output types
|
|
||||||
-- are result types, they are defined in Language.GraphQL.Type.Out. Root types
|
|
||||||
-- are always object types.
|
|
||||||
--
|
|
||||||
-- Here we define a type "Query". The second argument is an optional
|
|
||||||
-- description, the third one is the list of interfaces implemented by the
|
|
||||||
-- object type. The last argument is a field map. Keys are field names, values
|
|
||||||
-- are field definitions and resolvers. Resolvers are the functions, where the
|
|
||||||
-- actual logic lives, they return values for the respective fields.
|
|
||||||
queryType :: Out.ObjectType IO
|
|
||||||
queryType = Out.ObjectType "Query" (Just "Root Query type.") []
|
|
||||||
$ HashMap.singleton "cite" citeResolver
|
|
||||||
where
|
|
||||||
-- 'ValueResolver' is a 'Resolver' data constructor, it combines a field
|
|
||||||
-- definition with its resolver function. This function resolves a value for
|
|
||||||
-- a field (as opposed to the 'EventStreamResolver' used by subscriptions).
|
|
||||||
-- Our resolver just returns a constant value.
|
|
||||||
citeResolver = ValueResolver citeField
|
|
||||||
$ pure "Piscis primum a capite foetat"
|
|
||||||
|
|
||||||
-- The first argument is an optional field description. The second one is
|
|
||||||
-- the field type and the third one is for arguments (we have none in this
|
|
||||||
-- example).
|
|
||||||
--
|
|
||||||
-- GraphQL has named and wrapping types. String is a scalar, named type.
|
|
||||||
-- Named types are nullable by default. To make our "cite" field
|
|
||||||
-- non-nullable, we wrap it in the wrapping type, Non-Null.
|
|
||||||
citeField = Out.Field
|
|
||||||
(Just "Provides a cite.") (Out.NonNullScalarType string) HashMap.empty
|
|
||||||
|
|
||||||
-- Now we can execute a query. Since our schema defines only one field,
|
|
||||||
-- everything we can do is to ask to resolve it and give back the result.
|
|
||||||
-- Since subscriptions don't return plain values, the 'graphql' function returns
|
|
||||||
-- an 'Either'. 'Left' is for subscriptions, 'Right' is for queries and
|
|
||||||
-- mutations.
|
|
||||||
main :: IO ()
|
|
||||||
main = do
|
|
||||||
Right result <- graphql citeSchema "{ cite }"
|
|
||||||
ByteString.Lazy.Char8.putStrLn $ Aeson.encode result
|
|
||||||
|
|
@ -1,149 +0,0 @@
|
|||||||
---
|
|
||||||
title: GraphQL Haskell Tutorial
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
== Getting started ==
|
|
||||||
|
|
||||||
Welcome to GraphQL!
|
|
||||||
|
|
||||||
We have written a small tutorial to help you (and ourselves) understand the
|
|
||||||
graphql package.
|
|
||||||
|
|
||||||
Since this file is a literate haskell file, we start by importing some
|
|
||||||
dependencies.
|
|
||||||
|
|
||||||
> {-# LANGUAGE OverloadedStrings #-}
|
|
||||||
> module Main where
|
|
||||||
>
|
|
||||||
> import Control.Monad.IO.Class (liftIO)
|
|
||||||
> import Data.Aeson (encode)
|
|
||||||
> import Data.ByteString.Lazy.Char8 (putStrLn)
|
|
||||||
> import qualified Data.HashMap.Strict as HashMap
|
|
||||||
> import Data.Text (Text)
|
|
||||||
> import qualified Data.Text as Text
|
|
||||||
> import Data.Time (getCurrentTime)
|
|
||||||
>
|
|
||||||
> import Language.GraphQL
|
|
||||||
> import Language.GraphQL.Type
|
|
||||||
> import qualified Language.GraphQL.Type.Out as Out
|
|
||||||
>
|
|
||||||
> import Prelude hiding (putStrLn)
|
|
||||||
|
|
||||||
|
|
||||||
=== First example ===
|
|
||||||
|
|
||||||
Now, as our first example, we are going to look at the example from
|
|
||||||
[graphql.js](https://github.com/graphql/graphql-js).
|
|
||||||
|
|
||||||
First we build a GraphQL schema.
|
|
||||||
|
|
||||||
> schema1 :: Schema IO
|
|
||||||
> schema1 = schema queryType Nothing Nothing mempty
|
|
||||||
>
|
|
||||||
> queryType :: ObjectType IO
|
|
||||||
> queryType = ObjectType "Query" Nothing []
|
|
||||||
> $ HashMap.singleton "hello"
|
|
||||||
> $ ValueResolver helloField hello
|
|
||||||
>
|
|
||||||
> helloField :: Field IO
|
|
||||||
> helloField = Field Nothing (Out.NamedScalarType string) mempty
|
|
||||||
>
|
|
||||||
> hello :: Resolve IO
|
|
||||||
> hello = pure $ String "it's me"
|
|
||||||
|
|
||||||
This defines a simple schema with one type and one field, that resolves to a
|
|
||||||
fixed value.
|
|
||||||
|
|
||||||
Next we define our query.
|
|
||||||
|
|
||||||
> query1 :: Text
|
|
||||||
> query1 = "{ hello }"
|
|
||||||
|
|
||||||
To run the query, we call the `graphql` with the schema and the query.
|
|
||||||
|
|
||||||
> main1 :: IO ()
|
|
||||||
> main1 = graphql schema1 query1
|
|
||||||
> >>= either (const $ pure ()) (putStrLn . encode)
|
|
||||||
|
|
||||||
This runs the query by fetching the one field defined, returning
|
|
||||||
|
|
||||||
```{"data" : {"hello":"it's me"}}```
|
|
||||||
|
|
||||||
|
|
||||||
=== Monadic actions ===
|
|
||||||
|
|
||||||
For this example, we're going to be using time.
|
|
||||||
|
|
||||||
> schema2 :: Schema IO
|
|
||||||
> schema2 = schema queryType2 Nothing Nothing mempty
|
|
||||||
>
|
|
||||||
> queryType2 :: ObjectType IO
|
|
||||||
> queryType2 = ObjectType "Query" Nothing []
|
|
||||||
> $ HashMap.singleton "time"
|
|
||||||
> $ ValueResolver timeField time
|
|
||||||
>
|
|
||||||
> timeField :: Field IO
|
|
||||||
> timeField = Field Nothing (Out.NamedScalarType string) mempty
|
|
||||||
>
|
|
||||||
> time :: Resolve IO
|
|
||||||
> time = do
|
|
||||||
> t <- liftIO getCurrentTime
|
|
||||||
> pure $ String $ Text.pack $ show t
|
|
||||||
|
|
||||||
This defines a simple schema with one type and one field, which resolves to the
|
|
||||||
current time.
|
|
||||||
|
|
||||||
Next we define our query.
|
|
||||||
|
|
||||||
> query2 :: Text
|
|
||||||
> query2 = "{ time }"
|
|
||||||
>
|
|
||||||
> main2 :: IO ()
|
|
||||||
> main2 = graphql schema2 query2
|
|
||||||
> >>= either (const $ pure ()) (putStrLn . encode)
|
|
||||||
|
|
||||||
This runs the query, returning the current time
|
|
||||||
|
|
||||||
```{"data": {"time":"2016-03-08 23:28:14.546899 UTC"}}```
|
|
||||||
|
|
||||||
|
|
||||||
=== Combining resolvers ===
|
|
||||||
|
|
||||||
Now that we have two resolvers, we can define a schema which uses them both.
|
|
||||||
|
|
||||||
> schema3 :: Schema IO
|
|
||||||
> schema3 = schema queryType3 Nothing Nothing mempty
|
|
||||||
>
|
|
||||||
> queryType3 :: ObjectType IO
|
|
||||||
> queryType3 = ObjectType "Query" Nothing [] $ HashMap.fromList
|
|
||||||
> [ ("hello", ValueResolver helloField hello)
|
|
||||||
> , ("time", ValueResolver timeField time)
|
|
||||||
> ]
|
|
||||||
>
|
|
||||||
> query3 :: Text
|
|
||||||
> query3 = "query timeAndHello { time hello }"
|
|
||||||
>
|
|
||||||
> main3 :: IO ()
|
|
||||||
> main3 = graphql schema3 query3
|
|
||||||
> >>= either (const $ pure ()) (putStrLn . encode)
|
|
||||||
|
|
||||||
This queries for both time and hello, returning
|
|
||||||
|
|
||||||
```{ "data": {"hello":"it's me","time":"2016-03-08 23:29:11.62108 UTC"}}```
|
|
||||||
|
|
||||||
Notice that we can name our queries, as we did with `timeAndHello`. Since we
|
|
||||||
have only been using single queries, we can use the shorthand `{ time hello }`,
|
|
||||||
as we have been doing in the previous examples.
|
|
||||||
|
|
||||||
In GraphQL there can only be one operation per query.
|
|
||||||
|
|
||||||
|
|
||||||
== Further examples ==
|
|
||||||
|
|
||||||
More examples on queries and a more complex schema can be found in the test
|
|
||||||
directory, in the [Test.StarWars](../../tests/Test/StarWars) module. This
|
|
||||||
includes a more complex schema, and more complex queries.
|
|
||||||
|
|
||||||
> main :: IO ()
|
|
||||||
> main = main1 >> main2 >> main3
|
|
@ -4,7 +4,7 @@ cabal-version: 2.2
|
|||||||
--
|
--
|
||||||
-- see: https://github.com/sol/hpack
|
-- see: https://github.com/sol/hpack
|
||||||
--
|
--
|
||||||
-- hash: 64b8d806f87030d33d1f8d505887d4913689ee48bc6e79b1c24b5226ffd07ee8
|
-- hash: ddb79ddbd13b917f320fff372b4a29b63b6eb0ed113ca732c1d779b4e6a296d8
|
||||||
|
|
||||||
name: graphql
|
name: graphql
|
||||||
version: 0.10.0.0
|
version: 0.10.0.0
|
||||||
@ -25,9 +25,7 @@ license-files: LICENSE,
|
|||||||
build-type: Simple
|
build-type: Simple
|
||||||
extra-source-files:
|
extra-source-files:
|
||||||
CHANGELOG.md
|
CHANGELOG.md
|
||||||
CONTRIBUTING.md
|
|
||||||
README.md
|
README.md
|
||||||
docs/tutorial/tutorial.lhs
|
|
||||||
|
|
||||||
source-repository head
|
source-repository head
|
||||||
type: git
|
type: git
|
||||||
|
@ -23,9 +23,7 @@ license-file:
|
|||||||
- LICENSE.MPL
|
- LICENSE.MPL
|
||||||
extra-source-files:
|
extra-source-files:
|
||||||
- CHANGELOG.md
|
- CHANGELOG.md
|
||||||
- CONTRIBUTING.md
|
|
||||||
- README.md
|
- README.md
|
||||||
- docs/tutorial/tutorial.lhs
|
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
- aeson
|
- aeson
|
||||||
|
Loading…
Reference in New Issue
Block a user