trasa-0.2: test/Main.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
import Text.Read (readMaybe)
import Trasa.Core
import qualified Trasa.Method as M
import Data.Vinyl
import Data.Kind (Type)
import qualified Data.ByteString.Lazy.Char8 as LBSC
import qualified Data.Text as T
import Test.Tasty
import Test.Tasty.QuickCheck as QC
import Test.Tasty.HUnit
import Data.Functor.Identity
import Data.Monoid
import Test.DocTest (doctest)
main :: IO ()
main = do
putStrLn "\nRUNNING DOCTESTS"
doctest
[ "src/Trasa"
]
putStrLn "\nPRETTY ROUTER"
putStrLn (prettyRouter router)
putStrLn "\nRUNNING OTHER TESTS"
defaultMain tests
tests :: TestTree
tests = testGroup "Tests" [properties, unitTests]
-- todo: add a property test to show that parse and link
-- form a partial isomorphism.
properties :: TestTree
properties = testGroup "Properties"
[ QC.testProperty "roundtrip link parse" roundtripLinkParse
]
unitTests :: TestTree
unitTests = testGroup "Unit Tests"
[ testCase "link addition route"
$ link (prepare AdditionR 12 5 (Just 3)) @?= decodeUrl "/add/12/5?more=3"
, testCase "link left pad route"
$ link (prepare LeftPadR 5 "foo") @?= decodeUrl "/pad/left/5"
, testCase "parse hello route"
$ parse "/hello" Nothing @?= Right (conceal (prepare HelloR))
, testCase "parse addition route"
$ parse "/add/6/3" Nothing @?= Right (conceal (prepare AdditionR 6 3 Nothing))
]
data Route :: [Type] -> [Param] -> Bodiedness -> Type -> Type where
EmptyR :: Route '[] '[] Bodyless Int
HelloR :: Route '[] '[] Bodyless Int
AdditionR :: Route '[Int,Int] '[Optional Int] Bodyless Int
IdentityR :: Route '[String] '[] Bodyless String
LeftPadR :: Route '[Int] '[] (Body String) String
TrickyOneR :: Route '[Int] '[] Bodyless String
TrickyTwoR :: Route '[Int,Int] '[] Bodyless String
prepare :: Route cs qs rq rp -> Arguments cs qs rq (Prepared Route rp)
prepare = prepareWith (metaPath . meta) (metaQuery . meta) (metaRequestBody . meta)
link :: Prepared Route rp -> Url
link = linkWith
(mapPath (CaptureEncoding . captureCodecEncode) . metaPath . meta)
(mapQuery captureCodecToCaptureEncoding . metaQuery . meta)
parse :: T.Text -> Maybe Content -> Either TrasaErr (Concealed Route)
parse url = parseWith
(mapQuery captureCodecToCaptureDecoding . metaQuery . meta)
(mapRequestBody (Many . pure . bodyCodecToBodyDecoding) . metaRequestBody . meta)
router
"get"
(decodeUrl url)
allRoutes :: [Constructed Route]
allRoutes =
[ Constructed HelloR
, Constructed AdditionR
, Constructed IdentityR
, Constructed LeftPadR
, Constructed TrickyOneR
, Constructed TrickyTwoR
, Constructed EmptyR
]
router :: Router Route
router = routerWith
(metaMethod . meta)
(mapPath (CaptureDecoding . captureCodecDecode) . metaPath . meta)
allRoutes
data Meta ps qs rq rp = Meta
{ metaPath :: Path CaptureCodec ps
, metaQuery :: Rec (Query CaptureCodec) qs
, metaRequestBody :: RequestBody BodyCodec rq
, metaResponseBody :: ResponseBody BodyCodec rp
, metaMethod :: Method
}
meta :: Route ps qs rq rp -> Meta ps qs rq rp
meta x = case x of
EmptyR -> Meta
end
qend
bodyless (resp bodyInt) M.get
HelloR -> Meta
(match "hello" ./ end)
qend
bodyless (resp bodyInt) M.get
AdditionR -> Meta
(match "add" ./ capture int ./ capture int ./ end)
(optional "more" int .& qend)
bodyless (resp bodyInt) M.get
IdentityR -> Meta
(match "identity" ./ capture string ./ end)
qend
bodyless (resp bodyString) M.get
LeftPadR -> Meta
(match "pad" ./ match "left" ./ capture int ./ end)
qend
(body bodyString) (resp bodyString) M.get
TrickyOneR -> Meta
(match "tricky" ./ capture int ./ match "one" ./ end)
qend
bodyless (resp bodyString) M.get
TrickyTwoR -> Meta
(capture int ./ capture int ./ match "two" ./ end)
qend
bodyless (resp bodyString) M.get
int :: CaptureCodec Int
int = CaptureCodec (T.pack . show) (readMaybe . T.unpack)
string :: CaptureCodec String
string = CaptureCodec T.pack (Just . T.unpack)
bodyString :: BodyCodec String
bodyString = BodyCodec (pure "text/plain") LBSC.pack (Right . LBSC.unpack)
bodyUnit :: BodyCodec ()
bodyUnit = BodyCodec (pure "text/plain") (const "") (const (Right ()))
note :: e -> Maybe a -> Either e a
note e Nothing = Left e
note _ (Just x) = Right x
bodyInt :: BodyCodec Int
bodyInt = BodyCodec (pure "text/plain") (LBSC.pack . show)
(note "Could not decode int" . readMaybe . LBSC.unpack)
roundtripLinkParse :: Concealed Route -> Property
roundtripLinkParse c@(Concealed route captures querys reqBody) =
(case reqBody of
RequestBodyPresent _ -> False
RequestBodyAbsent -> True
)
==>
Right c == parse (encodeUrl (link (Prepared route captures querys reqBody))) Nothing
-- This instance is defined only so that the test suite can do
-- its job. It not not neccessary or recommended to write this
-- instance in production code.
instance Eq (Concealed Route) where
Concealed rt1 ps1 qs1 rq1 == Concealed rt2 ps2 qs2 rq2 = case (rt1,rt2) of
(AdditionR,AdditionR) -> ps1 == ps2 && qs1 == qs2 && rq1 == rq2
(IdentityR,IdentityR) -> ps1 == ps2 && qs1 == qs2 && rq1 == rq2
(LeftPadR,LeftPadR) -> case (rq1,rq2) of
(RequestBodyPresent a, RequestBodyPresent b) -> ps1 == ps2 && qs1 == qs2 && a == b
(TrickyOneR,TrickyOneR) -> ps1 == ps2 && qs1 == qs2 && rq1 == rq2
(TrickyTwoR,TrickyTwoR) -> ps1 == ps2 && qs1 == qs2 && rq1 == rq2
(HelloR,HelloR) -> ps1 == ps2 && qs1 == qs2 && rq1 == rq2
(EmptyR,EmptyR) -> ps1 == ps2 && qs1 == qs2 && rq1 == rq2
instance Arbitrary (Concealed Route) where
arbitrary = oneof
[ Concealed AdditionR <$> arbitrary <*> arbitrary <*> arbitrary
, Concealed IdentityR <$> arbitrary <*> arbitrary <*> arbitrary
, Concealed LeftPadR <$> arbitrary <*> arbitrary <*> arbitrary
, Concealed TrickyOneR <$> arbitrary <*> arbitrary <*> arbitrary
, Concealed TrickyTwoR <$> arbitrary <*> arbitrary <*> arbitrary
, Concealed HelloR <$> arbitrary <*> arbitrary <*> arbitrary
, Concealed EmptyR <$> arbitrary <*> arbitrary <*> arbitrary
]
instance Show (Concealed Route) where
show (Concealed r a q b) = show (link (Prepared r a q b))
instance Eq a => Eq (Parameter (Optional a)) where
ParameterOptional m1 == ParameterOptional m2 = m1 == m2
instance Arbitrary (Rec Identity '[]) where
arbitrary = pure RNil
instance (Arbitrary r, Arbitrary (Rec Identity rs)) => Arbitrary (Rec Identity (r ': rs)) where
arbitrary = (:&) <$> (Identity <$> arbitrary) <*> arbitrary
instance Arbitrary (Rec Parameter '[]) where
arbitrary = pure RNil
instance (Arbitrary r, Arbitrary (Rec Parameter rs)) => Arbitrary (Rec Parameter (Optional r ': rs)) where
arbitrary = (:&) <$> (ParameterOptional <$> arbitrary) <*> arbitrary
instance Arbitrary (RequestBody f 'Bodyless) where
arbitrary = pure RequestBodyAbsent
instance Arbitrary a => Arbitrary (RequestBody Identity (Body a)) where
arbitrary = RequestBodyPresent . Identity <$> arbitrary
instance Eq (RequestBody f 'Bodyless) where
RequestBodyAbsent == RequestBodyAbsent = True