2018-11-08 14:35:48 +01:00
|
|
|
{-# LANGUAGE DataKinds #-}
|
2019-10-01 02:59:56 +02:00
|
|
|
{-# LANGUAGE DeriveDataTypeable #-}
|
2018-11-08 14:35:48 +01:00
|
|
|
{-# LANGUAGE DeriveFunctor #-}
|
|
|
|
{-# LANGUAGE DeriveGeneric #-}
|
|
|
|
{-# LANGUAGE FlexibleContexts #-}
|
|
|
|
{-# LANGUAGE FlexibleInstances #-}
|
|
|
|
{-# LANGUAGE GADTs #-}
|
|
|
|
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
|
|
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
{-# LANGUAGE TypeOperators #-}
|
|
|
|
{-# LANGUAGE TypeSynonymInstances #-}
|
2017-06-19 17:58:25 +02:00
|
|
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
|
|
|
{-# OPTIONS_GHC -freduction-depth=100 #-}
|
|
|
|
|
2015-04-08 16:27:38 +02:00
|
|
|
module Servant.DocsSpec where
|
|
|
|
|
2015-06-16 22:38:14 +02:00
|
|
|
import Control.Lens
|
2018-11-08 14:57:04 +01:00
|
|
|
((&), (<>~))
|
2018-11-08 14:35:48 +01:00
|
|
|
import Control.Monad
|
|
|
|
(unless)
|
|
|
|
import Control.Monad.Trans.Writer
|
|
|
|
(Writer, runWriter, tell)
|
2015-04-08 16:27:38 +02:00
|
|
|
import Data.Aeson
|
2019-10-01 02:59:56 +02:00
|
|
|
import Data.Data
|
|
|
|
(Data)
|
2018-11-08 14:35:48 +01:00
|
|
|
import Data.List
|
|
|
|
(isInfixOf)
|
2015-04-08 16:27:38 +02:00
|
|
|
import Data.Proxy
|
2018-06-29 21:08:26 +02:00
|
|
|
import Data.String.Conversions
|
|
|
|
(cs)
|
2015-04-08 16:27:38 +02:00
|
|
|
import GHC.Generics
|
2018-11-08 14:35:48 +01:00
|
|
|
import Prelude ()
|
|
|
|
import Prelude.Compat
|
|
|
|
import Test.Tasty
|
|
|
|
(TestName, TestTree, testGroup)
|
2018-11-08 14:57:04 +01:00
|
|
|
import Test.Tasty.Golden
|
|
|
|
(goldenVsString)
|
2018-11-08 14:35:48 +01:00
|
|
|
import Test.Tasty.HUnit
|
|
|
|
(Assertion, HasCallStack, assertFailure, testCase, (@?=))
|
2019-10-01 02:59:56 +02:00
|
|
|
import Web.FormUrlEncoded
|
|
|
|
(ToForm)
|
2015-04-08 16:27:38 +02:00
|
|
|
|
|
|
|
import Servant.API
|
|
|
|
import Servant.Docs.Internal
|
2018-11-08 14:35:48 +01:00
|
|
|
import Servant.Test.ComprehensiveAPI
|
2015-04-08 16:27:38 +02:00
|
|
|
|
2016-01-16 19:17:46 +01:00
|
|
|
-- * comprehensive api
|
|
|
|
|
2016-01-18 19:55:14 +01:00
|
|
|
-- This declaration simply checks that all instances are in place.
|
2018-11-08 14:57:04 +01:00
|
|
|
comprehensiveDocs :: API
|
|
|
|
comprehensiveDocs = docs comprehensiveAPI
|
2016-01-16 19:17:46 +01:00
|
|
|
|
2017-12-10 13:25:14 +01:00
|
|
|
instance ToParam (QueryParam' mods "foo" Int) where
|
2018-11-08 14:57:04 +01:00
|
|
|
toParam _ = DocQueryParam "foo" ["1","2","3"] "QueryParams Int" Normal
|
2017-12-10 13:25:14 +01:00
|
|
|
instance ToParam (QueryParam' mods "bar" Int) where
|
2018-11-08 14:57:04 +01:00
|
|
|
toParam _ = DocQueryParam "bar" ["1","2","3"] "QueryParams Int" Normal
|
2016-01-16 19:17:46 +01:00
|
|
|
instance ToParam (QueryParams "foo" Int) where
|
2018-11-08 14:57:04 +01:00
|
|
|
toParam _ = DocQueryParam "foo" ["1","2","3"] "QueryParams Int" List
|
2019-10-01 02:59:56 +02:00
|
|
|
instance ToParam (QueryParam "query" String) where
|
2019-10-03 05:47:54 +02:00
|
|
|
toParam _ = DocQueryParam "query" ["a","b","c"] "QueryParams String" Normal
|
2016-01-16 19:17:46 +01:00
|
|
|
instance ToParam (QueryFlag "foo") where
|
2018-11-08 14:57:04 +01:00
|
|
|
toParam _ = DocQueryParam "foo" [] "QueryFlag" Flag
|
2016-01-16 19:17:46 +01:00
|
|
|
instance ToCapture (Capture "foo" Int) where
|
2018-11-08 14:57:04 +01:00
|
|
|
toCapture _ = DocCapture "foo" "Capture foo Int"
|
2016-07-04 14:47:34 +02:00
|
|
|
instance ToCapture (CaptureAll "foo" Int) where
|
2018-11-08 14:57:04 +01:00
|
|
|
toCapture _ = DocCapture "foo" "Capture all foo Int"
|
2016-01-16 19:17:46 +01:00
|
|
|
|
|
|
|
-- * specs
|
|
|
|
|
2018-11-08 14:35:48 +01:00
|
|
|
spec :: TestTree
|
2015-04-08 16:27:38 +02:00
|
|
|
spec = describe "Servant.Docs" $ do
|
2018-11-08 14:57:04 +01:00
|
|
|
golden "comprehensive API" "golden/comprehensive.md" (markdown comprehensiveDocs)
|
2015-04-08 16:27:38 +02:00
|
|
|
|
|
|
|
describe "markdown" $ do
|
|
|
|
let md = markdown (docs (Proxy :: Proxy TestApi1))
|
2015-06-16 22:38:14 +02:00
|
|
|
tests md
|
|
|
|
|
|
|
|
describe "markdown with extra info" $ do
|
|
|
|
let
|
|
|
|
extra = extraInfo
|
2015-09-24 13:02:21 +02:00
|
|
|
(Proxy :: Proxy (Get '[JSON, PlainText] (Headers '[Header "Location" String] Int)))
|
2015-06-16 22:38:14 +02:00
|
|
|
(defAction & notes <>~ [DocNote "Get an Integer" ["get an integer in Json or plain text"]])
|
|
|
|
<>
|
|
|
|
extraInfo
|
2019-10-01 02:59:56 +02:00
|
|
|
(Proxy :: Proxy ("postJson" :> ReqBody '[JSON] String :> Post '[JSON] Datatype1))
|
2015-06-16 22:38:14 +02:00
|
|
|
(defAction & notes <>~ [DocNote "Post data" ["Posts some Json data"]])
|
2015-09-21 12:36:57 +02:00
|
|
|
md = markdown (docsWith defaultDocOptions [] extra (Proxy :: Proxy TestApi1))
|
2015-06-16 22:38:14 +02:00
|
|
|
tests md
|
|
|
|
it "contains the extra info provided" $ do
|
|
|
|
md `shouldContain` "Get an Integer"
|
|
|
|
md `shouldContain` "Post data"
|
|
|
|
md `shouldContain` "get an integer in Json or plain text"
|
|
|
|
md `shouldContain` "Posts some Json data"
|
|
|
|
|
2015-09-16 12:42:05 +02:00
|
|
|
describe "tuple samples" $ do
|
|
|
|
it "looks like expected" $ do
|
|
|
|
(toSample (Proxy :: Proxy (TT, UT))) `shouldBe` Just (TT1,UT1)
|
|
|
|
(toSample (Proxy :: Proxy (TT, UT, UT))) `shouldBe` Just (TT1,UT1,UT1)
|
|
|
|
(toSamples (Proxy :: Proxy (TT, UT))) `shouldBe`
|
2015-09-21 11:46:21 +02:00
|
|
|
[ ("eins, yks",(TT1,UT1)), ("eins, kaks",(TT1,UT2))
|
|
|
|
, ("zwei, yks",(TT2,UT1)), ("zwei, kaks",(TT2,UT2))
|
2015-09-16 12:42:05 +02:00
|
|
|
]
|
|
|
|
(toSamples (Proxy :: Proxy (TT, UT, UT))) `shouldBe`
|
2015-09-21 11:46:21 +02:00
|
|
|
[ ("eins, yks, yks",(TT1,UT1,UT1))
|
|
|
|
, ("eins, yks, kaks",(TT1,UT1,UT2))
|
|
|
|
, ("zwei, yks, yks",(TT2,UT1,UT1))
|
|
|
|
, ("eins, kaks, yks",(TT1,UT2,UT1))
|
|
|
|
, ("zwei, yks, kaks",(TT2,UT1,UT2))
|
|
|
|
, ("eins, kaks, kaks",(TT1,UT2,UT2))
|
|
|
|
, ("zwei, kaks, yks",(TT2,UT2,UT1))
|
|
|
|
, ("zwei, kaks, kaks",(TT2,UT2,UT2))
|
2015-09-16 12:42:05 +02:00
|
|
|
]
|
|
|
|
|
2016-01-11 13:37:20 +01:00
|
|
|
|
2015-06-16 22:38:14 +02:00
|
|
|
where
|
|
|
|
tests md = do
|
2015-04-08 16:27:38 +02:00
|
|
|
it "mentions supported content-types" $ do
|
|
|
|
md `shouldContain` "application/json"
|
|
|
|
md `shouldContain` "text/plain;charset=utf-8"
|
|
|
|
|
|
|
|
it "mentions status codes" $ do
|
|
|
|
md `shouldContain` "Status code 200"
|
|
|
|
|
2016-01-15 12:17:48 +01:00
|
|
|
it "has methods as section headers" $ do
|
|
|
|
md `shouldContain` "## POST"
|
|
|
|
md `shouldContain` "## GET"
|
2015-04-08 16:27:38 +02:00
|
|
|
|
2019-10-01 02:59:56 +02:00
|
|
|
it "should mention the endpoints" $ do
|
|
|
|
md `shouldContain` "## POST /postJson"
|
|
|
|
md `shouldContain` "## GET /qparam"
|
|
|
|
md `shouldContain` "## GET /qparamform"
|
|
|
|
md `shouldContain` "## PUT /header"
|
|
|
|
|
2016-01-11 13:37:20 +01:00
|
|
|
it "mentions headers" $ do
|
|
|
|
md `shouldContain` "- This endpoint is sensitive to the value of the **X-Test** HTTP header."
|
|
|
|
|
2015-06-16 22:38:14 +02:00
|
|
|
it "contains response samples" $
|
2015-04-08 16:27:38 +02:00
|
|
|
md `shouldContain` "{\"dt1field1\":\"field 1\",\"dt1field2\":13}"
|
2015-06-16 22:38:14 +02:00
|
|
|
it "contains request body samples" $
|
2015-04-08 16:27:38 +02:00
|
|
|
md `shouldContain` "17"
|
2015-06-16 22:38:14 +02:00
|
|
|
|
2019-10-01 02:59:56 +02:00
|
|
|
it "mentions optional query-param" $ do
|
|
|
|
md `shouldContain` "### GET Parameters:"
|
|
|
|
md `shouldContain` "- query"
|
2019-10-03 05:47:54 +02:00
|
|
|
it "mentions optional query-param-form params from QueryParamForm" $
|
2019-10-01 02:59:56 +02:00
|
|
|
md `shouldContain` "- **Values**: *dt1field1=field%201&dt1field2=13*"
|
|
|
|
|
2017-05-16 12:29:27 +02:00
|
|
|
it "does not generate any docs mentioning the 'empty-api' path" $
|
|
|
|
md `shouldNotContain` "empty-api"
|
|
|
|
|
2016-01-11 13:37:20 +01:00
|
|
|
|
2015-04-08 16:27:38 +02:00
|
|
|
-- * APIs
|
|
|
|
|
|
|
|
data Datatype1 = Datatype1 { dt1field1 :: String
|
|
|
|
, dt1field2 :: Int
|
2019-10-01 02:59:56 +02:00
|
|
|
} deriving (Eq, Show, Data, Generic)
|
2015-04-08 16:27:38 +02:00
|
|
|
|
|
|
|
instance ToJSON Datatype1
|
2019-10-01 02:59:56 +02:00
|
|
|
instance ToForm Datatype1
|
2015-04-08 16:27:38 +02:00
|
|
|
|
2015-09-24 13:02:21 +02:00
|
|
|
instance ToSample Datatype1 where
|
2015-09-21 11:46:21 +02:00
|
|
|
toSamples _ = singleSample $ Datatype1 "field 1" 13
|
2015-04-08 16:27:38 +02:00
|
|
|
|
2015-09-24 13:02:21 +02:00
|
|
|
instance ToSample Char where
|
2015-09-21 11:51:00 +02:00
|
|
|
toSamples _ = samples ['a'..'z']
|
2015-04-08 16:27:38 +02:00
|
|
|
|
2015-09-24 13:02:21 +02:00
|
|
|
instance ToSample Int where
|
2015-09-21 11:46:21 +02:00
|
|
|
toSamples _ = singleSample 17
|
2015-04-08 16:27:38 +02:00
|
|
|
|
|
|
|
instance MimeRender PlainText Int where
|
2015-04-19 14:51:34 +02:00
|
|
|
mimeRender _ = cs . show
|
2015-04-08 16:27:38 +02:00
|
|
|
|
2015-09-24 13:02:21 +02:00
|
|
|
type TestApi1 = Get '[JSON, PlainText] (Headers '[Header "Location" String] Int)
|
2019-10-01 02:59:56 +02:00
|
|
|
:<|> "postJson" :> ReqBody '[JSON] String :> Post '[JSON] Datatype1
|
|
|
|
:<|> "qparam" :> QueryParam "query" String :> Get '[JSON] Datatype1
|
2019-10-03 05:47:54 +02:00
|
|
|
:<|> "qparamform" :> QueryParamForm Datatype1 :> Get '[JSON] Datatype1
|
2019-10-01 02:59:56 +02:00
|
|
|
:<|> "header" :> Header "X-Test" Int :> Put '[JSON] Int
|
|
|
|
:<|> "empty-api" :> EmptyAPI
|
2015-04-08 16:27:38 +02:00
|
|
|
|
2015-09-16 12:42:05 +02:00
|
|
|
data TT = TT1 | TT2 deriving (Show, Eq)
|
|
|
|
data UT = UT1 | UT2 deriving (Show, Eq)
|
|
|
|
|
2015-09-24 13:02:21 +02:00
|
|
|
instance ToSample TT where
|
2015-09-16 12:42:05 +02:00
|
|
|
toSamples _ = [("eins", TT1), ("zwei", TT2)]
|
|
|
|
|
2015-09-24 13:02:21 +02:00
|
|
|
instance ToSample UT where
|
2015-09-16 12:42:05 +02:00
|
|
|
toSamples _ = [("yks", UT1), ("kaks", UT2)]
|
2018-11-08 14:35:48 +01:00
|
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- HSpec like DSL for tasty
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
newtype TestTreeM a = TestTreeM (Writer [TestTree] a)
|
|
|
|
deriving (Functor, Applicative, Monad)
|
|
|
|
|
|
|
|
runTestTreeM :: TestTreeM () -> [TestTree]
|
|
|
|
runTestTreeM (TestTreeM m) = snd (runWriter m)
|
|
|
|
|
|
|
|
class Describe r where
|
|
|
|
describe :: TestName -> TestTreeM () -> r
|
|
|
|
|
|
|
|
instance a ~ () => Describe (TestTreeM a) where
|
|
|
|
describe n t = TestTreeM $ tell [ describe n t ]
|
|
|
|
|
|
|
|
instance Describe TestTree where
|
|
|
|
describe n t = testGroup n $ runTestTreeM t
|
|
|
|
|
|
|
|
it :: TestName -> Assertion -> TestTreeM ()
|
|
|
|
it n assertion = TestTreeM $ tell [ testCase n assertion ]
|
|
|
|
|
|
|
|
shouldBe :: (Eq a, Show a, HasCallStack) => a -> a -> Assertion
|
|
|
|
shouldBe = (@?=)
|
|
|
|
|
|
|
|
shouldContain :: (Eq a, Show a, HasCallStack) => [a] -> [a] -> Assertion
|
|
|
|
shouldContain = compareWith (flip isInfixOf) "does not contain"
|
|
|
|
|
|
|
|
shouldNotContain :: (Eq a, Show a, HasCallStack) => [a] -> [a] -> Assertion
|
|
|
|
shouldNotContain = compareWith (\x y -> not (isInfixOf y x)) "contains"
|
|
|
|
|
|
|
|
compareWith :: (Show a, Show b, HasCallStack) => (a -> b -> Bool) -> String -> a -> b -> Assertion
|
|
|
|
compareWith f msg x y = unless (f x y) $ assertFailure $
|
|
|
|
show x ++ " " ++ msg ++ " " ++ show y
|
2018-11-08 14:57:04 +01:00
|
|
|
|
|
|
|
golden :: TestName -> FilePath -> String -> TestTreeM ()
|
|
|
|
golden n fp contents = TestTreeM $ tell
|
|
|
|
[ goldenVsString n fp (return (cs contents)) ]
|