From 966ea85214d6bff5734bbc2655296fb2635d1f29 Mon Sep 17 00:00:00 2001 From: Anton Kholomiov Date: Sun, 5 Nov 2023 12:22:09 +0300 Subject: [PATCH] Update docs --- docs/src/00-foreword.md | 26 ++++++++-------- docs/src/01-hello-world.md | 47 +++++++++++++++++++++-------- docs/src/02-request-anatomy.md | 8 ++--- docs/src/03-response-anatomy.md | 38 ++++++++++++++++------- docs/src/04-other-monads.md | 53 +++++++++++++++++++++++++-------- docs/src/05-plugin.md | 18 ++++++----- docs/src/06-json-api-example.md | 12 ++++---- docs/src/06-swagger.md | 9 +++--- 8 files changed, 141 insertions(+), 70 deletions(-) diff --git a/docs/src/00-foreword.md b/docs/src/00-foreword.md index 9cb8464..3ade3a3 100644 --- a/docs/src/00-foreword.md +++ b/docs/src/00-foreword.md @@ -1,26 +1,21 @@ # Mig by example -Mig is a lightweight and easy to use library to build servers in Haskell. -It is sort of servant for Simple/Boring Haskell. +Mig is a lightweight and easy to use library to build HTTP servers and clients in Haskell. +It is kind of servant for Simple/Boring Haskell. This book is an example driven guide to the library. +The name `mig` (pronounced as meeg) is a russian word for "instant moment". -The main features are: +The main features of the mig library are: * lightweight library - * easy to use. It has simple design on purpose - * expressive DSL to compose servers - * type-safe route handlers and conversions - * handlers are encoded with generic Haskell functions - * built on top of WAI and warp server libraries. - * provides Swagger to your server with one-line of code - * relies on standard classes to compose servers. The server is a monoid +* we can build HTTP-clients from the server definition Example of hello world server: @@ -96,9 +91,9 @@ But it is akin to servant in usage of type-safe conversions and type-level safet ### servant -The mig uses the same ideas of type-safe handlers which a re based on generic Haskell functions. +The mig uses the same ideas of type-safe handlers which are based on generic Haskell functions. The main difference is that in servant the whole server is described as type. -Which leads to type-safety and ability to derive API schema, from the type. +Which leads to type-safety and ability to derive API schema from the type. But downside of it is fancy big types and very advanced concepts that user needs to know in order to use the library. Also one drawback to me is when things go wrong and you get @@ -115,8 +110,8 @@ Using type-level description of the routes provide the same benefits as in serva * safe type check of the conversions of low level request and response elements * usage of generic Haskell functions as handlers - * declarative design of the servers +* composition of servers from small sub-servers In the mig API is a value that is derived from the server at run-time. It allows us to build clients and OpenApi swagger too. @@ -127,9 +122,12 @@ something more simple. ### scotty The scotty is also in domain of simple, easy to use solutions. -so why did I wrote mig and haven't used the scotty instead? +So why did I wrote mig and haven't used the scotty instead? Scotty features more imperative approach where you write handlers as expression for Scotty library monad. But it does not looks so well as in servant's case to me. It is harder to assemble servers from parts. And I really like the idea of type-safe conversions of various parts of request and response. +So the scotty is simple enough but for me it lacks some servant features +such as composability of the servers (nice tree structure of the API) +and type-safe conversions of various parts of request and response. diff --git a/docs/src/01-hello-world.md b/docs/src/01-hello-world.md index d6639fb..b2de61d 100644 --- a/docs/src/01-hello-world.md +++ b/docs/src/01-hello-world.md @@ -2,7 +2,7 @@ Let's build hello world application. We are going to build simple JSON API server with single route which replies -with constant text to request +with constant text to request. We have installed the library `mig-server`. Let's import the main module. It brings into the scope all main functions of the library: @@ -24,6 +24,9 @@ hello = undefined ``` So we serve single route with path `"api/v1/hello"`. +This example relies on extension `OverloadedStrings` to convert +string literals to values of `Path` type. Usually I add it in the cabal file of +the project. Let's cover the types first. ### The server type @@ -36,7 +39,8 @@ newtype Server m = Server (Api (Route m)) ``` The `Api` type is a value to describe the API schema and `Route` contains -useful info on the type of the route (method, description of the inputs and outputs). +useful info on the type of the route (method, description of the inputs and outputs) +and how to run the handler function. The server is parametrized by some monad type. For this example we use `IO`-monad. It means that all our handlers are going to return `IO`-values. @@ -45,7 +49,7 @@ It means that all our handlers are going to return `IO`-values. To bind path "api/v1/hello" to handler `hello` we use function `(/.)`. Let's look at it's type signature: ```haskell -(/.) :: (ToServer a) => Path -> a -> Server (MonadOf a) +(/.) :: ToServer a => Path -> a -> Server (MonadOf a) ``` It expects the `Path` which has instance of class `IsString` that is why we can @@ -56,7 +60,7 @@ We have special class called `ToServer` which can convert many different types t The output type is a bit tricky: `Server (MonadOf a)`. The `MonadOf` is a type function which can extract `m` from `(Server m)`. Or for example it can extract `m` from the function `request -> m response`. -So the `MonadOf` is a way to get underlying server monad from any value. +So the `MonadOf` is a way to get underlying server monad from any type. Let's be more specific and study our example. The type of the handler is `Get IO (Resp Text)` @@ -77,8 +81,8 @@ hello :: Get IO (Resp Json Text) | | | | | | | | | +-- response body converted to byte string | | | | - | | | +---- codec to convert it - | | | (the media-type route uses for response body) + | | | +---- codec to convert result to response body + | | | (the media-type which the route uses for response body) | | | | | +---- type of response which holds HTTP-response info with result | | @@ -99,7 +103,7 @@ The type `Send` is just a wrapper on top of monadic value: newtype Send method m a = Send (m a) ``` -It encodes HTTP-method on type level. This is useful to aggregate value for API-schema of our server. +It encodes HTTP-method on type level as so called phantom type. This is useful to aggregate value for API-schema of our server. We have type synonyms for all HTTP-methods (`Get`, `Post`, `Put` etc). It's interesting to know that library mig does not use any custom monads for operation. @@ -145,7 +149,7 @@ Let's complete the example and define a handler which returns static text: ```haskell hello :: Get IO (Resp Json) -hello = pure $ ok "Hello World!" +hello = Send $ pure $ ok "Hello World!" ``` We have several wrappers here: @@ -154,6 +158,14 @@ We have several wrappers here: * `pure` - converts pure value to IO-based value * `Send` - send converts monadic value to server. It adds information on HTTP-method of the return type. +As `Send` is also monad if `m` is a monad we can write this definition +a bit shorter and omit the `Send` constructor: + +```haskell +hello :: Get IO (Resp Json) +hello = pure $ ok "Hello World!" +``` + ### Run a server Let's run the server with warp. For that we define the `main` function for our application: @@ -312,6 +324,17 @@ server = ] ``` +Also for example with paths for alternatives in the list we can omit `toServer` too: + +```haskell +server = + "api/v1" /. + [ "hello" /. hello + , "bye" /. bye + ] +``` + + ### The path type Let's discuss the `Path` type. @@ -331,8 +354,8 @@ data PathItem ``` The static path item is a rigid entity with exact match to string. -We used it in all our examples so far. -but capture is wild-card that is going to be used as input to the handler. +We have used it in all our examples so far. +But capture is wild-card which is going to be used as input to the handler. To construct only rigid paths we can use strings: @@ -341,13 +364,13 @@ To construct only rigid paths we can use strings: "foo/bar" ``` -To get captures we use `*`-wildcard: +To specify captures we use `*`-wildcard: ``` api/v2/*/get ``` -In the star request captures any text. There might be as many stars +In the star mark the request captures any text. There might be as many stars in the path as you wish. But they should be supported by the handler. We will touch upon that later. diff --git a/docs/src/02-request-anatomy.md b/docs/src/02-request-anatomy.md index 0be5066..1404d30 100644 --- a/docs/src/02-request-anatomy.md +++ b/docs/src/02-request-anatomy.md @@ -60,7 +60,7 @@ We have several types of inputs in HTTP: right into it: `api/get/route/someCaptureValueA/someCaptureValueB` * header parameters. They are in HTTP-request headers. For example header that - reports media-type of the request body: "Content-Type: application/json" + reports media-type of the request body: `"Content-Type: application/json"` * request body. It is a value packed into HTTP-request. It can be JSON or text or raw string or XML. All sorts of things can be used as request bodies. @@ -377,7 +377,7 @@ Making `curl` request can quickly become hard to manage as our servers become more complicated. There is OpenAPI standard that defines how to describe HTTP-server API. Also it provides Swagger. It is a tool to make it easy to check how server behaves. -It provides an HTTP-client for the server which allows us to +It provides an HTTP-client for the server usable from the browser as plain web-page which allows us to query server routes. Let's add a swagger to our server. Just add this line: @@ -400,7 +400,7 @@ We can add swagger to any server with function: withSwagger :: SwaggerConfig m -> Server m -> Server m ``` -We will study the `ServerConfig` in details in one of the next chapters +We will study the `SwaggerConfig` in details in one of the next chapters but for now the default value which is set with `def` from library `data-default` is fine. @@ -410,7 +410,7 @@ We can look at the request and response data with tracing functions which come from library `mig-extra` from the module `Mig.Extra.Plugin.Trace`: ```haskell -data Verbosity = V0 | V1 | V2 | V3 +data Verbosity = V0 | V1 | V2 | V3 -- log http requests and responses logHttp :: Verbosity -> Plugin m diff --git a/docs/src/03-response-anatomy.md b/docs/src/03-response-anatomy.md index 76ce21a..fcf2e64 100644 --- a/docs/src/03-response-anatomy.md +++ b/docs/src/03-response-anatomy.md @@ -54,7 +54,7 @@ type than the type of the result. ### Response type class `IsResp` -To unify the output we have special type class called `IsResp` for +To unify the output for both cases of `Resp` and `RespOr` we have special type class called `IsResp` for all types which can be converted to low-level HTTP-response type `Response`. Let's study this type class. @@ -64,6 +64,7 @@ It has two associated types for the type of the body (`RespBody`) and type of th class IsResp a where type RespBody a :: Type type RespError a :: Type + type RespMedia a :: Type ``` We can return successful result with method `ok`: @@ -80,7 +81,7 @@ When things go bad we can report error with method `bad`: bad :: Status -> RespError a -> a ``` -Sometimes at rare cases we do not what to return any content from response. +Sometimes we do not want to return any content from response. We can just report error status and leave the body empty: ```haskell @@ -103,6 +104,14 @@ we would like set it explicitly. For that we have the method: setMedia :: MediaType -> a -> a ``` +Also we can set response status with function: + +```haskell + -- | Set the response status + setStatus :: Status -> a -> a +``` + + Also the core of the class is the method to convert value to low-level response: ```haskell @@ -112,7 +121,7 @@ Also the core of the class is the method to convert value to low-level response: Both `Resp` and `RespOr` are instances of `IsResp` class and we can `Send` as HTTP-response anything which has instance of `IsResp`. -For now there are only three types. The third one is the low-level `Response`. +For now there are only three types. The third one instance is the low-level `Response` type. ## Examples @@ -124,7 +133,7 @@ and we use `RespOr` if handler can produce and error. We already have seen many usages of `Resp` type. Let's define something that can produce an error. Let's define server that calculates square root of the value. For negative numbers it is not defined in the -realm of real numbers. So let's define the handler that use `RespOr` type: +domain of real numbers. So let's define the handler that use `RespOr` type: ```haskell import Mig.Json.IO @@ -188,7 +197,7 @@ for successful response and all functions that need the status take it as argume ### How it works with server definition -How we can use both of the types as responses: `Resp` and `RespOr`. +How can we use both of the types as responses: `Resp` and `RespOr`? Recall that `/.` function is overloaded by the second argument and we have a rule for `ToServer` class that: @@ -207,17 +216,26 @@ We have learned that there are only tow types to return from server handler: The need for the second type is to have different type of the error and different for the result. If both error and result have the same type then we can use `Resp`. This is common case for HTML servers when we -return HTML-page as result. In case of error we would like to show the page too -as in case of success. The difference would be in the HTTP-status of the response. +return HTML-page as a result. In the case of error we would like to show the page too +as in the case of success. The difference would be in the HTTP-status of the response. And this goes well with `IsResp` class as for `Resp media a` error type `RespError` equals to `a` as the value for `RespBody` too. - Also we have learned various methods of the `IsResp` class and how they can be useful in server definitions. -With this chapter we have covered both requests and responses and which types the can -have. See the source code [`RouteArgs`](https://github.com/anton-k/mig/blob/main/examples/mig-example-apps/RouteArgs/Main.hs) +See the source code [`RouteArgs`](https://github.com/anton-k/mig/blob/main/examples/mig-example-apps/RouteArgs/Main.hs) for examples on the topic that we have just studied. +With this chapter we have covered both requests and responses and which types the can +have. +It covers all basics of the mig library. You are now well equipped to build +HTTP-servers in Haskell. The rest of the tutorial covers more advanced features of the library: + +* how to use custom monads. So far we used only plain `IO`-monad +* how to use plugins/middlewares to add common procedures to all handlers of the server +* how to create HTTP-clients from servers +* description of two more substantial examples + * JSON API application for weather forecast + * HTML example for blogpost site diff --git a/docs/src/04-other-monads.md b/docs/src/04-other-monads.md index 20e2910..050b9a6 100644 --- a/docs/src/04-other-monads.md +++ b/docs/src/04-other-monads.md @@ -10,6 +10,18 @@ only three types of monads are supported for `Servers`: So the library is limited in monad choice but all of the cases can cover everything you need from the server. + +Also we can use any monad which is convertible to `IO` with function: + +```haskell +hoistServer :: forall a . (m a -> n a) -> Server m -> Server n +``` + +The reason why we would like to convert to `IO` because warp server +convertion function `runServer` works only for the type `Server IO`. +So we can use any monad but we would like to convert to `IO` at the very and +to be able to run our server with warp. + I personally prefer to just use `IO` and pass environment around to handlers. This process can be automated with `ReaderT` monad. Let's study how to use `ReaderT` with the server. @@ -89,7 +101,7 @@ main :: IO () main = do env <- initEnv putStrLn ("The counter server listens on port: " <> show port) - runServer port . withSwagger def =<< renderServer server env + runServer port $ withSwagger def $ renderServer server env where port = 8085 @@ -99,18 +111,12 @@ server :: Server App Here we also add the swagger to the server for easy testing and trying things out with swagger. -Note that we use bind operator: - -```haskell - runServer port =<< renderServer server env -``` - ### Server with Reader monad Our server has two routes: -* get - to query current state -* put - to add some integer to the state +* `get` - to query current state +* `put` - to add some integer to the state ```haskell server :: Server App @@ -144,7 +150,7 @@ Let's define the `put` handler: ```haskell -- | Put handler. It logs the call and updates -- the state with integer which is read from URL -handlePut :: Capture "arg" Int -> Get App (Resp ()) +handlePut :: Capture "arg" Int -> Post App (Resp ()) handlePut (Capture val) = Send $ do logInfo $ "Call put with: " <> show val ref <- asks (.current) @@ -156,10 +162,33 @@ So we have completed the definition and we can run the app and try it out. You can find the complete code of the example in the [`mig` repo](https://github.com/anton-k/mig/blob/main/examples/mig-example-apps/Counter/Main.hs). +## Using custom monad + +We have studied how to use `ReaderT IO` and `newtype`-wrappers on top of it +as monads for our server. To use any other monad we need to have the function: + +```haskell +runAsIO :: MyMonad a -> IO a +``` + +For custom monad `MyMonad`. If there is such a function we can use function: + +```haskell +hoistServer :: forall a . (m a -> n a) -> Server m -> Server n +``` + +Prior to call to `runServer` and run the server which is based on our custom monad: + +```haskell +main :: IO () +main = runServer 8085 (hoistServer runAsIO server) + +server :: Server MyMonad +server = ... +``` + ## Summary In this chapter we have learned how to use Reader-monad with `mig` library. We can define our custom wrapper for `ReaderT+IO` and derive instance of `HasServer` and we are ready to go. - - diff --git a/docs/src/05-plugin.md b/docs/src/05-plugin.md index 7840bd2..d646ca4 100644 --- a/docs/src/05-plugin.md +++ b/docs/src/05-plugin.md @@ -1,6 +1,8 @@ # Plugins A plugin is a transformation which is applied to all routes in the server. +Also often it is called a middleware. But here we use a bit shorter name for it +and call it a `Plugin`. It is a pair of functions which transform API-description and server function: ```haskell @@ -59,24 +61,24 @@ Let's imagine that we have a function logInfo :: Text -> IO () ``` -We can query the path with `PathInfo` `newtype`: +We can query the path with `FullPathInfo` `newtype`: ```haskell -newtype PathInfo = PathInfo [Text] +newtype FullPathInfo = FullPathInfo Text ``` And we have a rule for `ToPlugin` class: -> if `f` is `ToPlugin` then `(PathInfo -> ToPlugin f)` is `ToPlugin` +> if `f` is `ToPlugin` then `(FullPathInfo -> ToPlugin f)` is `ToPlugin` So we can create a plugin function: ```haskell logRoutes :: Plugin IO -logRoutes = toPlugin $ \(PathInfo pathItems) -> prependServerAction $ do +logRoutes = toPlugin $ \(FullPathInfo path) -> prependServerAction $ do now <- getCurrentTime logInfo $ mconcat - [ "Call route: ", Text.intercalata "/" pathItems + [ "Call route: ", path , " at ", Text.pack (show now) ] ``` @@ -127,11 +129,11 @@ route is not going to be performed if connection is insecure. Let's use this schema for authorization to site. There is a route that provides authorized users with session tokens. -User can pass credentials as request body over secure connection +A user can pass credentials as request body over secure connection and get session token in response which is valid for some time. -With that token user can access the rest of the application. -User can pass token as special header. And we check in the application +With that token the user can access the rest of the application. +The user can pass token as a special header. And we check in the application that token is valid. Imagine that we have a type for a session token: diff --git a/docs/src/06-json-api-example.md b/docs/src/06-json-api-example.md index 11109db..930c967 100644 --- a/docs/src/06-json-api-example.md +++ b/docs/src/06-json-api-example.md @@ -4,12 +4,12 @@ We have learned all we need to know about `mig` to be able to build something co Let's build a weather forecast application. The app has registered users which can request authorization tokens. With that token users can request for weather in specific city and on specific time and also they can update the weather data. -For simplicity we omit user registration and defining roles for the user. +For simplicity we omit user registration and defining roles for the users. ## Domain for our application Let's define main types for our application in the module `Types.hs`. -We will import `Mig.Json.IO` to bring in scope some classes and types +We will import `Mig.Json.IO` to bring in the scope some classes and types common for HTTP-servers: ```haskell @@ -35,7 +35,7 @@ newtype AuthToken = AuthToken Text (ToJSON, FromJSON, FromHttpApiData, Eq, Ord, Show, ToParamSchema, ToSchema) ``` -We need instances to pass the data over HTTP wires. +We need the instances to pass the data over HTTP wires. ### Domain of weather @@ -86,7 +86,7 @@ That is our domain for the weather application. ## Lets define a server -We are going to build JSON HTTP application. For that we will use module `Mig.Json.IO` +We are going to build JSON HTTP application. For that we will use the module `Mig.Json.IO` which provides handy types specified to our domain. We expect our application to have shared context `Env` which we pass to all handlers. @@ -399,7 +399,7 @@ So this is all we need to start the server. For the purpose of the example we will create a mock application. A bit more detailed implementation is in the source code of the `mig` library. -See example `JsonApi`. +See example [`JsonApi`](https://github.com/anton-k/mig/tree/main/examples/mig-example-apps/JsonApi). ## Mock application @@ -479,4 +479,4 @@ You can find the complete code of the example in the [`mig` repo](https://github ## Summary In this chapter we have defined a more substantial example of JSON HTTP application -and saw how we can apply various concepts in practice. +and applied various concepts in practice. diff --git a/docs/src/06-swagger.md b/docs/src/06-swagger.md index 7a21e4e..f54e7ee 100644 --- a/docs/src/06-swagger.md +++ b/docs/src/06-swagger.md @@ -2,7 +2,7 @@ The Swagger is a powerful tool to try out your servers. It provides easy to use Web UI to call routes in the server. -We already saw know how to augment server with swagger. +We already have seen how to augment server with swagger. It is just a line of code: ```haskell @@ -106,9 +106,10 @@ We can apply those functions at definition of the route. Also we can describe the inputs for the route: ```haskell -{-| Appends descriptiton for the inputs. It passes pairs for @(input-name, input-description)@. -special name request-body is dedicated to request body input -nd raw-input is dedicated to raw input +{-| Appends descriptiton for the inputs. It passes pairs for +@(input-name, input-description)@. Special name request-body +is dedicated to request body input nd raw-input is dedicated +to raw input -} describeInputs :: [(Text, Text)] -> Server m -> Server m ```