diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Changelog
+
+`hspec-tables` uses [PVP Versioning][1].
+The changelog is available [on GitHub][2].
+
+## 0.0.1
+
+* Initially created.
+
+[1]: https://pvp.haskell.org
+[2]: https://github.com/marcin-rzeznicki/hspec-tables/releases
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Marcin Rzeźnicki
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,99 @@
+# hspec-tables
+
+[![Hackage](https://img.shields.io/hackage/v/hspec-tables.svg?logo=haskell)](https://hackage.haskell.org/package/hspec-tables)
+[![Stackage Lts](http://stackage.org/package/hspec-tables/badge/lts)](http://stackage.org/lts/package/hspec-tables)
+[![Stackage Nightly](http://stackage.org/package/hspec-tables/badge/nightly)](http://stackage.org/nightly/package/hspec-tables)
+[![MIT license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
+[![Build Status](https://travis-ci.com/marcin-rzeznicki/hspec-tables.svg?branch=master)](https://travis-ci.com/marcin-rzeznicki/hspec-tables)
+
+`Test.Hspec.Tables` allows you to define table-driven (or, _by-example_) [HSpec](https://hspec.github.io/) tests. For example:
+
+```haskell
+import Test.Hspec
+import Test.Hspec.Tables
+
+example1 :: Spec
+example1 =
+  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)
+```
+
+or
+
+```haskell
+example2 :: Spec
+example2 =
+  describe "reverse" $
+    byExample
+      ("list", "reversed")
+      [("abc", "cba"), ("", ""), ("0123456", "6543210")]
+      (shouldBe . reverse)
+```
+
+When you run these, you'll see that each row becomes a seperate test labelled with `show row` (so the requirement for rows is to consist of elements that have a `Show` instance):
+
+```
+Example specs
+  multiplication table
+    ("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)
+  reverse
+    ("list","reversed")
+      ("abc","cba")
+      ("","")
+      ("0123456","6543210")
+
+Finished in 0.0008 seconds
+12 examples, 0 failures
+
+```
+
+The `byExample` method is type-safe. The table _header_ must be a 2- to 7- element tuple of String and you must provide the same number of "columns" or it doesn't compile. So if you attempted to write:
+
+```haskell
+example1 :: Spec
+example1 =
+  describe "THIS EXAMPLE DOES NOT COMPILE" $
+    byExample
+      ("x", "y", "result") -- 3 columns
+      [ (0, 0, 0, 0) ]     -- 4 columns
+      (\a b c expected -> a * b * c == expected)
+```
+
+then you should get the following error, hopefully guiding to what the problem is
+
+```
+error:
+    • Couldn't match type ‘([Char], [Char], [Char])’
+                     with ‘(String, String, String, String)’
+      Expected type: Test.Hspec.Tables.Header
+                       (Integer, Integer, Integer, Integer)
+        Actual type: ([Char], [Char], [Char])
+
+```
+
+The assertion function will match the table type, so if your table is of shape `(a, b, c)` then the assertion is assumed to be of type `(Example e) => a -> b -> c -> e` (ie. it's always curried). [Example](https://hackage.haskell.org/package/hspec/docs/Test-Hspec.html#t:Example) comes from `HSpec`
+
+## Caveats
+
+* You can define tables up-to 7 columns (adding columns beyond that requires providing an instance of the `Table` type-class)
diff --git a/hspec-tables.cabal b/hspec-tables.cabal
new file mode 100644
--- /dev/null
+++ b/hspec-tables.cabal
@@ -0,0 +1,61 @@
+cabal-version:       2.4
+name:                hspec-tables
+version:             0.0.1
+synopsis:            Table-driven (by-example) HSpec tests
+description:         Table-driven (by-example) HSpec tests
+homepage:            https://github.com/marcin-rzeznicki/hspec-tables
+bug-reports:         https://github.com/marcin-rzeznicki/hspec-tables/issues
+license:             MIT
+license-file:        LICENSE
+author:              Marcin Rzeźnicki
+maintainer:          Marcin Rzeźnicki <marcin.rzeznicki@gmail.com>
+copyright:           2020 Marcin Rzeźnicki
+category:            Testing
+build-type:          Simple
+extra-doc-files:     README.md
+                     CHANGELOG.md
+tested-with:         GHC == 8.8.2
+
+source-repository head
+  type:                git
+  location:            https://github.com/marcin-rzeznicki/hspec-tables.git
+
+common common-options
+  build-depends:       base ^>= 4.13.0.0,
+                       hspec-core ==2.7.*
+  
+  ghc-options:         -Wall
+                       -Wno-type-defaults
+                       -Wcompat
+                       -Widentities
+                       -Wincomplete-uni-patterns
+                       -Wincomplete-record-updates
+                       -fdefer-typed-holes
+  if impl(ghc >= 8.0)
+    ghc-options:       -Wredundant-constraints
+  if impl(ghc >= 8.2)
+    ghc-options:       -fhide-source-paths
+  if impl(ghc >= 8.4)
+    ghc-options:       -Wmissing-export-lists
+                       -Wpartial-fields
+  if impl(ghc >= 8.8)
+    ghc-options:       -Wmissing-deriving-strategies
+
+  default-language:    Haskell2010
+
+library
+  import:              common-options
+  hs-source-dirs:      src
+  exposed-modules:     Test.Hspec.Tables
+
+test-suite hspec-tables-test
+  import:              common-options
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             Spec.hs
+  other-modules:       ExampleSpecs
+  build-depends:       hspec-tables,
+                       hspec ==2.7.*
+  ghc-options:         -threaded
+                       -rtsopts
+                       -with-rtsopts=-N
diff --git a/src/Test/Hspec/Tables.hs b/src/Test/Hspec/Tables.hs
new file mode 100644
--- /dev/null
+++ b/src/Test/Hspec/Tables.hs
@@ -0,0 +1,129 @@
+{-# 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
diff --git a/test/ExampleSpecs.hs b/test/ExampleSpecs.hs
new file mode 100644
--- /dev/null
+++ b/test/ExampleSpecs.hs
@@ -0,0 +1,34 @@
+module ExampleSpecs (exampleSpecs) where
+
+import Test.Hspec
+import Test.Hspec.Tables
+
+exampleSpecs :: Spec
+exampleSpecs = describe "Example specs" $ do
+  example1
+  example2
+
+example1 :: Spec
+example1 =
+  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)
+
+example2 :: Spec
+example2 =
+  describe "reverse" $
+    byExample
+      ("list", "reversed")
+      [("abc", "cba"), ("", ""), ("0123456", "6543210")]
+      (shouldBe . reverse)
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,95 @@
+{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE RecordWildCards #-}
+
+import Control.Monad (join)
+import Data.Foldable
+import Data.Functor ((<&>))
+import ExampleSpecs
+import Test.Hspec
+import Test.Hspec.Core.Runner (ColorMode (..), Config (..), Summary (..), defaultConfig, runSpec)
+import Test.Hspec.Core.Spec (Item (..), runSpecM)
+import Test.Hspec.Tables
+
+main :: IO ()
+main = hspec $ do
+  spec
+  exampleSpecs
+
+spec :: Spec
+spec = forM_ [spec2Bool, spec2BoolF, spec2Expectation, spec3, spec4, spec5, spec6, spec7] specMetaSpec
+
+data MetaSpec b = forall a. Show a => MetaSpec {label :: String, def :: [a], thisSpec :: SpecWith b, expectedFailures :: Int}
+
+withFailures :: MetaSpec a -> Int -> MetaSpec a
+withFailures meta failures = meta {expectedFailures = failures}
+
+describeMeta :: (Show a) => String -> [a] -> ([a] -> SpecWith b) -> MetaSpec b
+describeMeta label def generateSpec = MetaSpec label def generatedSpec 0
+  where
+    generatedSpec = generateSpec def
+
+spec2Bool :: MetaSpec ()
+spec2Bool = describeMeta
+  "spec2Bool"
+  [(0, 0), (1, 2), (2, 4), (3, 6), (4, 8)]
+  $ \examples -> byExample ("A", "expected") examples (\a expected -> expected == a * 2)
+
+spec2BoolF :: MetaSpec ()
+spec2BoolF =
+  describeMeta
+    "spec2BoolF"
+    [(0, 0), (1, 2), (2, 4), (3, 6), (4, 8)]
+    (\examples -> byExample ("A", "expected") examples (==))
+    `withFailures` 4
+
+spec2Expectation :: MetaSpec ()
+spec2Expectation = describeMeta
+  "spec2Expectation"
+  [(0, 0), (1, 2), (2, 4), (3, 6), (4, 8)]
+  $ \examples -> byExample ("A", "expected") examples (shouldBe . (* 2))
+
+spec3 :: MetaSpec ()
+spec3 = describeMeta
+  "spec3"
+  [(0, 0, 0), (1, 1, 2), (2, 2, 4), (3, 3, 6), (4, 4, 8), (5, 5, 10)]
+  $ \examples -> byExample ("A", "B", "expected") examples (\a b expected -> a + b == expected)
+
+spec4 :: MetaSpec ()
+spec4 = describeMeta
+  "spec4"
+  [('a', 'b', 'c', "abc"), ('g', 'e', 'f', "gef"), ('x', 'y', 'z', "xyz")]
+  $ \examples -> byExample ("A", "B", "C", "expected") examples (\a b c expected -> [a, b, c] `shouldBe` expected)
+
+spec5 :: MetaSpec ()
+spec5 = describeMeta
+  "spec5"
+  [('a', 'b', 'c', 'd', "abcd"), ('g', 'e', 'f', 'h', "gefh"), ('x', 'y', 'z', 'a', "xyza")]
+  $ \examples -> byExample ("A", "B", "C", "D", "expected") examples (\a b c d expected -> [a, b, c, d] `shouldBe` expected)
+
+spec6 :: MetaSpec ()
+spec6 = describeMeta
+  "spec6"
+  [(1, 1, 1, 1, 1, 1), (2, 2, 2, 2, 2, 32)]
+  $ \examples -> byExample ("A", "B", "C", "D", "E", "expected") examples (\a b c d e expected -> a * b * c * d * e == expected)
+
+spec7 :: MetaSpec ()
+spec7 = describeMeta
+  "spec7"
+  [(1, 1, 1, 1, 1, 1, 1), (2, 2, 2, 2, 2, 2, 64)]
+  $ \examples -> byExample ("A", "B", "C", "D", "E", "F", "expected") examples (\a b c d e f expected -> a * b * c * d * e * f == expected)
+
+_config :: Config
+_config = defaultConfig {configIgnoreConfigFile = True, configColorMode = ColorNever}
+
+specMetaSpec :: MetaSpec () -> Spec
+specMetaSpec MetaSpec {..} = describe label $ do
+  specify "Analysis" $ expectCorrectSpec def thisSpec
+  let expectedSummary = Summary (length def) expectedFailures
+  specify "Execution" $ runSpec thisSpec _config `shouldReturn` expectedSummary
+
+expectCorrectSpec :: Show a => [a] -> SpecWith b -> Expectation
+expectCorrectSpec def generatedSpec = runSpecM generatedSpec <&> getAllItems >>= (shouldMatchList labels . itemRequirements)
+  where
+    itemRequirements = map itemRequirement
+    getAllItems = join . map toList
+    labels = map show def
