Update docs to remove reference to w parameter (#132)

This commit is contained in:
Alejandro Serrano 2020-03-10 09:45:58 +01:00 committed by GitHub
parent 7dab092b81
commit 23e1d096cc
11 changed files with 81 additions and 71 deletions

View File

@ -3,7 +3,12 @@ options:
url: /
- title: Introduction
url: intro/
nested_options:
- title: For RPC
url: intro-rpc/
- title: For GraphQL
url: intro-graphql/
- title: Schemas
url: schema/
@ -23,11 +28,11 @@ options:
- title: Streams
url: stream/
- title: Integrations
nested_options:
- title: Databases
url: db/
- title: Integrations
nested_options:
- title: Transformers
url: transformer/

View File

@ -8,14 +8,16 @@ permalink: /
Mu-Haskell is a set of packages that help you build both servers and clients for (micro)services. The main goal of Mu-Haskell is to allow you to focus on your domain logic, instead of worrying about format and protocol issues.
* [Introduction]({% link docs/intro.md %})
* Introduction
* [For RPC]({% link docs/intro-rpc.md %})
* [For GraphQL]({% link docs/intro-graphql.md %})
* [Schemas]({% link docs/schema.md %})
* [Serialization formats]({% link docs/serializers.md %}): Protocol Buffers and Avro
* [Registry]({% link docs/registry.md %})
* [Services and servers]({% link docs/rpc.md %})
* [gRPC servers and clients]({% link docs/grpc.md %})
* [Streams]({% link docs/stream.md %})
* [Databases]({% link docs/db.md %}), including resource pools
* Integration with other libraries
* [Databases]({% link docs/db.md %}), including resource pools
* [Using transformers]({% link docs/transformer.md %}): look here for logging
* [WAI Middleware]({% link docs/middleware.md %}): look here for metrics

View File

@ -61,20 +61,20 @@ As we've seen in the rest of the docs, we define our own data types to mirror ou
grpc "PersistentSchema" id "with-persistent.proto"
newtype MPersonRequest = MPersonRequest
{ identifier :: Maybe Int64
{ identifier :: Int64
} deriving (Eq, Show, Ord, Generic)
instance ToSchema Maybe PersistentSchema "PersonRequest" MPersonRequest
instance FromSchema Maybe PersistentSchema "PersonRequest" MPersonRequest
instance ToSchema PersistentSchema "PersonRequest" MPersonRequest
instance FromSchema PersistentSchema "PersonRequest" MPersonRequest
data MPerson = MPerson
{ pid :: Maybe MPersonRequest
, name :: Maybe T.Text
, age :: Maybe Int32
, name :: T.Text
, age :: Int32
} deriving (Eq, Ord, Show, Generic)
instance ToSchema Maybe PersistentSchema "Person" MPerson
instance FromSchema Maybe PersistentSchema "Person" MPerson
instance ToSchema PersistentSchema "Person" MPerson
instance FromSchema PersistentSchema "Person" MPerson
```
Remember that all the magic starts with that first `grpc` line! ✨
@ -116,7 +116,7 @@ type PersonFieldMapping
= '[ "personAge" ':-> "age", "personName" ':-> "name" ]
deriving via (WithEntityNestedId "Person" PersonFieldMapping (Entity Person))
instance ToSchema Maybe PersistentSchema "Person" (Entity Person)
instance ToSchema PersistentSchema "Person" (Entity Person)
```
Have in mind that we still need to define our own custom field mapping, in this case `PersonFieldMapping` so that the deriving via does its job properly.

View File

@ -61,26 +61,25 @@ To call a method, you use the corresponding getter (for those familiar with opti
```haskell
{-# language OverloadedLabels #-}
import Text.Read (readMaybe)
get :: GRpcConnection QuickstartService -> String -> IO ()
get client idPerson = do
let req = record (readMaybe idPerson)
let req = record1 (read idPerson)
putStrLn $ "GET: is there some person with id: " ++ idPerson ++ "?"
res <- (client ^. #getPerson) req
res <- client ^. #getPerson $ req
putStrLn $ "GET: response was: " ++ show res
```
Notice the use of `readMaybe` to convert the strings to the appropiate type in a safe manner! 👆🏼
Notice the use of `read` to convert the strings to the appropiate type. Be careful, though, since that function throws an exception if the string is not a proper number! In a realistic scenario you should use `readMaybe` from `Text.Read` and handle the appropiate cases.
Using this approach you must also use the optics-based interface to the terms. As a quick reminder: you use `record` to build new values, and use `value ^. #field` to access a field. The rest of the methods look as follows:
```haskell
add :: GRpcConnection QuickstartService -> String -> String -> IO ()
add client nm ag = do
let p = record (Nothing, Just $ T.pack nm, readMaybe ag)
let p = record (Nothing, T.pack nm, read ag)
putStrLn $ "ADD: creating new person " ++ nm ++ " with age " ++ ag
res <- (client ^. #newPerson) p
res <- client ^. #newPerson $ p
putStrLn $ "ADD: was creating successful? " ++ show res
watching :: GRpcConnection QuickstartService -> IO ()
@ -123,14 +122,14 @@ After that, let's have a look at an example implementation of the three service
```haskell
get :: Call -> String -> IO ()
get client idPerson = do
let req = MPersonRequest $ readMaybe idPerson
let req = MPersonRequest $ read idPerson
putStrLn $ "GET: is there some person with id: " ++ idPerson ++ "?"
res <- call_getPerson client req
putStrLn $ "GET: response was: " ++ show res
add :: Call -> String -> String -> IO ()
add client nm ag = do
let p = MPerson Nothing (Just $ T.pack nm) (readMaybe ag)
let p = MPerson Nothing (T.pack nm) (read ag)
putStrLn $ "ADD: creating new person " ++ nm ++ " with age " ++ ag
res <- call_newPerson client p
putStrLn $ "ADD: was creating successful? " ++ show res
@ -156,7 +155,7 @@ main = do ...
get :: GrpcClient -> String -> IO ()
get client idPerson = do
let req = MPersonRequest $ readMaybe idPerson
let req = MPersonRequest $ read idPerson
putStrLn $ "GET: is there some person with id: " ++ idPerson ++ "?"
response :: GRpcReply MPerson
<- gRpcCall @Service @"getPerson" client req
@ -168,7 +167,7 @@ Notice that the type signatures of our functions needed to change to receive the
```haskell
add :: GrpcClient -> String -> String -> IO ()
add client nm ag = do
let p = MPerson Nothing (Just $ T.pack nm) (readMaybe ag)
let p = MPerson Nothing (T.pack nm) (read ag)
putStrLn $ "ADD: creating new person " ++ nm ++ " with age " ++ ag
response :: GRpcReply MPersonRequest
<- gRpcCall @Service @"newPerson" client p

View File

@ -0,0 +1,8 @@
---
layout: docs
title: Introduction for GraphQL
permalink: intro-graphql/
---
# Introduction to Mu-Haskell for GraphQL

View File

@ -1,10 +1,10 @@
---
layout: docs
title: Introduction
permalink: intro/
title: Introduction for RPC
permalink: intro-rpc/
---
# Introduction to Mu-Haskell
# Introduction to Mu-Haskell for RPC
Many companies have embraced microservices architectures as the best way to scale up their internal software systems, and separate work across different company divisions and development teams. Microservices architectures also allow teams to turn an idea or bug report into a working feature or fix in production more quickly, in accordance to the agile principles.
@ -15,7 +15,7 @@ However, microservices are not without costs. Every connection between microserv
The main goal of Mu-Haskell is to allow you to focus on your domain logic, instead of worrying about format and protocol issues. To achieve this goal, Mu-Haskell provides two sets of packages:
* `mu-schema` and `mu-rpc` define schemas for data and services, in a format- and protocol-independent way. These schemas are checked at compile-time, so you also gain an additional layer of type-safety.
* `mu-avro`, `mu-protobuf`, `mu-grpc` (and other to come) implement each concrete format and protocol, following the interfaces laid out by the former two. In addition, most of those packages can turn a schema in the corresponding format into the corresponding one in `mu-schema` and `mu-rpc` terms, alleviating the need to duplicate definitions.
* `mu-avro`, `mu-protobuf`, `mu-grpc`, `mu-graphql` (and other to come) implement each concrete format and protocol, following the interfaces laid out by the former two. In addition, most of those packages can turn a schema in the corresponding format into the corresponding one in `mu-schema` and `mu-rpc` terms, alleviating the need to duplicate definitions.
## Quickstart
@ -61,16 +61,16 @@ The aforementioned `.proto` file defines two messages. The corresponding data ty
```haskell
data HelloRequestMessage
= HelloRequestMessage { name :: Maybe T.Text }
= HelloRequestMessage { name :: T.Text }
deriving (Eq, Show, Generic
, ToSchema Maybe TheSchema "HelloRequest"
, FromSchema Maybe TheSchema "HelloRequest")
, ToSchema TheSchema "HelloRequest"
, FromSchema TheSchema "HelloRequest")
data HelloReplyMessage
= HelloReplyMessage { message :: Maybe T.Text }
= HelloReplyMessage { message :: T.Text }
deriving (Eq, Show, Generic
, ToSchema Maybe TheSchema "HelloReply"
, FromSchema Maybe TheSchema "HelloReply")
, ToSchema TheSchema "HelloReply"
, FromSchema TheSchema "HelloReply")
```
These data types should be added to the file `src/Schema.hs`, under the line that starts `grpc ...`. (See the [gRPC page]({% link docs/grpc.md %}) for information about what that line is doing.)
@ -78,7 +78,6 @@ These data types should be added to the file `src/Schema.hs`, under the line tha
You can give the data types and their constructors any name you like. However, keep in mind that:
* The names of the fields must correspond with those in the `.proto` files. Otherwise you have to use a *custom mapping*, which is fully supported by `mu-schema` but requires more code.
* All the fields must be wrapped in `Maybe` since all fields in Protocol Buffers version 3 are **optional by default**.
* The name `TheSchema` refers to a type generated by the `grpc` function, so it must match the first argument to that function.
* The name between quotes in each `deriving` clause defines the message type in the `.proto` file each data type corresponds to.
* To use the automatic-mapping functionality, it is required to also derive `Generic`, don't forget it!
@ -88,8 +87,8 @@ You can give the data types and their constructors any name you like. However, k
As we mentioned above, you may decide to not introduce new Haskell types, at the expense of losing some automatic checks against the current version of the schema. However, you gain access to a set of lenses and optics which can be used to inspect the values. In the Mu jargon, values from a schema which are not Haskell types are called *terms*, and we usually define type synonyms for each of them.
```haskell
type HelloRequestMessage' = Term Maybe TheSchema (TheSchema :/: "HelloRequest")
type HelloReplyMessage' = Term Maybe TheSchema (TheSchema :/: "HelloReply")
type HelloRequestMessage' = Term TheSchema (TheSchema :/: "HelloRequest")
type HelloReplyMessage' = Term TheSchema (TheSchema :/: "HelloReply")
```
The arguments to `Term` closely correspond to those in `FromSchema` and `ToSchema` described above.
@ -104,7 +103,7 @@ Open the `src/Main.hs` file. The contents are quite small right now: a `main` fu
main :: IO ()
main = runGRpcApp msgProtoBuf 8080 server
server :: (MonadServer m) => ServerT Maybe Service m _
server :: (MonadServer m) => ServerT Service m _
server = Server H0
```
@ -113,7 +112,7 @@ The simplest way to provide an implementation for a service is to define one fun
```haskell
sayHello :: (MonadServer m) => HelloRequestMessage -> m HelloReplyMessage
sayHello (HelloRequestMessage nm)
= return (HelloReplyMessage (("hi, " <>) <$> nm))
= pure $ HelloReplyMessage ("hi, " <> nm)
```
The `MonadServer` portion in the type is mandated by `mu-rpc`; it tells us that in a method we can perform any `IO` actions and additionally throw server errors (for conditions such as *not found*). We do not make use of any of those here, so we simply use `return` with a value. We could even make the definition a bit more polymorphic by replacing `MonadServer` by `Monad`.
@ -125,7 +124,7 @@ Another possibility is to use the `optics`-based API in `Mu.Schema.Optics`. In t
sayHello :: (MonadServer m) => HelloRequestMessage' -> m HelloReplyMessage'
sayHello (HelloRequestMessage nm)
= return $ record (("hi, " <>) <$> (nm ^. #name))
= pure $ record ("hi, " <> nm ^. #name)
```
How does `server` know that `sayHello` (any of the two versions) is part of the implementation of the service? We have to tell it, by adding `sayHello` to the list of methods. Unfortunately, we cannot use a normal list, so we use `(:<|>:)` to join them, and `H0` to finish it.

View File

@ -43,7 +43,7 @@ This is everything you need to start using gRPC services and clients in Haskell!
### Looking at the resulting code
In order to use the library proficiently, we should look a bit at the code generated in the previous sample. A type-level description of the messages is put into the type `QuickstartSchema`. However, there is some code you still have to write by hand, namely the Haskell type which correspond to that schema. Using `mu-schema` facilities, this amounts to declaring a bunch of data types and including `deriving (Generic, ToSchema Maybe <SchemaName> "<MessageType>", FromSchema Maybe <SchemaName> "<MessageType>")` at the end of each of them.
In order to use the library proficiently, we should look a bit at the code generated in the previous sample. A type-level description of the messages is put into the type `QuickstartSchema`. However, there is some code you still have to write by hand, namely the Haskell type which correspond to that schema. Using `mu-schema` facilities, this amounts to declaring a bunch of data types and including `deriving (Generic, ToSchema <SchemaName> "<MessageType>", FromSchema <SchemaName> "<MessageType>")` at the end of each of them.
```haskell
{-# language PolyKinds, DataKinds, TypeFamilies #-}
@ -67,15 +67,15 @@ type instance AnnotatedSchema ProtoBufAnnotation QuickstartSchema
-- TO BE WRITTEN
newtype HelloRequest
= HelloRequest { name :: Maybe T.Text }
= HelloRequest { name :: T.Text }
deriving (Generic
, ToSchema Maybe QuickstartSchema "HelloRequest"
, FromSchema Maybe QuickstartSchema "HelloRequest")
, ToSchema QuickstartSchema "HelloRequest"
, FromSchema QuickstartSchema "HelloRequest")
newtype HelloResponse
= HelloResponse { message :: Maybe T.Text }
= HelloResponse { message :: T.Text }
deriving (Generic
, ToSchema Maybe QuickstartSchema "HelloResponse"
, FromSchema Maybe QuickstartSchema "HelloResponse")
, ToSchema QuickstartSchema "HelloResponse"
, FromSchema QuickstartSchema "HelloResponse")
```
The service declaration looks very similar to a schema declaration, but instead of records and enumerations you define *methods*. Each method has a name, a list of arguments, and a return type.
@ -86,14 +86,15 @@ import Mu.Rpc
-- GENERATED
type QuickstartService
= 'Service "Greeter"
'[ 'Method "SayHello"
'[ 'ArgSingle ('FromSchema QuickstartSchema "HelloRequest") ]
'[ 'Method "SayHello" '[]
'[ 'ArgSingle 'Nothing '[] ('FromSchema QuickstartSchema "HelloRequest") ]
('RetSingle ('FromSchema QuickstartSchema "HelloResponse")) ]
```
In order to support both [Avro IDL](https://avro.apache.org/docs/current/idl.html) and [gRPC](https://grpc.io/), the declaration of the method arguments and return types is a bit fancier than you might expect:
* Each *argument* declares the schema type used for serialization. Furthermore, the argument can be declared as `ArgSingle` (only one value is provided by the client) or `ArgStream` (a stream of values is provided).
* gRPC defines no names for arguments, hence the use of `Nothing` in `ArgSingle`. Other service APIs, like GraphQL, have names on that possitions.
* The *return types* gives the same two choices under the names `RetSingle` or `RetStream`, and additionally supports the declaration of methods which may raise exceptions using `RetThrows`, or methods which do not retun any useful information using `RetNothing`.
Note that depending on the concrete implementation you use to run the server, one or more of these choices may not be available. For example, gRPC only supports one argument and return value, either single or streaming, but not exceptions.
@ -104,7 +105,7 @@ In order to implement the service, you have to define the behavior of each metho
```haskell
sayHello :: (MonadServer m) => HelloRequest -> m HelloResponse
sayHello (HelloRequest nm) = return (HelloResponse ("hi, " <> nm))
sayHello (HelloRequest nm) = pure $ HelloResponse ("hi, " <> nm)
```
Notice the use of `MonadServer` in this case. This gives us the ability to:
@ -119,6 +120,6 @@ Since you can declare more than one method in a service, you need to join them i
```haskell
{-# language PartialTypeSignatures #-}
quickstartServer :: (MonadServer m) => ServerT Maybe QuickstartService m _
quickstartServer :: (MonadServer m) => ServerT QuickstartService m _
quickstartServer = Server (sayHello :<|>: H0)
```

View File

@ -89,15 +89,15 @@ data Address
= Address { postcode :: T.Text
, country :: T.Text }
deriving (Eq, Show, Generic)
deriving (ToSchema Maybe ExampleSchema "address")
deriving (FromSchema Maybe ExampleSchema "address")
deriving (ToSchema ExampleSchema "address")
deriving (FromSchema ExampleSchema "address")
```
Once again, you need to enable some extensions in the compiler (but do not worry, GHC should tell you which ones you need in case you forgot). You first must include `Generic` in the list of automatically-derived classes. Then you *derive* the mapping by using the lines:
```haskell
deriving (ToSchema Maybe YourSchema "yourSchemaType")
deriving (FromSchema Maybe YourSchema "yourSchemaType")
deriving (ToSchema YourSchema "yourSchemaType")
deriving (FromSchema YourSchema "yourSchemaType")
```
## Customizing the mapping
@ -115,7 +115,7 @@ type GenderFieldMapping
data Gender = Male | Female | NonBinary
deriving (Eq, Show, Generic)
deriving (ToSchema f ExampleSchema "gender", FromSchema f ExampleSchema "gender")
deriving (ToSchema ExampleSchema "gender", FromSchema ExampleSchema "gender")
via (CustomFieldMapping "gender" GenderFieldMapping Gender)
```

View File

@ -27,17 +27,15 @@ grpc "Schema" id "defn.proto"
This asks the compiler to load the `defn.proto` file in order to create a series of (Haskell) types. The first one, which is called `"Schema"`, as the first argument shows, includes the definition of all the schema types. Then, for every service defined in the file (since `.proto` files may have more than one), the second function is applied to obtain the name of the corresponding Haskell type. In this case we want to use the same names, so we use the identity function.
When defining the conversion, it is important for `FromSchema` and `ToSchema` to receive `Maybe` as first argument:
```haskell
data PersonMsg
= PersonMsg { name :: Maybe Text, age :: Maybe Int }
= PersonMsg { name :: Text, age :: Int }
deriving ( Eq, Show, Ord, Generic
, ToSchema Maybe Schema "Person"
, FromSchema Maybe Schema "Person" )
, ToSchema Schema "Person"
, FromSchema Schema "Person" )
```
This indicates that every field in the record should we wrapped by a `Maybe` layer. This is required because Protocol Buffers version 3 mandates all fields to be optional. This also means that every field in your Haskell types must be wrapped in that `Maybe` layer.
Protocol Buffers version 3 has complicated rules about when a field may or may not appear in the message. For example it is not possible to distinguish whether the empty string is to be transmitted, or that field is missing. In that case, deserialization from a Protocol Buffers message returns the default value. However, for references to other message we can spot the difference, so those references *must* be wrapped in a `Maybe`.
Finally, when starting a server or client you must provide `msgProtoBuf` as argument. For example, a server is started by running:
@ -58,16 +56,14 @@ avdl "Schema" "Service" "." "defn.avdl"
Each `.avdl` file defines just one protocol, so instead of a function like in the case of Protocol Buffers, the second argument is simply the name of the Haskell type to create. But you might have noticed that there's an additional argument, which in this case is `"."`. The reason for this argument is that `.avdl` files routinely *import* other files. This third argument indicates the *base directory* to search for those files.
The conversion is almost identical to Protocol Buffers too, except that instead of `Maybe` you have to use `Identity`. This is because Avro makes fields *required* by default, so there's no need to have an additional `Maybe` layer. Of course, those fields which are optional (usually specified as a union with `null`) must still use `Maybe`:
The conversion is almost identical to Protocol Buffers too. In Avro fields are required by default, only those fields which are optional (usually specified as a union with `null`) must still use `Maybe`:
```haskell
import Data.Functor.Identity
data PersonMsg
= PersonMsg { name :: Text, age :: Int }
deriving ( Eq, Show, Ord, Generic
, ToSchema Identity Schema "Person"
, FromSchema Identity Schema "Person" )
, ToSchema Schema "Person"
, FromSchema Schema "Person" )
```
Finally, when starting a server or client you must provide `msgAvro` as argument. For example, a server is started by running:

View File

@ -21,8 +21,8 @@ Adding this method to the service definition should be easy, we just need to use
type QuickstartService
= 'Service "Greeter"
'[ 'Method "SayHello" ...
, 'Method "SayManyHellos"
'[ 'ArgStream ('FromSchema QuickstartSchema "HelloRequest")]
, 'Method "SayManyHellos" '[]
'[ 'ArgStream 'Nothing '[] ('FromSchema QuickstartSchema "HelloRequest")]
('RetStream ('FromSchema QuickstartSchema "HelloResponse")) ]
```

View File

@ -27,7 +27,7 @@ sayHello :: (MonadServer m, MonadReader T.Text m)
=> HelloRequest -> m HelloResponse
sayHello (HelloRequest nm) = do
greeting <- ask
return (HelloResponse (greeting <> ", " <> nm))
pure $ HelloResponse (greeting <> ", " <> nm)
```
Unfortunately, the simple way to run a gRPC application no longer works:
@ -53,7 +53,7 @@ sayHello :: (MonadServer m, MonadLogger m)
=> HelloRequest -> m HelloResponse
sayHello (HelloRequest nm) = do
logInfoN "running hi"
return (HelloResponse ("hi, " <> nm))
pure $ HelloResponse ("hi, " <> nm)
```
The most important addition with respect to the [original code](rpc.md) is in the signature. Before we only had `MonadServer m`, now we have an additional `MonadLogger m` there.
@ -73,7 +73,7 @@ sayHello :: (MonadServer m, WithLog env String m)
=> HelloRequest -> m HelloResponse
sayHello (HelloRequest nm) = do
logInfoN "running hi"
return (HelloResponse ("hi, " <> nm))
pure $ HelloResponse ("hi, " <> nm)
```
In this case, the top-level handler is called [`usingLoggerT`](http://hackage.haskell.org/package/co-log/docs/Colog-Monad.html#v:usingLoggerT). Its definition is slightly more involved because `co-log` gives you maximum customization power on your logging, instead of defining a set of predefined logging mechanisms.