packages feed

servant-routes-0.1.0.0: test/Servant/API/Routes/PathSpec.hs

module Servant.API.Routes.PathSpec where

import qualified Data.Text as T
import Data.Typeable
import Servant.API.Routes.Internal.Path
import Servant.API.Routes.Path
import Test.Hspec as H
import Test.Hspec.QuickCheck as H
import Test.QuickCheck as Q

genAlphaText :: Q.Gen T.Text
genAlphaText =
  T.pack <$> do
    first' <- alnum
    rest <- Q.listOf alnumOrOther
    last' <- alnum
    pure $ first' : rest <> [last']
  where
    alnumChars = ['a' .. 'z'] <> ['A' .. 'Z'] <> ['0' .. '9']
    alnum = Q.elements alnumChars
    alnumOrOther = Q.elements $ alnumChars <> "-_"

shrinkText :: T.Text -> [T.Text]
shrinkText = fmap T.pack . filter (not . null) . Q.shrinkList (const []) . T.unpack

genStringPart :: Q.Gen T.Text
genStringPart = genAlphaText

genTypeRep :: Q.Gen TypeRep
genTypeRep =
  Q.elements
    [ typeRep (Proxy @Int)
    , typeRep (Proxy @String)
    , typeRep (Proxy @())
    , typeRep (Proxy @[Int])
    ]

genPathPart :: Q.Gen PathPart
genPathPart =
  Q.frequency
    [ (6, StringPart <$> genStringPart)
    , (3, CapturePart <$> genStringPart <*> genTypeRep)
    , (1, CaptureAllPart <$> genStringPart <*> genTypeRep)
    ]

genStringParts :: Q.Gen T.Text
genStringParts = unSplit <$> Q.listOf genStringPart

shrinkStringPart :: T.Text -> [T.Text]
shrinkStringPart = shrinkText

shrinkStringParts :: T.Text -> [T.Text]
shrinkStringParts = fmap unSplit . Q.shrinkList shrinkStringPart . T.splitOn "/"

unSplit :: [T.Text] -> T.Text
unSplit = mappend pathSeparator . T.intercalate pathSeparator

shrinkPathPart :: PathPart -> [PathPart]
shrinkPathPart = \case
  StringPart str -> StringPart <$> shrinkStringPart str
  CapturePart name tRep -> [CapturePart name' tRep | name' <- shrinkStringPart name]
  CaptureAllPart name tRep -> [CaptureAllPart name' tRep | name' <- shrinkStringPart name]

normalise :: T.Text -> T.Text
normalise =
  unSplit
    . filter (not . T.null)
    . T.splitOn pathSeparator

instance Q.Arbitrary Path where
  arbitrary = Path <$> Q.listOf genPathPart
  shrink = fmap Path . Q.shrinkList shrinkPathPart . unPath

testPrep :: T.Text -> Path -> Q.Property
testPrep part path =
  let preped = prependPathPart part path
      lhs = renderPath preped
      rhs = part <> pathSeparator <> renderPath path
  in  normalise lhs === normalise rhs

spec :: Spec
spec = do
  describe "renderPath" $ do
    it "should render the root path correctly" $
      renderPath rootPath `shouldBe` pathSeparator
  describe "prependPathPart" $ do
    H.prop "should correctly prepend a single path part" $
      \(path :: Path) ->
        Q.forAllShrink genStringPart shrinkStringPart $ \part ->
          testPrep part path
    H.prop "should correctly prepend a multi-part path" $
      \(path :: Path) ->
        Q.forAllShrink genStringParts shrinkStringParts $ \part ->
          testPrep part path