graphula
A declarative library for describing dependencies between data
https://github.com/freckle/graphula#readme
| Version on this page: | 2.0.0.5 | 
| LTS Haskell 24.17: | 2.1.2.0 | 
| Stackage Nightly 2025-10-31: | 2.1.2.0 | 
| Latest on Hackage: | 2.1.2.0 | 
graphula-2.0.0.5@sha256:be302441ebe8b3b76d6401ce8afddf350463742cb47e42ba69f8b24d59230a1f,2580Module documentation for 2.0.0.5
Graphula Core
Graphula is a simple interface for generating persistent data and linking its dependencies. We use this interface to generate fixtures for automated testing. The interface is extensible and supports pluggable front-ends.
Arbitrary Data
Graphula utilizes QuickCheck to generate random data. We need to declare Arbitrary instances for our types.
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
A
  a String
  b Int
  deriving Show Eq Generic
B
  a AId
  b String
  deriving Show Eq Generic
C
  a AId
  b BId
  c String
  deriving Show Eq Generic
D
  Id UUIDKey
  a Int
  b String
  deriving Show Eq Generic
E
  Id DId sqltype=uuid
  a String
  deriving Show Eq Generic
F
  a Bool
  UniqueFA a
  deriving Show Eq Generic
|]
instance Arbitrary A where
  arbitrary = A <$> arbitrary <*> arbitrary
instance Arbitrary B where
  arbitrary = B <$> arbitrary <*> arbitrary
instance Arbitrary C where
  arbitrary = C <$> arbitrary <*> arbitrary <*> arbitrary
instance Arbitrary D where
  arbitrary = D <$> arbitrary <*> arbitrary
instance Arbitrary E where
  arbitrary = E <$> arbitrary
instance Arbitrary F where
  arbitrary = F <$> arbitrary
Dependencies
We declare dependencies via the HasDependencies typeclass and its associated type Dependencies.
By default a type does not have any dependencies. We only need to declare an empty instance.
instance HasDependencies A
instance HasDependencies F
For single dependencies we use the Only type.
instance HasDependencies B where
  type Dependencies B = Only AId
Groups of dependencies use tuples. Declare these dependencies in the order they appear in the type. HasDependencies leverages generic programming to inject dependencies for you.
instance HasDependencies C where
  type Dependencies C = (AId, BId)
Non Sequential Keys
Graphula supports non-sequential keys with the KeySource associated type. To generate a key using
its Arbitrary instance, use 'SourceArbitrary. Non-serial keys will need to also derive
an overlapping Arbitrary instance.
instance HasDependencies D where
  type KeySource D = 'SourceArbitrary
deriving newtype instance {-# OVERLAPPING #-} Arbitrary (Key D)
You can also elect to always specify an external key using 'SourceExternal. This means that
this type cannot be constructed with node; use nodeKeyed instead.
instance HasDependencies E where
  type KeySource E = 'SourceExternal
By default, HasDependencies instances use type KeySource _ = 'SourceDefault, which means
that graphula will expect the database to provide a key.
Serialization
Graphula allows logging of graphs via runGraphulaLogged. Graphula dumps graphs
to a temp file on test failure.
loggingSpec :: IO ()
loggingSpec = do
  let
    logFile :: FilePath
    logFile = "test.graphula"
    -- We'd typically use `runGraphulaLogged` which utilizes a temp file.
    failingGraph :: IO ()
    failingGraph = runGraphulaT Nothing runDB . runGraphulaLoggedWithFileT logFile $ do
      Entity _ a <- node @A () $ edit $ \n ->
        n {aA = "success"}
      liftIO $ aA a `shouldBe` "failed"
  failingGraph
    `shouldThrow` anyException
  n <- lines <$> readFile "test.graphula"
  n `shouldSatisfy` (not . null)
Running It
simpleSpec :: IO ()
simpleSpec =
  runGraphulaT Nothing runDB $ do
    -- Type application is not necessary, but recommended for clarity.
    Entity aId _ <- node @A () mempty
    Entity bId b <- node @B (only aId) mempty
    Entity _ c <- node @C (aId, bId) $ edit $ \n -> n { cC = "edited" }
    Entity dId _ <- node @D () mempty
    Entity eId _ <- nodeKeyed @E (EKey dId) () mempty
    -- Do something with your data
    liftIO $ do
      cC c `shouldBe` "edited"
      cA c `shouldBe` bA b
      unEKey eId `shouldBe` dId
runGraphulaT carries frontend instructions. If we’d like to override them we need to declare our own frontend.
For example, a front-end that always fails to insert.
newtype GraphulaFailT m a = GraphulaFailT { runGraphulaFailT :: m a }
  deriving newtype (Functor, Applicative, Monad, MonadIO, MonadGraphulaBackend)
instance MonadGraphulaFrontend (GraphulaFailT m) where
  insert _ _ = pure Nothing
  remove = const (pure ())
insertionFailureSpec :: IO ()
insertionFailureSpec = do
  let
    failingGraph :: IO ()
    failingGraph =  runGraphulaT Nothing runDB . runGraphulaFailT $ do
      Entity _ _ <- node @A () mempty
      pure ()
  failingGraph
    `shouldThrow` (== (GenerationFailureMaxAttemptsToInsert (typeRep $ Proxy @A)))
Note that graphula can fail naturally if we define a graph that violates unique constraints in the database:
constraintFailureSpec :: IO ()
constraintFailureSpec = do
  let
    failingGraph :: IO ()
    failingGraph =  runGraphulaT Nothing runDB $
      replicateM_ 3 $ node @F () mempty
  failingGraph
    `shouldThrow` (== (GenerationFailureMaxAttemptsToInsert (typeRep $ Proxy @F)))
or if we define a graph with an unsatisfiable predicates:
ensureFailureSpec :: IO ()
ensureFailureSpec = do
  let
    failingGraph :: IO ()
    failingGraph =  runGraphulaT Nothing runDB $ do
      Entity _ _ <- node @A () $ ensure $ \a -> a /= a
      pure ()
  failingGraph
    `shouldThrow` (== (GenerationFailureMaxAttemptsToConstrain (typeRep $ Proxy @A)))
