DEV Community

Riccardo Odone
Riccardo Odone

Posted on • Updated on • Originally published at

Rewriting to Haskell–Parsing Query Params, Again

You can keep reading here or jump to my blog to get the full experience, including the wonderful pink, blue and white palette.

This is part of a series:

In the previous post we covered how to have Servant parse URL query parameters to custom data types. In this post, we see a similar technique without the use of FromHttpApiData.

The search endpoint in Stream has the following type signature:

type SearchAPI =
  QueryParam "query" Text
    :> QueryParam "quantity" Int
    :> QueryParam "comments" Bool
    :> QueryParam "channel" Text
    :> QueryParam "last_id" Int
    :> Get '[JSON] SearchResults
Enter fullscreen mode Exit fullscreen mode

Which translates to the following handler function

  :: Maybe Text
  -> Maybe Int
  -> Maybe Bool
  -> Maybe Text
  -> Maybe Int
  -> Handler SearchResults
Enter fullscreen mode Exit fullscreen mode

Contrarily to the previous post, in this case we chose to use primitive types (e.g. Text, Int) instead of defining our own. What we do instead is to parse all the values in the first few lines of the handler:

getSearchResults configuration connection mQuery mQuantity mComments mChannel mLastId = do
  let searchQuery = mkSearchQuery mQuery
  let searchQuantity = mkSearchQuantity mQuantity
  let searchComments = mkSearchComments searchQuery mComments
  let searchChannels = mkSearchChannels mChannel
  let searchLastId = mkSearchLastId mLastId
  -- ...
Enter fullscreen mode Exit fullscreen mode

By doing that, we can translate Maybes into something that makes sense in Stream. For example, when in the URL query is not present or is an empty string, we want to return all posts. Otherwise, we use the value to filter:

data SearchQuery
  = Query Text
  | NoQuery

mkSearchQuery :: Maybe Text -> SearchQuery
mkSearchQuery Nothing = NoQuery
mkSearchQuery (Just "") = NoQuery
mkSearchQuery (Just query) = Query query
Enter fullscreen mode Exit fullscreen mode

In the case of quantity (of posts returned) and (return posts older than) last_id:

data SearchQuantity
  = Limit Int
  | NoLimit

mkSearchQuantity :: Maybe Int -> SearchQuantity
mkSearchQuantity (Just quantity) = Limit quantity
mkSearchQuantity Nothing = NoLimit

data SearchLastId
  = LastId Int
  | NoLastId

mkSearchLastId :: Maybe Int -> SearchLastId
mkSearchLastId (Just lastId) = LastId lastId
mkSearchLastId Nothing = NoLastId
Enter fullscreen mode Exit fullscreen mode

When it comes to what channels to search posts in, we limit to one only if channel is specified in the URL:

data SearchChannels
  = Channel Text
  | All

mkSearchChannels :: Maybe Text -> SearchChannels
mkSearchChannels Nothing = All
mkSearchChannels (Just channel) = Channel channel
Enter fullscreen mode Exit fullscreen mode

More interesting is when the endpoint is instructed to also match against the comments belonging to the post:

data SearchComments
  = Enabled SearchQuery
  | Disabled

mkSearchComments :: SearchQuery -> Maybe Bool -> SearchComments
mkSearchComments searchQuery searchComments =
  case searchComments of
    Just True -> Enabled searchQuery
    _ -> Disabled
Enter fullscreen mode Exit fullscreen mode

When comments=true, the query used to match against comments is the same used for posts. Otherwise, the search in comments is Disabled.

Get the latest content via email from me personally. Reply with your thoughts. Let's learn from each other. Subscribe to my PinkLetter!

Top comments (2)

tfausak profile image
Taylor Fausak

I like how you use primitive types in the API signature and custom types in the handler! That's a clever way to avoid ending up with Maybe SearchQuery as a function argument, especially since Nothing should be treated the same as Just NoQuery.

riccardoodone profile image
Riccardo Odone

Thanks for the kind words Taylor!