variadic-0.0.0.0: test/Test.hs
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Main where
import Control.Monad.IO.Class (MonadIO(liftIO))
import Control.Monad.Reader (ReaderT, ask, runReaderT)
import Control.Variadic
import Control.Variadic.Generic (ghoist)
import Data.Char (chr, ord)
import Data.IORef (IORef, atomicModifyIORef', newIORef, readIORef, writeIORef)
import Data.Map.Strict (Map)
import Data.Maybe (isJust, listToMaybe)
import Test.Hspec (HasCallStack, SpecWith, describe, hspec)
import Test.Hspec.Expectations.Lifted (shouldBe, shouldReturn)
import Test.Infra.Handle (Handle(Handle))
import Test.ShouldNotCompile (shouldNotCompile)
import qualified Data.Map.Strict as Map
import qualified Test.Hspec as Hspec
import qualified Test.Infra.Handle
-- | Specialized 'Hspec.it' body to @IO ()@ to make type inference
-- easier for @hspec-expectations-lifted@.
it :: (HasCallStack) => String -> IO () -> SpecWith ()
it = Hspec.it
main :: IO ()
main = hspec do
describe "Variadic" do
describe "arity" do
it "0" do
let v = toVariadic ioA
fromVariadic v `shouldReturn` 'a'
it "1" do
let v = toVariadic ioCharToInt
fromVariadic v 'a' `shouldReturn` 97
it "2" do
let v = toVariadic ioPlusChars
fromVariadic v 'a' 2 `shouldReturn` 'c'
describe "VariadicT" do
it "*>" do
let v1 = toVariadicT ioCharToInt
let v2 = toVariadicT ioIncChar
let v3 = v1 *> v2
fromVariadicT v3 'a' `shouldReturn` 'b'
describe "Functions" do
it "...*>" do
ref <- newIORef '\0'
let f = writeIORef ref ...*> ioIncChar
f 'a' `shouldReturn` 'b'
readIORef ref `shouldReturn` 'a'
it "vhoist" do
let f = vhoist listToMaybe twoList
f 1 2 `shouldBe` Just 1
describe "Generic" do
it "errors" do
shouldNotCompile "TestCompilerErrors"
it "ghoist" do
-- Keep track of how many times we call 'close'.
closeCounter <- newIORef (0 :: Int)
-- A starting implementation of 'Handle'.
let readerHandle :: Handle (ReaderT DB IO)
readerHandle = Handle
{ insert = \name descr -> do
db <- ask
liftIO $ atomicModifyIORef' db \rows ->
let k = maybe 1 (+1) $ fmap fst $ Map.lookupMax rows
in ( Map.insert k (name, descr) rows
, k
)
, get = \k -> do
db <- ask
rows <- liftIO $ readIORef db
pure $ Map.lookup k rows
, delete = \k -> do
db <- ask
liftIO $ atomicModifyIORef' db \rows ->
let (oldValue, rows') = Map.updateLookupWithKey mempty k rows
in (rows', isJust oldValue)
, close = atomicModifyIORef' closeCounter \i -> (i + 1, ())
}
-- Create the initial database.
db :: DB <- newIORef mempty
-- Tests for the @readerHandle@.
flip runReaderT db do
let Handle { insert, get, delete, close } = readerHandle
get 1 `shouldReturn` Nothing
insert "foo" "bar" `shouldReturn` 1
get 1 `shouldReturn` Just ("foo", "bar")
get 2 `shouldReturn` Nothing
insert "spam" "eggs" `shouldReturn` 2
get 2 `shouldReturn` Just ("spam", "eggs")
delete 2 `shouldReturn` True
get 2 `shouldReturn` Nothing
delete 2 `shouldReturn` False
liftIO do
readIORef closeCounter `shouldReturn` 0
close
readIORef closeCounter `shouldReturn` 1
-- Used to keep track of how many times our @runner@ is invoked.
runCounter <- newIORef (0 :: Int)
-- Natural transformation for converting our @readerHandle@ to @ioHandle@.
let runner :: ReaderT DB IO x -> IO x
runner action = do
atomicModifyIORef' runCounter \i -> (i + 1, ())
runReaderT action db
-- Example usage of 'ghoist'.
let ioHandle :: Handle IO
ioHandle = ghoist runner readerHandle
-- Tests for the @ioHandle@.
do
let Handle { insert, get, delete, close } = ioHandle
readIORef runCounter `shouldReturn` 0
get 1 `shouldReturn` Just ("foo", "bar")
readIORef runCounter `shouldReturn` 1
get 2 `shouldReturn` Nothing
readIORef runCounter `shouldReturn` 2
k <- insert "baz" "quux"
readIORef runCounter `shouldReturn` 3
k `shouldBe` 2
get 2 `shouldReturn` Just ("baz", "quux")
readIORef runCounter `shouldReturn` 4
delete 2 `shouldReturn` True
readIORef runCounter `shouldReturn` 5
get 2 `shouldReturn` Nothing
readIORef runCounter `shouldReturn` 6
-- Calling 'close' should not increment the @runCounter@
-- but should still increment @closeCounter@.
readIORef closeCounter `shouldReturn` 1
close
readIORef closeCounter `shouldReturn` 2
readIORef runCounter `shouldReturn` 6
ioA :: IO Char
ioA = pure 'a'
ioCharToInt :: Char -> IO Int
ioCharToInt c = pure (ord c)
ioIncChar :: Char -> IO Char
ioIncChar c = pure (chr (ord c + 1))
ioPlusChars :: Char -> Int -> IO Char
ioPlusChars c n = pure (chr (ord c + n))
twoList :: Int -> Int -> [Int]
twoList x y = [x, y]
-- | A simple in-memory database.
type DB = IORef (Map Int (String, String))