servant-queryparam-core: Use records for query parameters in servant APIs.

This is a package candidate release! Here you can preview how this package release will appear once published to the main package index (which can be accomplished via the 'maintain' link below). Please note that once a package has been published to the main package index it cannot be undone! Please consult the package uploading documentation for more information.

[maintain] [Publish]

Warnings:

Having positional parameters in servant can be error-prone, especially when there are a lot of them and they have similar types. This package solves that problem by letting one use records to specify query parameters in servant APIs. Use servant-queryparam-server for servers, servant-queryparam-client for clients, servant-queryparam-openapi3 for openapi3. See the README for more information.


[Skip to Readme]

Properties

Versions 0.0.1, 1.0.0, 1.0.1, 2.0.0, 2.0.1
Change log None available
Dependencies base (>=4.16 && <5), first-class-families (>=0.8.0.0), servant (>=0.19) [details]
License BSD-3-Clause
Copyright Kristof Bastiaensen 2020
Author Kristof Bastiaensen
Maintainer Danila Danko <https://github.com/deemp>
Category Servant, Web
Source repo head: git clone https://github.com/deemp/servant-queryparam
Uploaded by deemp at 2023-05-26T19:24:52Z

Modules

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees


Readme for servant-queryparam-core-1.0.1

[back to package description]

servant-queryparam

Provides several libraries that let you use records to specify query parameters in servant APIs.

These libraries are:

The following example was taken from here.

Example

This example demonstrates servant APIs with query parameters. There's an OpenAPI specification for each of them.

First, I enable compiler extensions.

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeFamilies #-}

Next, I import the necessary modules.

import Control.Monad.IO.Class (MonadIO (..))
import Control.Monad.Trans.Except (ExceptT (..))
import Data.Aeson (ToJSON)
import Data.ByteString.Lazy.Char8 qualified as BSL8
import Data.Data (Proxy (..), Typeable)
import Data.Maybe (maybeToList)
import Data.OpenApi (OpenApi, ToParamSchema, ToSchema)
import Data.OpenApi.Internal.Utils (encodePretty)
import GHC.Generics (Generic)
import GHC.TypeLits (Symbol)
import Network.Wai.Handler.Warp qualified as Warp
import Servant (Application, Context (..), GenericMode ((:-)), Get, Handler (..), JSON, NamedRoutes, ServerError, (:>))
import Servant.OpenApi (HasOpenApi (..))
import Servant.OpenApi.Record ()
import Servant.Record (RecordParam)
import Servant.Server.Generic (genericServe, genericServeTWithContext)
import Servant.Server.Record ()
import Servant.TypeLevel (DropPrefix, Eval, Exp)

I define a label for dropping the prefix of a Symbol.

data DropPrefixExp :: Symbol -> Exp Symbol

Next, I provide an interpreter for that wrapper.

type instance Eval (DropPrefixExp sym) = DropPrefix sym

DropPrefix drops the leading '_' characters, then the non-'_' characters, then the '_' characters.

-- >>> :kind! DropPrefix "_params_user"
-- DropPrefix "_params_user" :: Symbol
-- = "user"

Then, I define a label for keeping the prefix of a Symbol.

data KeepPrefixExp :: Symbol -> Exp Symbol

The interpreter of this label does nothing to a Symbol.

type instance Eval (KeepPrefixExp sym) = sym

Now, I write a record. I'll use this record to produce query parameters.

data Params = Params
  { _params_user :: Maybe String
  , _params_users :: [String]
  , _params_oneUser :: String
  , _params_userFlag :: Bool
  }
  deriving (Show, Generic, Typeable, ToJSON, ToSchema)

This is our first API. In this API, the query parameter names are the record field names with dropped prefixes. E.g., the query parameter user corresponds to the field name _params_user.

newtype UserAPI1 routes = UserAPI1
  { get :: routes :- "get" :> RecordParam DropPrefixExp Params :> Get '[JSON] [String]
  }
  deriving (Generic)

This is our second API. In this API, query parameter names are the record field names. E.g., the query parameter _params_user corresponds to the field name _params_user.

newtype UserAPI2 routes = UserAPI2
  { get :: routes :- "get" :> RecordParam KeepPrefixExp Params :> Get '[JSON] [String]
  }
  deriving (Generic)

Now, I define an OpenAPI specification for the first API.

specDrop :: OpenApi
specDrop = toOpenApi (Proxy :: Proxy (NamedRoutes UserAPI1))

Then, I define an OpenAPI specification for the second API.

specKeep :: OpenApi
specKeep = toOpenApi (Proxy :: Proxy (NamedRoutes UserAPI2))

Following that, I define an Application. At the /get endpoint, the application returns a list of query parameter values.

server :: Application
server =
  genericServeTWithContext
    (\x -> Servant.Handler (ExceptT $ Right <$> liftIO x))
    UserAPI1
      { get = \Params{..} ->
          pure $
            maybeToList _params_user
              <> _params_users
              <> [_params_oneUser, show _params_userFlag]
      }
    EmptyContext

Finally, I combine all parts.

First, the application will print the OpenAPI specifications in JSON format. Next, it will start a server.

main :: IO ()
main = do
  putStrLn "\n---\nQuery parameters without prefixes\n---\n"
  BSL8.putStrLn $ encodePretty specDrop
  putStrLn "\n---\nQuery parameters with prefixes\n---\n"
  BSL8.putStrLn $ encodePretty specKeep

  putStrLn "\n---\nStarting the server...\n---\n"
  putStrLn "\nTry running\n"
  putStrLn "curl -v \"localhost:8080/get?user=1&users=2&users=3&oneUser=4&userFlag=true\""
  Warp.run 8080 server

Run

Run the server.

cabal run example

Test

When the server is up, request the /get endpoint via curl.

curl -v "localhost:8080/get?user=1&users=2&users=3&oneUser=4&userFlag=true"

You should receive the values of query parameters in a list.

...
["1","2","3","4","True"]