packages feed

adhoc-fixtures-0.1.0.0: src/Data/Fixtures/Adhoc.hs

{-# LANGUAGE ConstraintKinds #-}
{-# OPTIONS_GHC -Wno-redundant-constraints #-}

-- |
-- Module        : Data.Fixtures.Adhoc
-- Copyright     : Gautier DI FOLCO
-- License       : ISC
--
-- Maintainer    : Gautier DI FOLCO <gautier.difolco@gmail.com>
-- Stability     : Unstable
-- Portability   : not portable
--
-- Fixtures builder and runner
--
-- Example:
--
-- > boxFixture ::
-- >   HasFixture items "tracker" Tracker =>
-- >   BuilderWith items IO "box" Box
-- > boxFixture =
-- >   buildWithClean
-- >   (\prev -> let box = Box 42 "box00" in addId box.boxKey box.boxId prev.tracker >> return box)
-- >   (\prev box -> rmId box.boxKey prev.tracker)
module Data.Fixtures.Adhoc
  ( Builder (..),
    BuilderWith,
    HasFixture,
    buildWith,
    buildWithClean,
    build,
    buildClean,
    nullBuilder,
    pureBuilder,
    (&:),
    (&>),
    runWithFixtures,
    createFixtures,
  )
where

import Control.Exception.Safe (MonadMask, bracket)
import Data.Records.Yarl.LinkedList
import GHC.TypeLits

-- | Fixture builder (should be used directly with care)
data Builder m items = Builder
  { create :: m (Record items),
    clean :: Record items -> m ()
  }

-- | Builder relying on other builder(s)
type BuilderWith items m (name :: Symbol) a =
  HasNotField name items =>
  Builder m items ->
  Builder m (Field name a ': items)

-- | Helper around 'HasRecord'
type HasFixture items (name :: Symbol) a = HasField name (Record items) a

-- | Simple builder, no clean operation
buildWith ::
  forall (name :: Symbol) a m items.
  (Monad m, HasNotField name items) =>
  (Record items -> m a) ->
  BuilderWith items m name a
buildWith f = buildWithClean f $ \_ _ -> return ()

-- | Builder with cleaning operation
buildWithClean ::
  forall (name :: Symbol) a m items.
  (Monad m, HasNotField name items) =>
  (Record items -> m a) ->
  (Record items -> a -> m ()) ->
  BuilderWith items m name a
buildWithClean create' clean' previous =
  Builder
    { create = do
        xs <- previous.create
        x <- create' xs
        return $ Field x :> xs,
      clean =
        \(Field x :> xs) ->
          clean' xs x >> previous.clean xs
    }

-- | Simple builder without dependency, no clean operation
build ::
  forall (name :: Symbol) a m items.
  (Monad m, HasNotField name items) =>
  m a ->
  BuilderWith items m name a
build f = buildClean f $ const $ return ()

-- | Builder without dependency with cleaning operation
buildClean ::
  forall (name :: Symbol) a m items.
  (Monad m, HasNotField name items) =>
  m a ->
  (a -> m ()) ->
  BuilderWith items m name a
buildClean create' clean' previous =
  Builder
    { create = do
        xs <- previous.create
        x <- create'
        return $ Field x :> xs,
      clean =
        \(Field x :> xs) ->
          clean' x >> previous.clean xs
    }

-- | Base builder
nullBuilder :: Monad m => Builder m '[]
nullBuilder =
  Builder
    { create = return RNil,
      clean = const $ return ()
    }

-- | Pure builder
pureBuilder :: Monad m => Record items -> Builder m items
pureBuilder built =
  Builder
    { create = return built,
      clean = const $ return ()
    }

-- | Chain builders
(&:) :: HasNotField name items => BuilderWith items m name a -> Builder m items -> Builder m (Field name a ': items)
(&:) = ($)

infixr 5 &:

-- | Nest builders
(&>) :: (HasNotField name items, Monad m) => BuilderWith items m name a -> Record items -> Builder m (Field name a ': items)
(&>) builderWith built = builderWith &: pureBuilder built

infixr 5 &>

-- | Run fixtures with clean up (bracket)
runWithFixtures :: MonadMask m => Builder m items -> (Record items -> m a) -> m a
runWithFixtures builder = bracket builder.create builder.clean

-- | Create fixtures (no clean up)
createFixtures :: Monad m => Builder m items -> (Record items -> m a) -> m a
createFixtures builder act = builder.create >>= act