├── .ghci ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── README.md ├── exe └── Main.hs ├── package.yaml ├── src └── App.hs ├── stack.yaml ├── stack.yaml.lock └── test ├── AppSpec.hs └── Spec.hs /.ghci: -------------------------------------------------------------------------------- 1 | :set -itest -isrc -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - "*" 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | defaults: 14 | run: 15 | shell: bash 16 | 17 | env: 18 | CACHE_KEY: 1 19 | 20 | jobs: 21 | all: 22 | name: All 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | 27 | - name: Cache 28 | uses: actions/cache@v2 29 | with: 30 | path: | 31 | ~/.stack 32 | key: stack-${{ env.CACHE_KEY }}-${{ runner.os }}-${{ hashFiles('stack.yaml*') }} 33 | 34 | - name: Info 35 | run: stack --version 36 | 37 | - name: Setup 38 | run: stack setup 39 | 40 | - name: Install Dependencies 41 | run: stack build --test --only-dependencies --haddock-deps 42 | 43 | - name: Test 44 | run: stack test --pedantic 45 | 46 | - name: Docs 47 | run: stack haddock 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .stack-work/ 2 | example-servant-minimal.cabal 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a very minimal example of a project that uses 2 | 3 | - `servant` to specify a REST API, 4 | - `servant-server` to implement a server, 5 | - `hspec` and `servant-client` for the test-suite. 6 | 7 | To set up the project and run the test-suite, do: 8 | 9 | ``` bash 10 | stack setup 11 | stack test --fast 12 | ``` 13 | 14 | To execute the test-suite faster while developing, do: 15 | ``` bash 16 | chmod go-w .ghci . 17 | stack exec ghci test/Spec.hs 18 | ``` 19 | 20 | and then at the `ghci` prompt do: 21 | 22 | ``` haskell 23 | :main 24 | ``` 25 | 26 | to run the tests and 27 | 28 | ``` haskell 29 | :r 30 | :main 31 | ``` 32 | 33 | to reload the code (after making changes) and run the tests again. 34 | 35 | To run the app, do: 36 | 37 | ``` bash 38 | stack exec example-servant-minimal 39 | ``` 40 | 41 | Then you can query the server like this: 42 | 43 | ``` bash 44 | curl localhost:3000/item 45 | ``` 46 | -------------------------------------------------------------------------------- /exe/Main.hs: -------------------------------------------------------------------------------- 1 | 2 | module Main where 3 | 4 | import App 5 | 6 | main :: IO () 7 | main = run 8 | -------------------------------------------------------------------------------- /package.yaml: -------------------------------------------------------------------------------- 1 | name: example-servant-minimal 2 | 3 | ghc-options: -Wall 4 | 5 | tests: 6 | spec: 7 | main: Spec.hs 8 | source-dirs: 9 | - test 10 | dependencies: 11 | - hspec 12 | - example-servant-minimal 13 | - servant-client 14 | - warp >= 3.2.4 15 | - http-client 16 | - http-types 17 | 18 | library: 19 | source-dirs: 20 | - src 21 | 22 | executables: 23 | example-servant-minimal: 24 | main: exe/Main.hs 25 | dependencies: 26 | - example-servant-minimal 27 | 28 | dependencies: 29 | - base 30 | - servant 31 | - servant-server 32 | - transformers 33 | - aeson 34 | - wai 35 | - warp 36 | -------------------------------------------------------------------------------- /src/App.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE DeriveGeneric #-} 3 | {-# LANGUAGE LambdaCase #-} 4 | {-# LANGUAGE TypeOperators #-} 5 | 6 | module App where 7 | 8 | import Data.Aeson 9 | import GHC.Generics 10 | import Network.Wai 11 | import Network.Wai.Handler.Warp 12 | import Servant 13 | import System.IO 14 | 15 | -- * api 16 | 17 | type ItemApi = 18 | "item" :> Get '[JSON] [Item] :<|> 19 | "item" :> Capture "itemId" Integer :> Get '[JSON] Item 20 | 21 | itemApi :: Proxy ItemApi 22 | itemApi = Proxy 23 | 24 | -- * app 25 | 26 | run :: IO () 27 | run = do 28 | let port = 3000 29 | settings = 30 | setPort port $ 31 | setBeforeMainLoop (hPutStrLn stderr ("listening on port " ++ show port)) $ 32 | defaultSettings 33 | runSettings settings =<< mkApp 34 | 35 | mkApp :: IO Application 36 | mkApp = return $ serve itemApi server 37 | 38 | server :: Server ItemApi 39 | server = 40 | getItems :<|> 41 | getItemById 42 | 43 | getItems :: Handler [Item] 44 | getItems = return [exampleItem] 45 | 46 | getItemById :: Integer -> Handler Item 47 | getItemById = \ case 48 | 0 -> return exampleItem 49 | _ -> throwError err404 50 | 51 | exampleItem :: Item 52 | exampleItem = Item 0 "example item" 53 | 54 | -- * item 55 | 56 | data Item 57 | = Item { 58 | itemId :: Integer, 59 | itemText :: String 60 | } 61 | deriving (Eq, Show, Generic) 62 | 63 | instance ToJSON Item 64 | instance FromJSON Item 65 | 66 | data a + b = Foo a b 67 | 68 | type X = Int + Bool 69 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-18.3 2 | packages: 3 | - '.' 4 | -------------------------------------------------------------------------------- /stack.yaml.lock: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by Stack. 2 | # You should not edit this file by hand. 3 | # For more information, please see the documentation at: 4 | # https://docs.haskellstack.org/en/stable/lock_files 5 | 6 | packages: [] 7 | snapshots: 8 | - completed: 9 | size: 585603 10 | url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/3.yaml 11 | sha256: 694573e96dca34db5636edb1fe6c96bb233ca0f9fb96c1ead1671cdfa9bd73e9 12 | original: lts-18.3 13 | -------------------------------------------------------------------------------- /test/AppSpec.hs: -------------------------------------------------------------------------------- 1 | 2 | module AppSpec where 3 | 4 | import Control.Exception (throwIO) 5 | import Network.HTTP.Client (Manager, newManager, defaultManagerSettings) 6 | import Network.HTTP.Types 7 | import Network.Wai.Handler.Warp 8 | import Servant 9 | import Servant.Client 10 | import Test.Hspec 11 | 12 | import App hiding (getItems) 13 | 14 | getItems :: ClientM [Item] 15 | getItem :: Integer -> ClientM Item 16 | getItems :<|> getItem = client itemApi 17 | 18 | spec :: Spec 19 | spec = do 20 | describe "/item" $ do 21 | withClient mkApp $ do 22 | it "lists an example item" $ \ env -> do 23 | try env getItems `shouldReturn` [Item 0 "example item"] 24 | 25 | it "allows to show items by id" $ \ env -> do 26 | try env (getItem 0) `shouldReturn` Item 0 "example item" 27 | 28 | it "throws a 404 for missing items" $ \ env -> do 29 | try env (getItem 42) `shouldThrow` errorsWithStatus notFound404 30 | 31 | errorsWithStatus :: Status -> ClientError -> Bool 32 | errorsWithStatus status servantError = case servantError of 33 | FailureResponse _ response -> responseStatusCode response == status 34 | _ -> False 35 | 36 | withClient :: IO Application -> SpecWith ClientEnv -> SpecWith () 37 | withClient x innerSpec = 38 | beforeAll (newManager defaultManagerSettings) $ do 39 | flip aroundWith innerSpec $ \ action -> \ httpManager -> do 40 | testWithApplication x $ \ port -> do 41 | let testBaseUrl = BaseUrl Http "localhost" port "" 42 | action (mkClientEnv httpManager testBaseUrl) 43 | 44 | type Host = (Manager, BaseUrl) 45 | 46 | try :: ClientEnv -> ClientM a -> IO a 47 | try clientEnv action = either throwIO return =<< 48 | runClientM action clientEnv 49 | -------------------------------------------------------------------------------- /test/Spec.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -F -pgmF hspec-discover #-} --------------------------------------------------------------------------------