packages feed

hspec-tables-0.0.1: src/Test/Hspec/Tables.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}

-- |
-- Copyright: (c) 2020 Marcin Rzeźnicki
-- SPDX-License-Identifier: MIT
-- Maintainer: Marcin Rzeźnicki <marcin.rzeznicki@gmail.com>
--
-- Table-driven (by-example) HSpec tests.
--
-- Example usage:
--
-- > describe "multiplication table" $
-- >  byExample
-- >    ("x", "y", "result")
-- >    [ (0, 0, 0),
-- >      (0, 1, 0),
-- >      (0, 2, 0),
-- >      (1, 0, 0),
-- >      (1, 1, 1),
-- >      (1, 2, 2),
-- >      (2, 0, 0),
-- >      (2, 1, 2),
-- >      (2, 2, 4)
-- >    ]
-- >    (\a b expected -> a * b == expected)
--
-- > describe "reverse" $
-- >  byExample
-- >    ("list", "reversed")
-- >    [("abc", "cba"), ("", ""), ("0123456", "6543210")]
-- >    (shouldBe . reverse)
module Test.Hspec.Tables
  ( Table (..),
    byExample,
    testTable,
  )
where

import Test.Hspec.Core.Spec

-- | A type class for tables.
--
-- A table type binds together the row type @r@ with the type of its header and the "generic" curried function @forall p. r -> p@
class Table r where
  type Header r
  type Forall r p
  apply :: Forall r p -> r -> p

  showHeader :: Header r -> String
  default showHeader :: Show (Header r) => Header r -> String
  showHeader = show

instance Table (a, b) where
  type Header (a, b) = (String, String)
  type Forall (a, b) p = a -> b -> p
  apply = uncurry

instance Table (a, b, c) where
  type Header (a, b, c) = (String, String, String)
  type Forall (a, b, c) p = a -> b -> c -> p
  apply f (a, b, c) = f a b c

instance Table (a, b, c, d) where
  type Header (a, b, c, d) = (String, String, String, String)
  type Forall (a, b, c, d) p = a -> b -> c -> d -> p
  apply f (a, b, c, d) = f a b c d

instance Table (a, b, c, d, e) where
  type Header (a, b, c, d, e) = (String, String, String, String, String)
  type Forall (a, b, c, d, e) p = a -> b -> c -> d -> e -> p
  apply f (a, b, c, d, e) = f a b c d e

instance Table (a, b, c, d, e, f) where
  type Header (a, b, c, d, e, f) = (String, String, String, String, String, String)
  type Forall (a, b, c, d, e, f) p = a -> b -> c -> d -> e -> f -> p
  apply f (a, b, c, d, e, f_) = f a b c d e f_

instance Table (a, b, c, d, e, f, g) where
  type Header (a, b, c, d, e, f, g) = (String, String, String, String, String, String, String)
  type Forall (a, b, c, d, e, f, g) p = a -> b -> c -> d -> e -> f -> g -> p
  apply f (a, b, c, d, e, f_, g) = f a b c d e f_ g

-- | Creates a 'Spec' from the /table/ consisting of:
--
--    * header
--    * list of examples (/rows/)
--    * assertion
--
--  The resulting spec consists of one test per each /row/.
--  For example:
--
--  > byExample
--  >   ("list", "reversed")
--  >   [("abc", "cba"), ("", ""), ("0123456", "6543210")]
--  >  (shouldBe . reverse)
--
--  is equivalent to:
--
--  > describe (show ("list", "reversed")) $ do
--  >   specify (show ("abc", "cba")) $ reverse "abc" `shouldBe` "cba"
--  >   specify (show ("", "")) $ reverse "" `shouldBe` ""
--  >   specify (show ("0123456", "6543210")) $ reverse "0123456" `shouldBe` "6543210"
byExample ::
  forall a r.
  (Table r, Example a, Show r) =>
  -- | /header/ - tuple of strings (max 7 for now); used to 'describe' this spec, its arity == number of columns
  Header r ->
  -- | /rows/ - list of tuples of examples; arity of each tuple must match the number of columns (== arity of the /header/)
  [r] ->
  -- | /assertion/ - curried function from a row to an @a@ - if /row/ type is @(b,c,d)@ then it must be @('Example' a) => b -> c -> d -> a@
  Forall r a ->
  SpecWith (Arg a)
byExample header table test =
  describe (showHeader @r header) $ mapM_ (\row -> specify @a (show row) $ apply test row) table

-- | Alias for 'byExample'
testTable ::
  forall a r.
  (Table r, Example a, Show r) =>
  Header r ->
  [r] ->
  Forall r a ->
  SpecWith (Arg a)
testTable = byExample @a