Trait Internals
The previous section of the tutorial showcased many traits and middlewares included in WebGear. Occasionally, you may want to extend these traits or implement your own traits. This chapter reveals all the internals of traits to help you do this.
Basics
The traits included in webgear-core
package are not special in anyway; you may define your own traits that are as
first-class as the traits provided by WebGear. We'll use an example to demonstrate this.
Many systems use a special HTTP header X-Request-ID
for distributed tracing. This header value is often included in
logs and telemetry to trace the progress of a request through all the systems involved in processing it.
Implementation
Let us see how to implement a trait to capture the request ID value from HTTP requests.
1. Define a Trait Type
The first step in defining any trait is to define a type representing the trait. Here is the type for our request ID trait:
We don't need anything fancy here because our trait is very simple. If you need any additional configuration associated with your trait, you may let the data constructor accept those as parameters.
2. Define a Trait Attribute
Next we define a type for the attribute value associated with our trait type. This is the value that we get when we extract the request ID successfully from the request.
newtype RequestIDValue = RequestIDValue Text
type instance Attribute RequestID Request = RequestIDValue
We use a newtype wrapper around Text
as we don't assume any further structure to the request ID header.
3. Define Trait Absence
Next we need to handle the case of missing trait attributes. What if the request does not contain a request ID? We'll indicate that as an error.
4. Define Prerequisites
Optionally, a trait can have prerequisites. These are usually other traits that you want to depend on. In this case,
we'll use OptionalRequestHeader
as a prerequisite because the request ID is derived from a request header.
type RequestIDHeader = RequestHeader Optional Lenient "X-Request-ID" Text
type instance Prerequisite RequestID ts = HasTrait RequestIDHeader ts
You may set this type instance to ()
if the trait does not have any prerequisites.
5. Implement Get Instances
Now we can implement instances of Get
type class for each type of handler arrows that we want to support.
instance Monad m => Get (ServerHandler m) RequestID where
getTrait ::
HasTrait RequestIDHeader ts =>
RequestID ->
ServerHandler m (Request `With` ts) (Either RequestIDMissing RequestIDValue)
getTrait RequestID = proc request -> do
let hdr = pick @RequestIDHeader $ from request
case hdr of
Just (Right val) -> returnA -< Right (RequestIDValue val)
Just (Left _) -> returnA -< Left RequestIDMissing
Nothing -> returnA -< Left RequestIDMissing
Notice how the implementation uses the prerequisite to get the header value. The getTrait
method returns an Either
value - the Left
value must be of the Absence
type and the Right
value must be of the Attribute
type.
You can also define additional Get
type class instances - for example, define an OpenApiHandler
instance
if you want to include this trait in the OpenAPI documentation.
6. Implement a Middleware
It is a good practice to define one or more middlewares for your trait. They make it convenient to use the trait in API handlers. Here is a middleware for the request ID trait.
requestID ::
(Get h RequestID, ArrowChoice h, HasTrait RequestIDHeader ts) =>
h (Request `With` ts, RequestIDMissing) Response ->
Middleware h ts (RequestID : ts)
requestID errorHandler nextHandler =
proc request -> do
result <- probe RequestID -< request
case result of
Left e -> errorHandler -< (request, e)
Right request' -> nextHandler -< request'
The probe
arrow makes use of the getTrait
implementation above. If getTrait
fails with an error,
probe too will fail with a Left
value. But if getTrait
succeeds, probe
will return a request value
with the additional RequestID
trait attached to it. In other words, the type of request'
in the above
middleware is Request `With` (RequestID : ts)
.
As with other handlers we've seen so far, this middleware is also polymorphic in the type variable h
. It will use
the appropriate handler arrow as required - ServerHandler
, OpenApiHandler
, etc.