After finishing the wonderful Haskellbook, the first "real world" project I've started writing is a Haskell API wrapper for Shipwire called Ballast. While doing that, I was following the general architecture of dmjio's Stripe API wrapper. In this post I will try to describe an overview of how both the stripe library and Ballast are built.

Making an easy to use library

A goal of Ballast was to make it easy to use. Here's some example code:

main :: IO () main = do let config <- sandboxEnvConfig result <- shipwire config $ getReceivings - & - ( ExpandReceivingsParam [ ExpandAll ]) return result

A couple of things:

sandboxEnvConfig gets environment variables and is needed for authentication with Shipwire. getReceivings is a function defined in the Client module that corresponds to a specific endpoint and accepts optional query parameters via (-&-) .

You can find similar functions for all the other endpoints.

Simplicity through type families

I wanted to be able to vary return type based on what sort of request I made. To do this I used type families.

shipwire :: ( FromJSON ( ShipwireReturn a)) => ShipwireConfig -> ShipwireRequest a TupleBS8 BSL . ByteString -> IO ( Either ShipwireError ( ShipwireReturn a)) -- This part right here shipwire config request = do response <- shipwire' config request let result = eitherDecode $ responseBody response case result of Left s -> return ( Left ( ShipwireError s response)) ( Right r) -> return ( Right r)

shipwire needs to return ShipwireReturn a where a is dependent on the type of ShipwireRequest we have submitted.

Because of that restriction I define the following type family:

type family ShipwireReturn a :: *

This lets us specify a particular request and response type for each endpoint. We can generate a real-time shipping quote with the Rate endpoint like so:

data ShipwireRequest a b c = ShipwireRequest { rMethod :: Method -- ^ Method of ShipwireRequest , endpoint :: Text -- ^ Endpoint of ShipwireRequest , params :: [ Params TupleBS8 BSL . ByteString ] -- ^ Request params of ShipwireRequest } data RateRequest type instance ShipwireReturn RateRequest = RateResponse

With that in place, I can now define our GetRate datatype, whose JSON representation will be sent to this endpoint:

data GetRate = GetRate { rateOptions :: RateOptions , rateOrder :: RateOrder } deriving ( Eq , Show ) instance ToJSON GetRate where toJSON GetRate { .. } = object [ "options" .= rateOptions , "order" .= rateOrder]

Ballast uses the following function to perform the HTTP request:

createRateRequest :: GetRate -> ShipwireRequest RateRequest TupleBS8 BSL . ByteString createRateRequest getRate = mkShipwireRequest NHTM . methodPost url params where url = "/rate" params = [ Body (encode getRate)]

mkShipwirerequest is a constructor that creates our request, NHTM.methodPost is http-client 's POST method.

Handling optional query parameters

You might be wondering what TupleBS8 BSL.ByteString is. That's how we pass optional parameters to an endpoint.

Here's how I set that up:

-- | Parameters for each request which include both the query and the body of a -- request data Params b c = Query TupleBS8 | Body BSL . ByteString deriving ( Show ) -- | Type alias for query parameters type TupleBS8 = ( BS8 . ByteString , BS8 . ByteString ) -- | Convert a parameter to a key/value class ToShipwireParam param where toShipwireParam :: param -> [ Params TupleBS8 c ] -> [ Params TupleBS8 c ] class ( ToShipwireParam param) => ShipwireHasParam request param where -- | Add an optional query parameter ( - & - ) :: ShipwireHasParam request param => ShipwireRequest request b c -> param -> ShipwireRequest request b c stripeRequest - & - param = stripeRequest { params = toShipwireParam param (params stripeRequest) }

That allows us to specify which endpoint might have optional query parameters like so:

instance ShipwireHasParam StockRequest SKU instance ToShipwireParam SKU where toShipwireParam ( SKU i) = ( Query ( "sku" , TE . encodeUtf8 i) : )

You can chain multiple parameters with (-&-) :

result <- shipwire config $ getReceivings - & - ( ExpandReceivingsParam [ ExpandAll ]) - & - ( ReceivingStatusParams [ StatusCanceled ]) - & - ( WarehouseIdParam [ "TEST 1" ]) - & - ( UpdatedAfter $ (read "2017-11-19 18:28:52 UTC" :: UTCTime )) -- Note: using `read` for UTCTime is not a good idea, this code exists in tests only.

Conclusion

This proved to be a pleasant way to structure a client API wrapper. It's straightforward and flexible and I believe I will reuse this in my future projects.

Further reading material:

Type Families and Pokemon

type family vs data family, in brief?

Type Classes with Functional Dependencies