Rewriting to Haskell–Parsing Query Params, Again

Posted on May 4, 2020 by Riccardo.

Rewriting to Haskell (Series)

Rewriting to Haskell–Intro Rewriting to Haskell–Project Setup Rewriting to Haskell–Deployment Rewriting to Haskell–Automatic Formatting Rewriting to Haskell–Configuration Rewriting to Haskell–Standing on the shoulders of Rails Rewriting to Haskell–Making GHC More Nitpicky Rewriting to Haskell–Testing Rewriting to Haskell–Linting Rewriting to Haskell–Parsing Query Params Rewriting to Haskell–Parsing Query Params, Again Rewriting to Haskell–Errors

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

Which translates to the following handler function

  :: Maybe Text
  -> Maybe Int
  -> Maybe Bool
  -> Maybe Text
  -> Maybe Int
  -> Handler SearchResults

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
  -- ...

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

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

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

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

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

I need your help to make the blog awesome. Here's a survey. Since there are no required questions, you decide how much time to spend on it.

Support my work by tweeting this article! 🙏