graphula-2.1.0.0: src/Graphula.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilyDependencies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE UndecidableSuperClasses #-}
-- |
--
-- Graphula is a compact interface for generating data and linking its
-- dependencies. You can use this interface to generate fixtures for automated
-- testing.
--
-- @
-- {- config/models
--
-- School
-- name Text
-- deriving Generic
--
-- Teacher
-- schoolId SchoolId
-- name Text
-- deriving Generic
--
-- Course
-- schoolId SchoolId
-- teacherId TeacherId
-- name Text
-- deriving Generic
--
-- -}
--
-- instance Arbitrary School where
-- -- ...
--
-- instance Arbitrary Teacher where
-- -- ...
--
-- instance Arbitrary Course where
-- -- ...
--
-- instance 'HasDependencies' School
--
-- instance 'HasDependencies' Teacher where
-- type Dependencies Teacher = Only SchoolId
--
-- instance 'HasDependencies' Course where
-- type Dependencies Course = (SchoolId, CourseId)
--
-- 'runGraphulaT' runDB $ do
-- school <- 'node' \@School () mempty
--
-- teacher <- 'node' \@Teacher ('onlyKey' school)
-- $ edit
-- $ \t -> t { teacherName = \"Alice\" }
--
-- course <- 'node' \@Course ('keys' (school, teacher))
-- $ 'ensure'
-- $ not . courseIsArchived
-- @
module Graphula
( -- * Basic usage
-- ** Model requirements
HasDependencies (..)
, Only (..)
, only
-- ** Defining the graph
, node
, edit
, ensure
-- ** Running the graph
, GraphulaT
, runGraphulaT
, GenerationFailure (..)
-- * Advanced usage
-- ** Non-serial keys
, KeySourceType (..)
, nodeKeyed
-- ** Running with logging
, GraphulaLoggedT
, runGraphulaLoggedT
, runGraphulaLoggedWithFileT
-- ** Running idempotently
, GraphulaIdempotentT
, runGraphulaIdempotentT
-- * Useful synonymns
-- |
--
-- When declaring your own functions that call 'node', these synonyms can help
-- with the constraint soup.
--
-- > genSchoolWithTeacher
-- > :: GraphulaContext m '[School, Teacher]
-- > -> m (Entity Teacher)
-- > genSchoolWithTeacher = do
-- > school <- node @School () mempty
-- > node @Teacher (onlyKey school) mempty
, GraphulaContext
, GraphulaNode
-- * Lower-level details
-- |
--
-- These exports are likely to be removed from this module in a future
-- version. If you are using them, consider importing from their own modules.
, MonadGraphula
, MonadGraphulaBackend (..)
, MonadGraphulaFrontend (..)
, NodeOptions
, GenerateKey
, NoConstraint
) where
import Prelude hiding (readFile)
import Control.Monad.IO.Unlift
import Control.Monad.Reader (MonadReader, ReaderT, asks, runReaderT)
import Control.Monad.Trans (MonadTrans, lift)
import Data.IORef (IORef, newIORef)
import Data.Kind (Constraint, Type)
import Data.Typeable (Typeable)
import Database.Persist
( PersistEntity
, PersistEntityBackend
, checkUnique
, delete
, get
, getEntity
, insertUnique
)
import qualified Database.Persist as Persist
import Database.Persist.Sql (SqlBackend)
import Graphula.Class
import Graphula.Dependencies
import Graphula.Idempotent
import Graphula.Logged
import Graphula.NoConstraint
import Graphula.Node
import System.Random (randomIO)
import Test.HUnit.Lang
( FailureReason (..)
, HUnitFailure (..)
, formatFailureReason
)
import Test.QuickCheck (Arbitrary (..))
import Test.QuickCheck.Random (QCGen, mkQCGen)
import UnliftIO.Exception (catch, throwIO)
-- | A constraint over lists of nodes for 'MonadGraphula', and 'GraphulaNode'.
--
-- Helpful for defining utility functions over many nodes.
--
-- @
-- mkABC :: (GraphulaContext m '[A, B, C]) => m (Node m C)
-- mkABC = do
-- a <- node @A () mempty
-- b <- node @B (only a) mempty
-- node @C (a, b) $ edit $ \n ->
-- n { cc = "spanish" }
-- @
type family GraphulaContext (m :: Type -> Type) (ts :: [Type]) :: Constraint where
GraphulaContext m '[] = MonadGraphula m
GraphulaContext m (t ': ts) = (GraphulaNode m t, GraphulaContext m ts)
data Args backend n m = Args
{ dbRunner :: RunDB backend n m
, gen :: IORef QCGen
}
newtype RunDB backend n m = RunDB (forall b. ReaderT backend n b -> m b)
newtype GraphulaT n m a = GraphulaT {runGraphulaT' :: ReaderT (Args SqlBackend n m) m a}
deriving newtype
(Functor, Applicative, Monad, MonadIO, MonadReader (Args SqlBackend n m))
instance MonadTrans (GraphulaT n) where
lift = GraphulaT . lift
instance MonadUnliftIO m => MonadUnliftIO (GraphulaT n m) where
{-# INLINE withRunInIO #-}
withRunInIO inner =
GraphulaT $ withRunInIO $ \run -> inner $ run . runGraphulaT'
instance MonadIO m => MonadGraphulaBackend (GraphulaT n m) where
type Logging (GraphulaT n m) = NoConstraint
askGen = asks gen
logNode _ = pure ()
instance (MonadIO m, MonadIO n) => MonadGraphulaFrontend (GraphulaT n m) where
insert mKey n = do
RunDB runDB <- asks dbRunner
lift . runDB $ case mKey of
Nothing ->
insertUnique n >>= \case
Nothing -> pure Nothing
Just key -> getEntity key
Just key -> do
existingKey <- get key
whenNothing existingKey $ do
existingUnique <- checkUnique n
whenNothing existingUnique $ do
Persist.insertKey key n
getEntity key
insertKeyed key n = do
RunDB runDB <- asks dbRunner
lift . runDB $ do
existingKey <- get key
whenNothing existingKey $ do
existingUnique <- checkUnique n
whenNothing existingUnique $ do
Persist.insertKey key n
getEntity key
remove key = do
RunDB runDB <- asks dbRunner
lift . runDB $ delete key
whenNothing :: Applicative m => Maybe a -> m (Maybe b) -> m (Maybe b)
whenNothing Nothing f = f
whenNothing (Just _) _ = pure Nothing
runGraphulaT
:: MonadUnliftIO m
=> Maybe Int
-- ^ Optional seed
-> (forall b. ReaderT SqlBackend n b -> m b)
-- ^ Database runner
-> GraphulaT n m a
-> m a
runGraphulaT mSeed runDB action = do
seed <- maybe (liftIO randomIO) pure mSeed
qcGen <- liftIO $ newIORef $ mkQCGen seed
runReaderT (runGraphulaT' action) (Args (RunDB runDB) qcGen)
`catch` logFailingSeed seed
logFailingSeed :: MonadIO m => Int -> HUnitFailure -> m a
logFailingSeed seed = rethrowHUnitWith ("Graphula with seed: " ++ show seed)
rethrowHUnitWith :: MonadIO m => String -> HUnitFailure -> m a
rethrowHUnitWith message (HUnitFailure l r) =
throwIO . HUnitFailure l . Reason $ message ++ "\n\n" ++ formatFailureReason r
type GraphulaNode m a =
( HasDependencies a
, Logging m a
, PersistEntityBackend a ~ SqlBackend
, PersistEntity a
, Typeable a
, Arbitrary a
)