packages feed

graphula 2.0.0.5 → 2.0.1.0

raw patch · 16 files changed

+1121/−993 lines, 16 filesdep +generic-arbitrarydep −uuidPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: generic-arbitrary

Dependencies removed: uuid

API changes (from Hackage documentation)

- Graphula: -- <a>dependsOn</a> supports tuples as <a>Dependencies</a>. Data types
- Graphula: -- <a>dependsOn</a>. The default generic implementation of
- Graphula: -- <tt><a>SourceArbitrary</a></tt>. Only use
- Graphula: -- <tt><a>SourceExternal</a></tt> if the key for a value is always
- Graphula: -- definition of <tt>a</tt>.
- Graphula: -- nodes as <a>Show</a>n <a>Text</a> values
- Graphula: -- note: The contents of a tuple must be ordered as they appear in the
- Graphula: -- persistence. This is used by <tt>runGraphulaLogged</tt> to store graph
- Graphula: -- with a single dependency should use <a>Only</a> as a 1-tuple.
- Graphula: instance (Control.Monad.IO.Class.MonadIO m, GHC.Base.Applicative n, Control.Monad.IO.Class.MonadIO n) => Graphula.MonadGraphulaFrontend (Graphula.GraphulaT n m)
- Graphula: instance (Control.Monad.IO.Class.MonadIO m, Graphula.MonadGraphulaFrontend m) => Graphula.MonadGraphulaFrontend (Graphula.GraphulaIdempotentT m)
- Graphula: instance (GHC.Base.Monad m, Graphula.MonadGraphulaFrontend m) => Graphula.MonadGraphulaFrontend (Graphula.GraphulaLoggedT m)
- Graphula: instance (Graphula.Internal.GenerateKeyInternal (Graphula.KeySource a) a, Graphula.Internal.KeyConstraint (Graphula.KeySource a) a) => Graphula.GenerateKey a
- Graphula: instance (Graphula.Internal.MonadGraphulaBackend m, Control.Monad.IO.Class.MonadIO m) => Graphula.Internal.MonadGraphulaBackend (Graphula.GraphulaLoggedT m)
- Graphula: instance Control.Monad.IO.Class.MonadIO m => Control.Monad.IO.Class.MonadIO (Graphula.GraphulaIdempotentT m)
- Graphula: instance Control.Monad.IO.Class.MonadIO m => Control.Monad.IO.Class.MonadIO (Graphula.GraphulaLoggedT m)
- Graphula: instance Control.Monad.IO.Class.MonadIO m => Graphula.Internal.MonadGraphulaBackend (Graphula.GraphulaT n m)
- Graphula: instance Control.Monad.IO.Unlift.MonadUnliftIO m => Control.Monad.IO.Unlift.MonadUnliftIO (Graphula.GraphulaIdempotentT m)
- Graphula: instance Control.Monad.Trans.Class.MonadTrans Graphula.GraphulaIdempotentT
- Graphula: instance Control.Monad.Trans.Class.MonadTrans Graphula.GraphulaLoggedT
- Graphula: instance Data.Foldable.Foldable Graphula.Only
- Graphula: instance Data.Traversable.Traversable Graphula.Only
- Graphula: instance GHC.Base.Applicative m => GHC.Base.Applicative (Graphula.GraphulaIdempotentT m)
- Graphula: instance GHC.Base.Applicative m => GHC.Base.Applicative (Graphula.GraphulaLoggedT m)
- Graphula: instance GHC.Base.Functor Graphula.Only
- Graphula: instance GHC.Base.Functor m => GHC.Base.Functor (Graphula.GraphulaIdempotentT m)
- Graphula: instance GHC.Base.Functor m => GHC.Base.Functor (Graphula.GraphulaLoggedT m)
- Graphula: instance GHC.Base.Monad m => Control.Monad.Reader.Class.MonadReader (GHC.IORef.IORef (Data.Sequence.Internal.Seq Data.Text.Internal.Text)) (Graphula.GraphulaLoggedT m)
- Graphula: instance GHC.Base.Monad m => Control.Monad.Reader.Class.MonadReader (GHC.IORef.IORef (m ())) (Graphula.GraphulaIdempotentT m)
- Graphula: instance GHC.Base.Monad m => GHC.Base.Monad (Graphula.GraphulaIdempotentT m)
- Graphula: instance GHC.Base.Monad m => GHC.Base.Monad (Graphula.GraphulaLoggedT m)
- Graphula: instance GHC.Base.Monad m => GHC.Base.Monoid (Graphula.Kendo m a)
- Graphula: instance GHC.Base.Monad m => GHC.Base.Semigroup (Graphula.Kendo m a)
- Graphula: instance GHC.Base.Monoid (Graphula.NodeOptions a)
- Graphula: instance GHC.Base.Semigroup (Graphula.NodeOptions a)
- Graphula: instance GHC.Classes.Eq Graphula.GenerationFailure
- Graphula: instance GHC.Classes.Eq a => GHC.Classes.Eq (Graphula.Only a)
- Graphula: instance GHC.Classes.Ord a => GHC.Classes.Ord (Graphula.Only a)
- Graphula: instance GHC.Exception.Type.Exception Graphula.GenerationFailure
- Graphula: instance GHC.Generics.Generic (Graphula.Kendo m a)
- Graphula: instance GHC.Generics.Generic (Graphula.NodeOptions a)
- Graphula: instance GHC.Generics.Generic (Graphula.Only a)
- Graphula: instance GHC.Show.Show Graphula.GenerationFailure
- Graphula: instance GHC.Show.Show a => GHC.Show.Show (Graphula.Only a)
- Graphula.Internal: -- nodes as <a>Show</a>n <a>Text</a> values
- Graphula.Internal: -- persistence. This is used by <tt>runGraphulaLogged</tt> to store graph
- Graphula.Internal: -- | A constraint provided to log details of the graph to some form of
- Graphula.Internal: SourceArbitrary :: KeySourceType
- Graphula.Internal: SourceDefault :: KeySourceType
- Graphula.Internal: SourceExternal :: KeySourceType
- Graphula.Internal: askGen :: MonadGraphulaBackend m => m (IORef QCGen)
- Graphula.Internal: class GHasDependencies nodeTyProxy depsTyProxy node deps
- Graphula.Internal: class GenerateKeyInternal (s :: KeySourceType) a where {
- Graphula.Internal: class MonadGraphulaBackend m where {
- Graphula.Internal: class NoConstraint a
- Graphula.Internal: data KeySourceType
- Graphula.Internal: generateKey :: (GenerateKeyInternal s a, KeyConstraint s a) => Gen (Maybe (Key a))
- Graphula.Internal: genericDependsOn :: GHasDependencies nodeTyProxy depsTyProxy node deps => nodeTyProxy -> depsTyProxy -> node -> deps -> node
- Graphula.Internal: instance (Graphula.Internal.FindMatches nodeTy depsTy node deps GHC.Types.~ fields, Graphula.Internal.GHasDependenciesRecursive (Data.Proxy.Proxy fields) node deps) => Graphula.Internal.GHasDependencies (Data.Proxy.Proxy nodeTy) (Data.Proxy.Proxy depsTy) (Data.Either.Either node Data.Void.Void) (Data.Either.Either deps Data.Void.Void)
- Graphula.Internal: instance (TypeError ...) => Graphula.Internal.GHasDependencies (Data.Proxy.Proxy nodeTy) (Data.Proxy.Proxy depsTy) (Data.Either.Either left (Data.Either.Either right rest)) (Data.Either.Either deps Data.Void.Void)
- Graphula.Internal: instance (TypeError ...) => Graphula.Internal.GHasDependencies (Data.Proxy.Proxy nodeTy) (Data.Proxy.Proxy depsTy) (Data.Either.Either left1 (Data.Either.Either right1 rest1)) (Data.Either.Either left2 (Data.Either.Either right2 rest2))
- Graphula.Internal: instance (TypeError ...) => Graphula.Internal.GHasDependencies (Data.Proxy.Proxy nodeTy) (Data.Proxy.Proxy depsTy) (Data.Either.Either node Data.Void.Void) (Data.Either.Either left (Data.Either.Either right rest))
- Graphula.Internal: instance (TypeError ...) => Graphula.Internal.GHasDependencies (Data.Proxy.Proxy nodeTy) (Data.Proxy.Proxy depsTy) Data.Void.Void (Data.Either.Either deps rest)
- Graphula.Internal: instance (TypeError ...) => Graphula.Internal.GHasDependencies (Data.Proxy.Proxy nodeTy) (Data.Proxy.Proxy depsTy) node Data.Void.Void
- Graphula.Internal: instance (TypeError ...) => Graphula.Internal.GenerateKeyInternal 'Graphula.Internal.SourceExternal a
- Graphula.Internal: instance (a GHC.Types.~ dep, Graphula.Internal.GHasDependenciesRecursive (Data.Proxy.Proxy fields) as deps) => Graphula.Internal.GHasDependenciesRecursive (Data.Proxy.Proxy ('Graphula.Internal.Match a : fields)) (a, as) (dep, deps)
- Graphula.Internal: instance Graphula.Internal.GHasDependencies (Data.Proxy.Proxy nodeTy) (Data.Proxy.Proxy depsTy) Data.Void.Void (Data.Either.Either () Data.Void.Void)
- Graphula.Internal: instance Graphula.Internal.GHasDependenciesRecursive (Data.Proxy.Proxy '[]) () ()
- Graphula.Internal: instance Graphula.Internal.GHasDependenciesRecursive (Data.Proxy.Proxy fields) as deps => Graphula.Internal.GHasDependenciesRecursive (Data.Proxy.Proxy ('Graphula.Internal.NoMatch a : fields)) (a, as) deps
- Graphula.Internal: instance Graphula.Internal.GenerateKeyInternal 'Graphula.Internal.SourceArbitrary a
- Graphula.Internal: instance Graphula.Internal.GenerateKeyInternal 'Graphula.Internal.SourceDefault a
- Graphula.Internal: instance Graphula.Internal.NoConstraint a
- Graphula.Internal: logNode :: (MonadGraphulaBackend m, Logging m a) => a -> m ()
- Graphula.Internal: type family KeyConstraint s a :: Constraint;
- Graphula.Internal: }
- Graphula.Key: instance Graphula.Key.EntityKeys (Graphula.Only (Database.Persist.Class.PersistEntity.Entity a))
+ Graphula: -- Models with multiple dependencies use tuple syntax,
+ Graphula: -- Only use <a>SourceExternal</a> if the key for a value is always
+ Graphula: -- constructor,
+ Graphula: -- instance <a>HasDependencies</a> Course where
+ Graphula: -- type Dependencies Course = (SchoolId, TeacherId)
+ Graphula: instance (Control.Monad.IO.Class.MonadIO m, GHC.Base.Applicative n, Control.Monad.IO.Class.MonadIO n) => Graphula.Class.MonadGraphulaFrontend (Graphula.GraphulaT n m)
+ Graphula: instance Control.Monad.IO.Class.MonadIO m => Graphula.Class.MonadGraphulaBackend (Graphula.GraphulaT n m)
+ Graphula.Class: askGen :: MonadGraphulaBackend m => m (IORef QCGen)
+ Graphula.Class: class MonadGraphulaBackend m where {
+ Graphula.Class: class MonadGraphulaFrontend m
+ Graphula.Class: insert :: (MonadGraphulaFrontend m, PersistEntityBackend a ~ SqlBackend, PersistEntity a, Monad m) => Maybe (Key a) -> a -> m (Maybe (Entity a))
+ Graphula.Class: logNode :: (MonadGraphulaBackend m, Logging m a) => a -> m ()
+ Graphula.Class: remove :: (MonadGraphulaFrontend m, PersistEntityBackend a ~ SqlBackend, PersistEntity a, Monad m) => Key a -> m ()
+ Graphula.Class: type MonadGraphula m = (Monad m, MonadIO m, MonadGraphulaBackend m, MonadGraphulaFrontend m)
+ Graphula.Class: type family Logging m :: Type -> Constraint;
+ Graphula.Class: }
+ Graphula.Dependencies: --
+ Graphula.Dependencies: -- 'SourceArbitrary -- automatically generate keys using <tt><a>Arbitrary</a></tt>
+ Graphula.Dependencies: -- 'SourceDefault -- automatically generate keys from the database
+ Graphula.Dependencies: -- 'SourceExternal -- explicitly pass a key using <tt><tt>nodeKeyed</tt></tt>
+ Graphula.Dependencies: -- </pre>
+ Graphula.Dependencies: -- <pre>
+ Graphula.Dependencies: -- Models with multiple dependencies use tuple syntax,
+ Graphula.Dependencies: -- Most types will use <a>SourceDefault</a> or <a>SourceArbitrary</a>.
+ Graphula.Dependencies: -- Only use <a>SourceExternal</a> if the key for a value is always
+ Graphula.Dependencies: -- This can be
+ Graphula.Dependencies: -- constructor,
+ Graphula.Dependencies: -- defined externally.
+ Graphula.Dependencies: -- instance <a>HasDependencies</a> Course where
+ Graphula.Dependencies: -- type Dependencies Course = (SchoolId, TeacherId)
+ Graphula.Dependencies: -- | Specify the method for resolving a node's key
+ Graphula.Dependencies: Only :: a -> Only a
+ Graphula.Dependencies: SourceArbitrary :: KeySourceType
+ Graphula.Dependencies: SourceDefault :: KeySourceType
+ Graphula.Dependencies: SourceExternal :: KeySourceType
+ Graphula.Dependencies: [fromOnly] :: Only a -> a
+ Graphula.Dependencies: class (GenerateKeyInternal (KeySource a) a, KeyConstraint (KeySource a) a) => GenerateKey a
+ Graphula.Dependencies: class HasDependencies a where {
+ Graphula.Dependencies: data KeySourceType
+ Graphula.Dependencies: dependsOn :: (HasDependencies a, HasEot a, HasEot (Dependencies a), GHasDependencies (Proxy a) (Proxy (Dependencies a)) (Eot a) (Eot (Dependencies a))) => a -> Dependencies a -> a
+ Graphula.Dependencies: generateKey :: (GenerateKeyInternal s a, KeyConstraint s a) => Gen (Maybe (Key a))
+ Graphula.Dependencies: instance (Graphula.Dependencies.GenerateKeyInternal (Graphula.Dependencies.KeySource a) a, Graphula.Dependencies.KeyConstraint (Graphula.Dependencies.KeySource a) a) => Graphula.Dependencies.GenerateKey a
+ Graphula.Dependencies: instance (TypeError ...) => Graphula.Dependencies.GenerateKeyInternal 'Graphula.Dependencies.SourceExternal a
+ Graphula.Dependencies: instance Data.Foldable.Foldable Graphula.Dependencies.Only
+ Graphula.Dependencies: instance Data.Traversable.Traversable Graphula.Dependencies.Only
+ Graphula.Dependencies: instance GHC.Base.Functor Graphula.Dependencies.Only
+ Graphula.Dependencies: instance GHC.Classes.Eq a => GHC.Classes.Eq (Graphula.Dependencies.Only a)
+ Graphula.Dependencies: instance GHC.Classes.Ord a => GHC.Classes.Ord (Graphula.Dependencies.Only a)
+ Graphula.Dependencies: instance GHC.Generics.Generic (Graphula.Dependencies.Only a)
+ Graphula.Dependencies: instance GHC.Show.Show a => GHC.Show.Show (Graphula.Dependencies.Only a)
+ Graphula.Dependencies: instance Graphula.Dependencies.GenerateKeyInternal 'Graphula.Dependencies.SourceArbitrary a
+ Graphula.Dependencies: instance Graphula.Dependencies.GenerateKeyInternal 'Graphula.Dependencies.SourceDefault a
+ Graphula.Dependencies: newtype Only a
+ Graphula.Dependencies: only :: a -> Only a
+ Graphula.Dependencies: type Dependencies _a = ();
+ Graphula.Dependencies: type KeySource _a = 'SourceDefault;
+ Graphula.Dependencies: type family KeySource a :: KeySourceType;
+ Graphula.Dependencies: }
+ Graphula.Dependencies.Generic: class GHasDependencies nodeTyProxy depsTyProxy node deps
+ Graphula.Dependencies.Generic: genericDependsOn :: GHasDependencies nodeTyProxy depsTyProxy node deps => nodeTyProxy -> depsTyProxy -> node -> deps -> node
+ Graphula.Dependencies.Generic: instance (Graphula.Dependencies.Generic.FindMatches nodeTy depsTy node deps GHC.Types.~ fields, Graphula.Dependencies.Generic.GHasDependenciesRecursive (Data.Proxy.Proxy fields) node deps) => Graphula.Dependencies.Generic.GHasDependencies (Data.Proxy.Proxy nodeTy) (Data.Proxy.Proxy depsTy) (Data.Either.Either node Data.Void.Void) (Data.Either.Either deps Data.Void.Void)
+ Graphula.Dependencies.Generic: instance (TypeError ...) => Graphula.Dependencies.Generic.GHasDependencies (Data.Proxy.Proxy nodeTy) (Data.Proxy.Proxy depsTy) (Data.Either.Either left (Data.Either.Either right rest)) (Data.Either.Either deps Data.Void.Void)
+ Graphula.Dependencies.Generic: instance (TypeError ...) => Graphula.Dependencies.Generic.GHasDependencies (Data.Proxy.Proxy nodeTy) (Data.Proxy.Proxy depsTy) (Data.Either.Either left1 (Data.Either.Either right1 rest1)) (Data.Either.Either left2 (Data.Either.Either right2 rest2))
+ Graphula.Dependencies.Generic: instance (TypeError ...) => Graphula.Dependencies.Generic.GHasDependencies (Data.Proxy.Proxy nodeTy) (Data.Proxy.Proxy depsTy) (Data.Either.Either node Data.Void.Void) (Data.Either.Either left (Data.Either.Either right rest))
+ Graphula.Dependencies.Generic: instance (TypeError ...) => Graphula.Dependencies.Generic.GHasDependencies (Data.Proxy.Proxy nodeTy) (Data.Proxy.Proxy depsTy) Data.Void.Void (Data.Either.Either deps rest)
+ Graphula.Dependencies.Generic: instance (TypeError ...) => Graphula.Dependencies.Generic.GHasDependencies (Data.Proxy.Proxy nodeTy) (Data.Proxy.Proxy depsTy) node Data.Void.Void
+ Graphula.Dependencies.Generic: instance (a GHC.Types.~ dep, Graphula.Dependencies.Generic.GHasDependenciesRecursive (Data.Proxy.Proxy fields) as deps) => Graphula.Dependencies.Generic.GHasDependenciesRecursive (Data.Proxy.Proxy ('Graphula.Dependencies.Generic.Match a : fields)) (a, as) (dep, deps)
+ Graphula.Dependencies.Generic: instance Graphula.Dependencies.Generic.GHasDependencies (Data.Proxy.Proxy nodeTy) (Data.Proxy.Proxy depsTy) Data.Void.Void (Data.Either.Either () Data.Void.Void)
+ Graphula.Dependencies.Generic: instance Graphula.Dependencies.Generic.GHasDependenciesRecursive (Data.Proxy.Proxy '[]) () ()
+ Graphula.Dependencies.Generic: instance Graphula.Dependencies.Generic.GHasDependenciesRecursive (Data.Proxy.Proxy fields) as deps => Graphula.Dependencies.Generic.GHasDependenciesRecursive (Data.Proxy.Proxy ('Graphula.Dependencies.Generic.NoMatch a : fields)) (a, as) deps
+ Graphula.Idempotent: data GraphulaIdempotentT m a
+ Graphula.Idempotent: instance (Control.Monad.IO.Class.MonadIO m, Graphula.Class.MonadGraphulaFrontend m) => Graphula.Class.MonadGraphulaFrontend (Graphula.Idempotent.GraphulaIdempotentT m)
+ Graphula.Idempotent: instance Control.Monad.IO.Class.MonadIO m => Control.Monad.IO.Class.MonadIO (Graphula.Idempotent.GraphulaIdempotentT m)
+ Graphula.Idempotent: instance Control.Monad.IO.Unlift.MonadUnliftIO m => Control.Monad.IO.Unlift.MonadUnliftIO (Graphula.Idempotent.GraphulaIdempotentT m)
+ Graphula.Idempotent: instance Control.Monad.Trans.Class.MonadTrans Graphula.Idempotent.GraphulaIdempotentT
+ Graphula.Idempotent: instance GHC.Base.Applicative m => GHC.Base.Applicative (Graphula.Idempotent.GraphulaIdempotentT m)
+ Graphula.Idempotent: instance GHC.Base.Functor m => GHC.Base.Functor (Graphula.Idempotent.GraphulaIdempotentT m)
+ Graphula.Idempotent: instance GHC.Base.Monad m => Control.Monad.Reader.Class.MonadReader (GHC.IORef.IORef (m ())) (Graphula.Idempotent.GraphulaIdempotentT m)
+ Graphula.Idempotent: instance GHC.Base.Monad m => GHC.Base.Monad (Graphula.Idempotent.GraphulaIdempotentT m)
+ Graphula.Idempotent: runGraphulaIdempotentT :: MonadUnliftIO m => GraphulaIdempotentT m a -> m a
+ Graphula.Key: instance Graphula.Key.EntityKeys (Graphula.Dependencies.Only (Database.Persist.Class.PersistEntity.Entity a))
+ Graphula.Logged: data GraphulaLoggedT m a
+ Graphula.Logged: instance (GHC.Base.Monad m, Graphula.Class.MonadGraphulaFrontend m) => Graphula.Class.MonadGraphulaFrontend (Graphula.Logged.GraphulaLoggedT m)
+ Graphula.Logged: instance (Graphula.Class.MonadGraphulaBackend m, Control.Monad.IO.Class.MonadIO m) => Graphula.Class.MonadGraphulaBackend (Graphula.Logged.GraphulaLoggedT m)
+ Graphula.Logged: instance Control.Monad.IO.Class.MonadIO m => Control.Monad.IO.Class.MonadIO (Graphula.Logged.GraphulaLoggedT m)
+ Graphula.Logged: instance Control.Monad.Trans.Class.MonadTrans Graphula.Logged.GraphulaLoggedT
+ Graphula.Logged: instance GHC.Base.Applicative m => GHC.Base.Applicative (Graphula.Logged.GraphulaLoggedT m)
+ Graphula.Logged: instance GHC.Base.Functor m => GHC.Base.Functor (Graphula.Logged.GraphulaLoggedT m)
+ Graphula.Logged: instance GHC.Base.Monad m => Control.Monad.Reader.Class.MonadReader (GHC.IORef.IORef (Data.Sequence.Internal.Seq Data.Text.Internal.Text)) (Graphula.Logged.GraphulaLoggedT m)
+ Graphula.Logged: instance GHC.Base.Monad m => GHC.Base.Monad (Graphula.Logged.GraphulaLoggedT m)
+ Graphula.Logged: runGraphulaLoggedT :: MonadUnliftIO m => GraphulaLoggedT m a -> m a
+ Graphula.Logged: runGraphulaLoggedUsingT :: MonadUnliftIO m => (IORef (Seq Text) -> HUnitFailure -> m a) -> GraphulaLoggedT m a -> m a
+ Graphula.Logged: runGraphulaLoggedWithFileT :: MonadUnliftIO m => FilePath -> GraphulaLoggedT m a -> m a
+ Graphula.NoConstraint: class NoConstraint a
+ Graphula.NoConstraint: instance Graphula.NoConstraint.NoConstraint a
+ Graphula.Node: GenerationFailureMaxAttemptsToConstrain :: TypeRep -> GenerationFailure
+ Graphula.Node: GenerationFailureMaxAttemptsToInsert :: TypeRep -> GenerationFailure
+ Graphula.Node: data GenerationFailure
+ Graphula.Node: data NodeOptions a
+ Graphula.Node: edit :: (a -> a) -> NodeOptions a
+ Graphula.Node: ensure :: (a -> Bool) -> NodeOptions a
+ Graphula.Node: instance GHC.Base.Monad m => GHC.Base.Monoid (Graphula.Node.Kendo m a)
+ Graphula.Node: instance GHC.Base.Monad m => GHC.Base.Semigroup (Graphula.Node.Kendo m a)
+ Graphula.Node: instance GHC.Base.Monoid (Graphula.Node.NodeOptions a)
+ Graphula.Node: instance GHC.Base.Semigroup (Graphula.Node.NodeOptions a)
+ Graphula.Node: instance GHC.Classes.Eq Graphula.Node.GenerationFailure
+ Graphula.Node: instance GHC.Exception.Type.Exception Graphula.Node.GenerationFailure
+ Graphula.Node: instance GHC.Generics.Generic (Graphula.Node.Kendo m a)
+ Graphula.Node: instance GHC.Generics.Generic (Graphula.Node.NodeOptions a)
+ Graphula.Node: instance GHC.Show.Show Graphula.Node.GenerationFailure
+ Graphula.Node: node :: forall a m. (MonadGraphula m, Logging m a, Arbitrary a, HasDependencies a, GenerateKey a, PersistEntityBackend a ~ SqlBackend, PersistEntity a, Typeable a) => Dependencies a -> NodeOptions a -> m (Entity a)
+ Graphula.Node: nodeKeyed :: forall a m. (MonadGraphula m, Logging m a, Arbitrary a, HasDependencies a, PersistEntityBackend a ~ SqlBackend, PersistEntity a, Typeable a) => Key a -> Dependencies a -> NodeOptions a -> m (Entity a)
- Graphula: -- 'SourceExternal -- explicitly pass a key using <tt><a>nodeKeyed</a></tt>
+ Graphula: -- 'SourceExternal -- explicitly pass a key using <tt><tt>nodeKeyed</tt></tt>
- Graphula: -- Most types will use <tt><a>SourceDefault</a></tt> or
+ Graphula: -- Most types will use <a>SourceDefault</a> or <a>SourceArbitrary</a>.
- Graphula: -- | A constraint provided to log details of the graph to some form of
+ Graphula: -- | Specify the method for resolving a node's key
- Graphula: node :: forall a m. (GraphulaContext m '[a], GenerateKey a) => Dependencies a -> NodeOptions a -> m (Entity a)
+ Graphula: node :: forall a m. (MonadGraphula m, Logging m a, Arbitrary a, HasDependencies a, GenerateKey a, PersistEntityBackend a ~ SqlBackend, PersistEntity a, Typeable a) => Dependencies a -> NodeOptions a -> m (Entity a)
- Graphula: nodeKeyed :: forall a m. GraphulaContext m '[a] => Key a -> Dependencies a -> NodeOptions a -> m (Entity a)
+ Graphula: nodeKeyed :: forall a m. (MonadGraphula m, Logging m a, Arbitrary a, HasDependencies a, PersistEntityBackend a ~ SqlBackend, PersistEntity a, Typeable a) => Key a -> Dependencies a -> NodeOptions a -> m (Entity a)
- Graphula: type MonadGraphula m = (Monad m, MonadGraphulaBackend m, MonadGraphulaFrontend m, MonadIO m)
+ Graphula: type MonadGraphula m = (Monad m, MonadIO m, MonadGraphulaBackend m, MonadGraphulaFrontend m)

Files

CHANGELOG.md view
@@ -1,6 +1,11 @@-## [*Unreleased*](https://github.com/freckle/graphula/compare/v2.0.0.5...main)+## [*Unreleased*](https://github.com/freckle/graphula/compare/v2.0.1.0...main)  None++## [v2.0.1.0](https://github.com/freckle/graphula/compare/v2.0.0.5...v2.0.1.0)++- Add/improve documentation+- Re-organize previously-internal module  ## [v2.0.0.5](https://github.com/freckle/graphula/compare/v2.0.0.4...v2.0.0.5) 
README.md view
@@ -1,6 +1,7 @@-# Graphula Core+# Graphula -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.+Graphula is a simple interface for generating persistent data and linking its+dependencies. We use this interface to generate fixtures for automated testing.   <!--@@ -24,7 +25,6 @@  module Main (module Main) where -import Control.Monad (replicateM_) import Control.Monad.IO.Class import Control.Monad.IO.Unlift import Control.Monad.Logger (NoLoggingT)@@ -32,12 +32,11 @@ import Control.Monad.Trans.Resource (ResourceT) import Database.Persist.Sqlite import Database.Persist.TH-import Data.Typeable import GHC.Generics (Generic) import Graphula-import Graphula.UUIDKey import Test.Hspec import Test.QuickCheck+import Test.QuickCheck.Arbitrary.Generic import Text.Markdown.Unlit ()  instance (ToBackendKey SqlBackend a) => Arbitrary (Key a) where@@ -47,115 +46,98 @@  ## Arbitrary Data -Graphula utilizes `QuickCheck` to generate random data. We need to declare `Arbitrary` instances for our types.+Graphula utilizes `QuickCheck` to generate random data. We need to declare+`Arbitrary` instances for our models.  ```haskell share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|-A-  a String-  b Int+School+  name String   deriving Show Eq Generic -B-  a AId-  b String+Teacher+  schoolId SchoolId+  name String   deriving Show Eq Generic -C-  a AId-  b BId-  c String+Course+  schoolId SchoolId+  teacherId TeacherId+  name String   deriving Show Eq Generic -D-  Id UUIDKey-  a Int-  b String+Student+  name String   deriving Show Eq Generic -E-  Id DId sqltype=uuid-  a String+Question+  content String   deriving Show Eq Generic -F-  a Bool-  UniqueFA a+Answer+  questionId QuestionId+  studentId StudentId+  yes Bool+  UniqueAnswer questionId studentId   deriving Show Eq Generic |] -instance Arbitrary A where-  arbitrary = A <$> arbitrary <*> arbitrary+instance Arbitrary School where+  arbitrary = genericArbitrary -instance Arbitrary B where-  arbitrary = B <$> arbitrary <*> arbitrary+instance Arbitrary Teacher where+  arbitrary = genericArbitrary -instance Arbitrary C where-  arbitrary = C <$> arbitrary <*> arbitrary <*> arbitrary+instance Arbitrary Course where+  arbitrary = genericArbitrary -instance Arbitrary D where-  arbitrary = D <$> arbitrary <*> arbitrary+instance Arbitrary Student where+  arbitrary = genericArbitrary -instance Arbitrary E where-  arbitrary = E <$> arbitrary+instance Arbitrary Question where+  arbitrary = genericArbitrary -instance Arbitrary F where-  arbitrary = F <$> arbitrary+instance Arbitrary Answer where+  arbitrary = genericArbitrary ```  ## 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.+We declare dependencies via the `HasDependencies` typeclass and its associated+type `Dependencies`. If a model does not have any dependencies, we only need to+declare an empty instance.  ```haskell-instance HasDependencies A-instance HasDependencies F-```+instance HasDependencies School -For single dependencies we use the `Only` type.+instance HasDependencies Student -```haskell-instance HasDependencies B where-  type Dependencies B = Only AId+instance HasDependencies Question ``` -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.+For single-dependency models, we use the `Only` type.  ```haskell-instance HasDependencies C where-  type Dependencies C = (AId, BId)+instance HasDependencies Teacher where+  type Dependencies Teacher = Only SchoolId ``` -## 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.+Multi-dependency models use tuples. Declare these dependencies in the order they+appear in the model's type definition. `HasDependencies` leverages generic+programming to inject dependencies for you.  ```haskell-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 Course where+  type Dependencies Course = (SchoolId, TeacherId) -```haskell-instance HasDependencies E where-  type KeySource E = 'SourceExternal+instance HasDependencies Answer where+  type Dependencies Answer = (QuestionId, StudentId) ``` -By default, `HasDependencies` instances use `type KeySource _ = 'SourceDefault`, which means-that graphula will expect the database to provide a key.--## Serialization+## Logging failures -Graphula allows logging of graphs via `runGraphulaLogged`. Graphula dumps graphs-to a temp file on test failure.+`runGraphulaLogged` will dump generated data to a temporary file. Or+`runGraphulaLoggedWithFileT` can be used to pass an explicit path.  ```haskell loggingSpec :: IO ()@@ -164,17 +146,20 @@     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"+      student <- node @Student () mempty+      question <- node @Question () mempty+      answer <- node @Answer+        (entityKey question, entityKey student)+        $ edit $ \a -> a { answerYes = True } -  failingGraph-    `shouldThrow` anyException+      -- Test failures will cause the graph to be logged (not any exception)+      liftIO $ answerYes (entityVal answer) `shouldBe` False -  n <- lines <$> readFile "test.graphula"+  failingGraph `shouldThrow` anyException++  n <- lines <$> readFile logFile   n `shouldSatisfy` (not . null) ``` @@ -184,69 +169,23 @@ 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+    school <- node @School () mempty+    teacher <- node @Teacher (Only $ entityKey school) mempty+    course <- node @Course (entityKey school, entityKey teacher) mempty+    student <- node @Student () $ edit $ \s -> s { studentName = "Pat" }+    question <- node @Question () mempty+    answer <- node @Answer+      (entityKey question, entityKey student)+      $ edit $ \a -> a { answerYes = True } -    -- 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.--```haskell-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:--```haskell-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:--```haskell-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)))+      -- Typically, you would run some other function like "fetch correct+      -- answers at school" and assert you found the correct answers you+      -- generated. In this example we just assert some things about the data+      -- directly:+      teacherSchoolId (entityVal teacher) `shouldBe` entityKey school+      courseTeacherId (entityVal course) `shouldBe` entityKey teacher+      answerYes (entityVal answer) `shouldBe` True ```  <!--@@ -256,9 +195,6 @@   describe "graphula" . parallel $ do     it "generates and links arbitrary graphs of data" simpleSpec     it "allows logging graphs" loggingSpec-    it "attempts to retry node generation on insertion failure" insertionFailureSpec-    it "attempts to retry node generation on a database constraint violation" constraintFailureSpec-    it "attempts to retry node generation on unsatisfiable predicates" ensureFailureSpec  runDB :: MonadUnliftIO m => ReaderT SqlBackend (NoLoggingT (ResourceT m)) a -> m a runDB f = runSqlite "test.db" $ do
graphula.cabal view
@@ -1,13 +1,13 @@ cabal-version:      1.12 name:               graphula-version:            2.0.0.5+version:            2.0.1.0 license:            MIT license-file:       LICENSE maintainer:         Freckle Education homepage:           https://github.com/freckle/graphula#readme bug-reports:        https://github.com/freckle/graphula/issues synopsis:-    A declarative library for describing dependencies between data+    A simple interface for generating persistent data and linking its dependencies  description:        Please see README.md category:           Network@@ -24,8 +24,14 @@     exposed-modules:         Graphula         Graphula.Arbitrary-        Graphula.Internal+        Graphula.Class+        Graphula.Dependencies+        Graphula.Dependencies.Generic+        Graphula.Idempotent         Graphula.Key+        Graphula.Logged+        Graphula.NoConstraint+        Graphula.Node      hs-source-dirs:   src     other-modules:    Paths_graphula@@ -59,10 +65,7 @@     type:             exitcode-stdio-1.0     main-is:          README.lhs     hs-source-dirs:   test-    other-modules:-        Graphula.UUIDKey-        Paths_graphula-+    other-modules:    Paths_graphula     default-language: Haskell2010     ghc-options:         -Weverything -Wno-unsafe -Wno-safe -Wno-missing-import-lists@@ -74,6 +77,7 @@         base >=4.14.1.0 && <5,         bytestring >=0.10.12.0,         containers >=0.6.2.1,+        generic-arbitrary >=0.1.0,         graphula -any,         hspec >=2.7.8,         http-api-data >=0.4.1.1,@@ -86,8 +90,7 @@         resourcet >=1.2.4.2,         text >=1.2.4.1,         transformers >=0.5.6.2,-        unliftio-core >=0.2.0.1,-        uuid >=1.3.14+        unliftio-core >=0.2.0.1      if impl(ghc >=8.10)         ghc-options:
src/Graphula.hs view
@@ -1,9 +1,6 @@ {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DataKinds #-}-{-# LANGUAGE DefaultSignatures #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE DeriveTraversable #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}@@ -16,10 +13,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PolyKinds #-} {-# LANGUAGE RankNTypes #-}-{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilyDependencies #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-}@@ -31,104 +25,148 @@ -- dependencies. You can use this interface to generate fixtures for automated -- testing. ----- The interface is extensible and supports pluggable front-ends.--- -- @--- runGraphIdentity . runGraphulaT $ do---   -- Compose dependencies at the value level---   Identity vet <- node @Veterinarian () mempty---   Identity owner <- node @Owner (only vet) mempty---   -- TypeApplications is not necessary, but recommended for clarity.---   Identity dog <- node @Dog (owner, vet) $ edit $ \d -> d { name = "fido" }+-- {- 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-  ( -- * Graph Declaration-    node-  , nodeKeyed-  , GraphulaNode-  , GraphulaContext-    -- ** Node options-  , NodeOptions-  , edit-  , ensure-    -- * Declaring Dependencies and key source-  , HasDependencies(..)-  , KeySourceType(..)-    -- * Abstract over how keys are generated using 'SourceDefault' or-    -- 'SourceArbitrary'-  , GenerateKey-    -- ** Singular Dependencies+  (+  -- * Basic usage+  -- ** Model requirements+    HasDependencies(..)   , Only(..)   , only-    -- * The Graph Monad-    -- ** Type Classes-  , MonadGraphula-  , MonadGraphulaBackend(..)-  , MonadGraphulaFrontend(..)-    -- ** Backends-  , runGraphulaT++  -- ** 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-  , GraphulaLoggedT-    -- ** Frontends-  , runGraphulaIdempotentT++  -- ** Running idempotently   , GraphulaIdempotentT-    -- * Extras+  , 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-    -- * Exceptions-  , GenerationFailure(..)-  )-where+  ) where  import Prelude hiding (readFile) -import Control.Monad (guard, (<=<)) import Control.Monad.IO.Unlift-import Control.Monad.Reader (MonadReader, ReaderT, ask, asks, runReaderT)+import Control.Monad.Reader (MonadReader, ReaderT, asks, runReaderT) import Control.Monad.Trans (MonadTrans, lift)-import Data.Foldable (for_, traverse_)-import Data.IORef (IORef, modifyIORef', newIORef, readIORef)+import Data.IORef (IORef, newIORef) import Data.Kind (Constraint, Type)-import Data.Proxy (Proxy(..))-import Data.Semigroup.Generic (gmappend, gmempty)-import Data.Sequence (Seq, empty, (|>))-import Data.Text (Text, pack)-import qualified Data.Text.IO as T-import Data.Traversable (for)-import Data.Typeable (TypeRep, Typeable, typeRep)+import Data.Typeable (Typeable) import Database.Persist-  ( Entity(..)-  , Key-  , PersistEntity+  ( PersistEntity   , PersistEntityBackend   , checkUnique   , delete-  , entityKey   , get   , getEntity   , insertKey   , insertUnique   ) import Database.Persist.Sql (SqlBackend)-import Generics.Eot (Eot, HasEot, fromEot, toEot)-import GHC.Generics (Generic)-import Graphula.Arbitrary (generate)-import Graphula.Internal-import System.Directory (createDirectoryIfMissing, getTemporaryDirectory)-import System.IO (Handle, IOMode(..), hClose, openFile)-import System.IO.Temp (openTempFile)+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-  (Exception, SomeException, bracket, catch, mask, throwIO)--type MonadGraphula m-  = (Monad m, MonadGraphulaBackend m, MonadGraphulaFrontend m, MonadIO m)+import UnliftIO.Exception (catch, throwIO)  -- | A constraint over lists of nodes for 'MonadGraphula', and 'GraphulaNode'. --@@ -147,12 +185,6 @@    GraphulaContext m '[] = MonadGraphula m    GraphulaContext m (t ': ts) = (GraphulaNode m t, GraphulaContext m ts) -class MonadGraphulaFrontend m where-  insert-    :: (PersistEntityBackend a ~ SqlBackend, PersistEntity a, Monad m)-    => Maybe (Key a) -> a -> m (Maybe (Entity a))-  remove :: (PersistEntityBackend a ~ SqlBackend, PersistEntity a, Monad m) => Key a -> m ()- data Args backend n m = Args   { dbRunner :: RunDB backend n m   , gen :: IORef QCGen@@ -215,191 +247,10 @@ logFailingSeed :: MonadIO m => Int -> HUnitFailure -> m a logFailingSeed seed = rethrowHUnitWith ("Graphula with seed: " ++ show seed) -newtype GraphulaIdempotentT m a =-  GraphulaIdempotentT {runGraphulaIdempotentT' :: ReaderT (IORef (m ())) m a}-  deriving newtype (Functor, Applicative, Monad, MonadIO, MonadReader (IORef (m ())))--instance MonadUnliftIO m => MonadUnliftIO (GraphulaIdempotentT m) where-  {-# INLINE withRunInIO #-}-  withRunInIO inner = GraphulaIdempotentT $ withRunInIO $ \run ->-    inner $ run . runGraphulaIdempotentT'--instance MonadTrans GraphulaIdempotentT where-  lift = GraphulaIdempotentT . lift--instance (MonadIO m, MonadGraphulaFrontend m) => MonadGraphulaFrontend (GraphulaIdempotentT m) where-  insert mKey n = do-    finalizersRef <- ask-    mEnt <- lift $ insert mKey n-    for_ (entityKey <$> mEnt)-      $ \key -> liftIO $ modifyIORef' finalizersRef (remove key >>)-    pure mEnt-  remove = lift . remove---- | A wrapper around a graphula frontend that produces finalizers to remove--- graph nodes on error or completion. An idempotent graph produces no data--- outside of its own closure.------ @--- runGraphIdentity . runGraphulaIdempotentT . runGraphulaT $ do---   node @PancakeBreakfast () mempty--- @----runGraphulaIdempotentT :: (MonadUnliftIO m) => GraphulaIdempotentT m a -> m a-runGraphulaIdempotentT action = mask $ \unmasked -> do-  finalizersRef <- liftIO . newIORef $ pure ()-  x <--    unmasked-    $ runReaderT (runGraphulaIdempotentT' action) finalizersRef-    `catch` rollbackRethrow finalizersRef-  rollback finalizersRef $ pure x- where-  rollback :: MonadIO m => IORef (m a) -> m b -> m b-  rollback finalizersRef x = do-    finalizers <- liftIO $ readIORef finalizersRef-    finalizers >> x--  rollbackRethrow :: MonadIO m => IORef (m a) -> SomeException -> m b-  rollbackRethrow finalizersRef (e :: SomeException) =-    rollback finalizersRef (throwIO e)--newtype GraphulaLoggedT m a =-  GraphulaLoggedT {runGraphulaLoggedT' :: ReaderT (IORef (Seq Text)) m a}-  deriving newtype (Functor, Applicative, Monad, MonadIO, MonadReader (IORef (Seq Text)))--instance MonadTrans GraphulaLoggedT where-  lift = GraphulaLoggedT . lift--instance (MonadGraphulaBackend m, MonadIO m) => MonadGraphulaBackend (GraphulaLoggedT m) where-  type Logging (GraphulaLoggedT m) = Show-  askGen = lift askGen-  logNode n = do-    graphLog <- ask-    liftIO $ modifyIORef' graphLog (|> pack (show n))--instance (Monad m, MonadGraphulaFrontend m) => MonadGraphulaFrontend (GraphulaLoggedT m) where-  insert mKey = lift . insert mKey-  remove = lift . remove---- | An extension of 'runGraphulaT' that logs all nodes to a temporary file on--- 'Exception' and re-throws the 'Exception'.-runGraphulaLoggedT :: MonadUnliftIO m => GraphulaLoggedT m a -> m a-runGraphulaLoggedT = runGraphulaLoggedUsingT logFailTemp---- | A variant of 'runGraphulaLoggedT' that accepts a file path to logged to--- instead of utilizing a temp file.-runGraphulaLoggedWithFileT-  :: MonadUnliftIO m => FilePath -> GraphulaLoggedT m a -> m a-runGraphulaLoggedWithFileT = runGraphulaLoggedUsingT . logFailFile--runGraphulaLoggedUsingT-  :: MonadUnliftIO m-  => (IORef (Seq Text) -> HUnitFailure -> m a)-  -> GraphulaLoggedT m a-  -> m a-runGraphulaLoggedUsingT logFail action = do-  graphLog <- liftIO $ newIORef empty-  runReaderT (runGraphulaLoggedT' action) graphLog `catch` logFail graphLog--logFailUsing-  :: MonadIO m-  => IO (FilePath, Handle)-  -> IORef (Seq Text)-  -> HUnitFailure-  -> m a-logFailUsing f graphLog hunitfailure =-  flip rethrowHUnitLogged hunitfailure =<< logGraphToHandle graphLog f--logFailFile :: MonadIO m => FilePath -> IORef (Seq Text) -> HUnitFailure -> m a-logFailFile path = logFailUsing ((path, ) <$> openFile path WriteMode)--logFailTemp :: MonadIO m => IORef (Seq Text) -> HUnitFailure -> m a-logFailTemp = logFailUsing $ do-  tmp <- (++ "/graphula") <$> getTemporaryDirectory-  createDirectoryIfMissing True tmp-  openTempFile tmp "fail-.graphula"--logGraphToHandle-  :: (MonadIO m) => IORef (Seq Text) -> IO (FilePath, Handle) -> m FilePath-logGraphToHandle graphLog openHandle = liftIO $ bracket-  openHandle-  (hClose . snd)-  (\(path, handle) -> do-    nodes <- readIORef graphLog-    path <$ traverse_ (T.hPutStrLn handle) nodes-  )- rethrowHUnitWith :: MonadIO m => String -> HUnitFailure -> m a rethrowHUnitWith message (HUnitFailure l r) =   throwIO . HUnitFailure l . Reason $ message ++ "\n\n" ++ formatFailureReason r -rethrowHUnitLogged :: MonadIO m => FilePath -> HUnitFailure -> m a-rethrowHUnitLogged path =-  rethrowHUnitWith ("Graph dumped in temp file: " ++ path)--class HasDependencies a where-  -- | A data type that contains values to be injected into @a@ via-  -- `dependsOn`. The default generic implementation of `dependsOn` supports-  -- tuples as 'Dependencies'. Data types with a single dependency should use-  -- 'Only' as a 1-tuple.-  ---  -- note: The contents of a tuple must be ordered as they appear in the-  -- definition of @a@.-  type Dependencies a-  type instance Dependencies _a = ()--  -- | Specify the method for resolving a node's key-  ---  -- This can be-  ---  -- @-  -- 'SourceDefault   -- automatically generate keys from the database-  -- 'SourceArbitrary -- automatically generate keys using @'Arbitrary'@-  -- 'SourceExternal  -- explicitly pass a key using @'nodeKeyed'@-  -- @-  ---  -- Most types will use @'SourceDefault'@ or @'SourceArbitrary'@. Only-  -- use @'SourceExternal'@ if the key for a value is always defined-  -- externally.-  ---  type KeySource a :: KeySourceType-  type instance KeySource _a = 'SourceDefault--  -- | Assign values from the 'Dependencies' collection to a value.-  -- 'dependsOn' must be an idempotent operation.-  ---  -- Law:-  ---  -- prop> (\x d -> x `dependsOn` d `dependsOn` d) = dependsOn-  dependsOn :: a -> Dependencies a -> a-  default dependsOn-    ::-      ( HasEot a-      , HasEot (Dependencies a)-      , GHasDependencies (Proxy a) (Proxy (Dependencies a)) (Eot a) (Eot (Dependencies a))-      )-    => a -> Dependencies a -> a-  dependsOn a dependencies =-    fromEot $-      genericDependsOn-        (Proxy :: Proxy a)-        (Proxy :: Proxy (Dependencies a))-        (toEot a)-        (toEot dependencies)---- | Abstract over how keys are generated using @'SourceDefault'@ or @'SourceArbitrary'@-class (GenerateKeyInternal (KeySource a) a, KeyConstraint (KeySource a) a) => GenerateKey a-instance (GenerateKeyInternal (KeySource a) a, KeyConstraint (KeySource a) a) => GenerateKey a--data GenerationFailure-  = GenerationFailureMaxAttemptsToConstrain TypeRep-  -- ^ Could not satisfy constraints defined using @'ensure'@-  | GenerationFailureMaxAttemptsToInsert TypeRep-  -- ^ Could not satisfy database constraints on insert-  deriving stock (Show, Eq)--instance Exception GenerationFailure- type GraphulaNode m a   = ( HasDependencies a     , Logging m a@@ -408,145 +259,3 @@     , Typeable a     , Arbitrary a     )--{-|-  Generate a value with data dependencies. This leverages-  'HasDependencies' to insert the specified data in the generated value. All-  dependency data is inserted after any editing operations.--  > node @Dog (ownerId, veterinarianId) mempty-  > node @Dog (ownerId, veterinarianId) $ edit $ \dog ->-  >   dog {name = "fido"}--  A value that has an externally managed key must use @'nodeKeyed'@ instead.--}-node-  :: forall a m-   . (GraphulaContext m '[a], GenerateKey a)-  => Dependencies a-  -> NodeOptions a-  -> m (Entity a)-node = nodeImpl $ generate $ generateKey @(KeySource a) @a--{-|-  Generate a value with data dependencies given an externally managed-  key. This leverages 'HasDependencies' to insert the specified data-  in the generated value. All dependency data is inserted after any-  editing operations.--  > someKey <- generateKey-  > node @Cat someKey (ownerId, veterinarianId) mempty-  > anotherKey <- generateKey-  > node @Cat anotherKey (ownerId, veterinarianId) $ edit $ \cat ->-  >   cat {name = "milo"}--  A value that has an automatically managed key may use @'node'@ instead.--}-nodeKeyed-  :: forall a m-   . GraphulaContext m '[a]-  => Key a-  -> Dependencies a-  -> NodeOptions a-  -> m (Entity a)-nodeKeyed key = nodeImpl $ pure $ Just key--nodeImpl-  :: forall a m-   . GraphulaContext m '[a]-  => m (Maybe (Key a))-  -> Dependencies a-  -> NodeOptions a-  -> m (Entity a)-nodeImpl genKey dependencies NodeOptions {..} = attempt 100 10 $ do-  initial <- generate arbitrary-  for (appKendo nodeOptionsEdit initial) $ \edited -> do-    let hydrated = edited `dependsOn` dependencies-    logNode hydrated-    mKey <- genKey-    pure (mKey, hydrated)---- | Modify the node after it's been generated------ @--- a <- node @A () $ edit $ \a -> a { someField = True }--- @----edit :: (a -> a) -> NodeOptions a-edit f = mempty { nodeOptionsEdit = Kendo $ Just . f }---- | Require a node to satisfy the specified predicate------ @--- a <- node @A () $ ensure $ (== True) . someField--- @----ensure :: (a -> Bool) -> NodeOptions a-ensure f = mempty { nodeOptionsEdit = Kendo $ \a -> a <$ guard (f a) }---- | Options for generating an individual node------ @'NodeOptions'@ can be created and combined with the Monoidal--- operations @'(<>)'@ and @'mempty'@.------ @--- a1 <- node @A () mempty--- a2 <- node @A () $ edit $ \a -> a { someField = True }--- a3 <- node @A () $ ensure $ (== True) . someField--- @----newtype NodeOptions a = NodeOptions-  { nodeOptionsEdit :: Kendo Maybe a-  }-  deriving stock Generic--instance Semigroup (NodeOptions a) where-  (<>) = gmappend-  {-# INLINE (<>) #-}--instance Monoid (NodeOptions a) where-  mempty = gmempty-  {-# INLINE mempty #-}---- | Like @'Endo'@ but uses Kliesli composition-newtype Kendo m a = Kendo { appKendo :: a -> m a }-    deriving stock Generic--instance Monad m => Semigroup (Kendo m a) where-  Kendo f <> Kendo g = Kendo $ f <=< g-  {-# INLINE (<>) #-}--instance Monad m => Monoid (Kendo m a) where-  mempty = Kendo pure-  {-# INLINE mempty #-}--attempt-  :: forall a m-   . (GraphulaContext m '[a])-  => Int-  -> Int-  -> m (Maybe (Maybe (Key a), a))-  -> m (Entity a)-attempt maxEdits maxInserts source = loop 0 0- where-  loop :: Int -> Int -> m (Entity a)-  loop numEdits numInserts-    | numEdits >= maxEdits = die GenerationFailureMaxAttemptsToConstrain-    | numInserts >= maxInserts = die GenerationFailureMaxAttemptsToInsert-    | otherwise = source >>= \case-      Nothing -> loop (succ numEdits) numInserts-      --               ^ failed to edit, only increments this-      Just (mKey, value) -> insert mKey value >>= \case-        Nothing -> loop (succ numEdits) (succ numInserts)-        --               ^ failed to insert, but also increments this-        Just a -> pure a--  die :: (TypeRep -> GenerationFailure) -> m (Entity a)-  die e = throwIO $ e $ typeRep (Proxy :: Proxy a)---- | For entities that only have singular 'Dependencies'-newtype Only a = Only { fromOnly :: a }-  deriving stock (Eq, Show, Ord, Generic, Functor, Foldable, Traversable)--only :: a -> Only a-only = Only
src/Graphula/Arbitrary.hs view
@@ -1,18 +1,13 @@-{-|-  Graphula tracks its own 'QCGen' for deterministic generation with 'Arbitrary'-  and 'Gen'. 'generate' can be used to produce arbitrary values utilizing-  graphula's generation.--}+-- | 'Arbitrary' operations that respect Graphula's seed module Graphula.Arbitrary   ( generate-  )-where+  ) where  import Prelude  import Control.Monad.IO.Unlift (MonadIO, liftIO) import Data.IORef (readIORef, writeIORef)-import Graphula.Internal (MonadGraphulaBackend, askGen)+import Graphula.Class (MonadGraphulaBackend, askGen) import System.Random (split) import Test.QuickCheck (Gen) import Test.QuickCheck.Gen (unGen)
+ src/Graphula/Class.hs view
@@ -0,0 +1,45 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE NoStarIsType #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++-- | Internal type class(es) for Graphula-related behaviors+module Graphula.Class+  ( MonadGraphula+  , MonadGraphulaFrontend(..)+  , MonadGraphulaBackend(..)+  ) where++import Control.Monad.IO.Class (MonadIO)+import Data.IORef (IORef)+import Data.Kind (Constraint, Type)+import Database.Persist (Entity(..), Key, PersistEntity, PersistEntityBackend)+import Database.Persist.Sql (SqlBackend)+import Test.QuickCheck.Random (QCGen)++type MonadGraphula m+  = (Monad m, MonadIO m, MonadGraphulaBackend m, MonadGraphulaFrontend m)++class MonadGraphulaFrontend m where+  insert+    :: (PersistEntityBackend a ~ SqlBackend, PersistEntity a, Monad m)+    => Maybe (Key a)+    -> a+    -> m (Maybe (Entity a))++  remove+    :: (PersistEntityBackend a ~ SqlBackend, PersistEntity a, Monad m)+    => Key a+    -> m ()++class MonadGraphulaBackend m where+  type Logging m :: Type -> Constraint+  askGen :: m (IORef QCGen)+  logNode :: Logging m a => a -> m ()
+ src/Graphula/Dependencies.hs view
@@ -0,0 +1,194 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DefaultSignatures #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DeriveTraversable #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE NoStarIsType #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilyDependencies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE UndecidableSuperClasses #-}++module Graphula.Dependencies+  ( HasDependencies(..)+  , Only(..)+  , only++  -- * Non-serial keys+  , KeySourceType(..)+  , GenerateKey+  , generateKey+  ) where++import Prelude++import Data.Kind (Constraint)+import Data.Proxy (Proxy(..))+import Database.Persist (Key)+import GHC.Generics (Generic)+import GHC.TypeLits (ErrorMessage(..), TypeError)+import Generics.Eot (Eot, HasEot, fromEot, toEot)+import Graphula.Dependencies.Generic+import Graphula.NoConstraint+import Test.QuickCheck.Arbitrary (Arbitrary(..))+import Test.QuickCheck.Gen (Gen)++class HasDependencies a where+  -- | A data type declaring the model's dependencies+  --+  -- Models with no dependencies can declare an empty instance,+  --+  -- @+  -- instance 'HasDependencies' School+  -- @+  --+  -- Models with one dependency must use the 'Only' 1-tuple constructor,+  --+  -- @+  -- instance 'HasDependencies' Teacher where+  --   type Dependencies Teacher = Only SchoolId+  -- @+  --+  -- Models with multiple dependencies use tuple syntax,+  --+  -- @+  -- instance 'HasDependencies' Course where+  --   type Dependencies Course = (SchoolId, TeacherId)+  -- @+  --+  type Dependencies a+  type instance Dependencies _a = ()++  -- | Specify the method for resolving a node's key+  --+  -- This can be+  --+  -- @+  -- 'SourceDefault   -- automatically generate keys from the database+  -- 'SourceArbitrary -- automatically generate keys using @'Arbitrary'@+  -- 'SourceExternal  -- explicitly pass a key using @'nodeKeyed'@+  -- @+  --+  -- Most types will use 'SourceDefault' or 'SourceArbitrary'. Only use+  -- 'SourceExternal' if the key for a value is always defined externally.+  --+  type KeySource a :: KeySourceType+  type instance KeySource _a = 'SourceDefault++  -- | Assign values from the 'Dependencies' collection to a value+  --+  -- This must be an idempotent operation. Law:+  --+  -- prop> (\x d -> x `dependsOn` d `dependsOn` d) = dependsOn+  --+  -- The default, 'Generic'-based implementation will assign values by the order+  -- of the fields in the model's type.+  --+  dependsOn :: a -> Dependencies a -> a+  default dependsOn+    ::+      ( HasEot a+      , HasEot (Dependencies a)+      , GHasDependencies (Proxy a) (Proxy (Dependencies a)) (Eot a) (Eot (Dependencies a))+      )+    => a -> Dependencies a -> a+  dependsOn a dependencies =+    fromEot $+      genericDependsOn+        (Proxy :: Proxy a)+        (Proxy :: Proxy (Dependencies a))+        (toEot a)+        (toEot dependencies)++-- | For entities that only have singular 'Dependencies'+newtype Only a = Only { fromOnly :: a }+  deriving stock (Eq, Show, Ord, Generic, Functor, Foldable, Traversable)++only :: a -> Only a+only = Only++data KeySourceType+  = SourceDefault+  -- ^ Generate keys using the database's @DEFAULT@ strategy+  | SourceArbitrary+  -- ^ Generate keys using the 'Arbitrary' instance for the 'Key'+  | SourceExternal+  -- ^ Always explicitly pass an external key+  --+  -- See 'nodeKeyed'.+  --++-- | Abstract constraint that some @a@ can generate a key+--+-- This is part of ensuring better error messages.+--+class (GenerateKeyInternal (KeySource a) a, KeyConstraint (KeySource a) a) => GenerateKey a+instance (GenerateKeyInternal (KeySource a) a, KeyConstraint (KeySource a) a) => GenerateKey a++class GenerateKeyInternal (s :: KeySourceType) a where+  type KeyConstraint s a :: Constraint+  generateKey :: KeyConstraint s a => Gen (Maybe (Key a))++instance GenerateKeyInternal 'SourceDefault a where+  type KeyConstraint 'SourceDefault a = NoConstraint a+  generateKey = pure Nothing++instance GenerateKeyInternal 'SourceArbitrary a where+  type KeyConstraint 'SourceArbitrary a = Arbitrary (Key a)+  generateKey = Just <$> arbitrary++-- Rendered:+--+-- @+-- Cannot generate a value of type ‘X’ using ‘node’ since+--+--   instance HasDependencies X where+--     type KeySource X = 'SourceExternal+--+-- Possible fixes include:+-- • Use ‘nodeKeyed’ instead of ‘node’+-- • Change ‘KeySource X’ to 'SourceDefault or 'SourceArbitrary+-- @+--+instance TypeError+  ( 'Text "Cannot generate a value of type "+    ':<>: Quote ('ShowType a)+    ':<>: 'Text " using "+    ':<>: Quote ('Text "node")+    ':<>: 'Text " since"+    ':$$: 'Text ""+    ':$$: 'Text "  instance HasDependencies "+    ':<>: 'ShowType a+    ':<>: 'Text " where"+    ':$$: 'Text "    "+    ':<>: 'Text "type KeySource "+    ':<>: 'ShowType a+    ':<>: 'Text  " = "+    ':<>: 'ShowType 'SourceExternal+    ':$$: 'Text ""+    ':$$: 'Text "Possible fixes include:"+    ':$$: 'Text "• Use "+    ':<>: Quote ('Text "nodeKeyed")+    ':<>: 'Text " instead of "+    ':<>: Quote ('Text "node")+    ':$$: 'Text "• Change "+    ':<>: Quote ('Text "KeySource " ':<>: 'ShowType a)+    ':<>: 'Text " to "+    ':<>: 'Text "'SourceDefault"+    ':<>: 'Text " or "+    ':<>: 'Text "'SourceArbitrary"+  ) => GenerateKeyInternal 'SourceExternal a where+  type KeyConstraint 'SourceExternal a = NoConstraint a+  generateKey = error "unreachable"++type family Quote t where+  Quote t = 'Text "‘" ':<>: t ':<>: 'Text "’"
+ src/Graphula/Dependencies/Generic.hs view
@@ -0,0 +1,141 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE NoStarIsType #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++-- | Machinery for the 'Generic'-based 'HasDependencies' instance+module Graphula.Dependencies.Generic+  ( GHasDependencies(..)+  ) where++import Data.Kind (Type)+import GHC.TypeLits (ErrorMessage(..), TypeError)+import Generics.Eot (Proxy(..), Void)++data Match t+  = NoMatch t+  | Match t++type family DependenciesTypeInstance nodeTy depsTy where+  DependenciesTypeInstance nodeTy depsTy =+    'Text "‘type Dependencies " ':<>: 'ShowType nodeTy ':<>:+    'Text " = " ':<>: 'ShowType depsTy ':<>: 'Text "’"++-- Walk through the fields of our node and match them up with fields from the dependencies.+type family FindMatches nodeTy depsTy as ds :: [Match Type] where+  -- Excess dependencies+  FindMatches nodeTy depsTy () (d, _ds) =+    TypeError+      ( 'Text "Excess dependency ‘" ':<>: 'ShowType d ':<>:+        'Text "’ in " ':$$: DependenciesTypeInstance nodeTy depsTy ':$$:+        'Text "Ordering of dependencies must match their occurrence in the target type ‘" ':<>:+        'ShowType nodeTy ':<>: 'Text "’"+      )++  -- No more fields or dependencies left+  FindMatches _nodeTy _depsTy () () = '[]++  -- Fields left, but no more dependencies+  FindMatches nodeTy depsTy (a, as) () = 'NoMatch a ': FindMatches nodeTy depsTy as ()++  -- Field matches dependency, keep going+  FindMatches nodeTy depsTy (a, as) (a, ds) = 'Match a ': FindMatches nodeTy depsTy as ds++  -- Field does not match dependency, keep going+  FindMatches nodeTy depsTy (a, as) (d, ds) = 'NoMatch a ': FindMatches nodeTy depsTy as (d, ds)++class GHasDependencies nodeTyProxy depsTyProxy node deps where+  genericDependsOn :: nodeTyProxy -> depsTyProxy -> node -> deps -> node++class GHasDependenciesRecursive fieldsProxy node deps where+  genericDependsOnRecursive :: fieldsProxy -> node -> deps -> node++-- This instance head only matches EoT representations of+-- datatypes with no constructors and no dependencies+instance {-# OVERLAPPING #-} GHasDependencies (Proxy nodeTy) (Proxy depsTy) Void (Either () Void) where+  genericDependsOn _ _ node _ = node++-- This instance warns the user if they give dependencies+-- to a datatype with no constructors+instance+  {-# OVERLAPPABLE #-}+  ( TypeError+    ( 'Text "A datatype with no constructors can't use the dependencies in" ':$$:+      DependenciesTypeInstance nodeTy depsTy+    )+  ) => GHasDependencies (Proxy nodeTy) (Proxy depsTy) Void (Either deps rest) where+  genericDependsOn _ _ _ _ = error "Impossible"++-- This instance head only matches EoT representations of+-- datatypes with a single constructor+instance+  ( FindMatches nodeTy depsTy node deps ~ fields+  , GHasDependenciesRecursive (Proxy fields) node deps+  ) => GHasDependencies (Proxy nodeTy) (Proxy depsTy) (Either node Void) (Either deps Void) where+  genericDependsOn _ _ (Left node) (Left deps) =+    Left (genericDependsOnRecursive (Proxy :: Proxy fields) node deps)+  genericDependsOn _ _ _ _ = error "Impossible" -- EoT never generates an actual `Right (x :: Void)` here++-- This instance matches a sum type as both node and dependencies.+-- We use this to report an error to the user.+instance+  ( TypeError+    ( 'Text "Cannot automatically find dependencies for sum type in" ':$$:+      DependenciesTypeInstance nodeTy depsTy+    )+  ) => GHasDependencies (Proxy nodeTy) (Proxy depsTy) (Either left (Either right rest)) (Either deps Void) where+  genericDependsOn _ _ _ _ = error "Impossible"++-- This instance matches a sum type as the node.+-- This is also an error.+instance+  ( TypeError+    ( 'Text "Cannot automatically use a sum type as dependencies in" ':$$:+      DependenciesTypeInstance nodeTy depsTy+    )+  ) => GHasDependencies (Proxy nodeTy) (Proxy depsTy) (Either node Void) (Either left (Either right rest)) where+  genericDependsOn _ _ _ _ = error "Impossible"++-- This instance matches a sum type as the dependencies.+-- This is also an error.+instance+  ( TypeError+    ( 'Text "Cannot automatically find dependencies for sum type or use a sum type as a dependency in" ':$$:+      DependenciesTypeInstance nodeTy depsTy+    )+  ) => GHasDependencies (Proxy nodeTy) (Proxy depsTy) (Either left1 (Either right1 rest1)) (Either left2 (Either right2 rest2)) where+  genericDependsOn _ _ _ _ = error "Impossible"++-- Don't let the user specify `Void` as a dependency+instance+  ( TypeError+    ( 'Text "Use ‘()’ instead of ‘Void’ for datatypes with no dependencies in" ':$$:+      DependenciesTypeInstance nodeTy depsTy+    )+  ) => GHasDependencies (Proxy nodeTy) (Proxy depsTy) node Void where+  genericDependsOn _ _ _ _ = error "Impossible"++instance+  ( a ~ dep+  , GHasDependenciesRecursive (Proxy fields) as deps+  ) => GHasDependenciesRecursive (Proxy ('Match a ': fields)) (a, as) (dep, deps) where+  genericDependsOnRecursive _ (_, as) (dep, deps) =+    (dep, genericDependsOnRecursive (Proxy :: Proxy fields) as deps)++instance+  ( GHasDependenciesRecursive (Proxy fields) as deps+  ) => GHasDependenciesRecursive (Proxy ('NoMatch a ': fields)) (a, as) deps where+  genericDependsOnRecursive _ (a, as) deps =+    (a, genericDependsOnRecursive (Proxy :: Proxy fields) as deps)++-- Without the kind-signature for '[], ghc will fail to find this+-- instance for nullary constructors+instance GHasDependenciesRecursive (Proxy ('[] :: [Match Type])) () () where+  genericDependsOnRecursive _ _ _ = ()
+ src/Graphula/Idempotent.hs view
@@ -0,0 +1,81 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE InstanceSigs #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE NoStarIsType #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilyDependencies #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE UndecidableSuperClasses #-}++-- | A version of 'GraphulaT' that @'remove'@s all @'insert'@ed data afterward+module Graphula.Idempotent+  ( GraphulaIdempotentT+  , runGraphulaIdempotentT+  ) where++import Prelude++import Control.Monad.IO.Unlift+import Control.Monad.Reader (MonadReader, ReaderT, ask, runReaderT)+import Control.Monad.Trans (MonadTrans, lift)+import Data.Foldable (for_)+import Data.IORef (IORef, modifyIORef', newIORef, readIORef)+import Database.Persist (Entity(..))+import Graphula.Class+import UnliftIO.Exception (SomeException, catch, mask, throwIO)++newtype GraphulaIdempotentT m a = GraphulaIdempotentT+  { runGraphulaIdempotentT' :: ReaderT (IORef (m ())) m a+  }+  deriving newtype+    ( Functor+    , Applicative+    , Monad+    , MonadIO+    , MonadReader (IORef (m ()))+    )++instance MonadUnliftIO m => MonadUnliftIO (GraphulaIdempotentT m) where+  {-# INLINE withRunInIO #-}+  withRunInIO inner = GraphulaIdempotentT $ withRunInIO $ \run ->+    inner $ run . runGraphulaIdempotentT'++instance MonadTrans GraphulaIdempotentT where+  lift = GraphulaIdempotentT . lift++instance (MonadIO m, MonadGraphulaFrontend m) => MonadGraphulaFrontend (GraphulaIdempotentT m) where+  insert mKey n = do+    finalizersRef <- ask+    mEnt <- lift $ insert mKey n+    for_ (entityKey <$> mEnt)+      $ \key -> liftIO $ modifyIORef' finalizersRef (remove key >>)+    pure mEnt+  remove = lift . remove++runGraphulaIdempotentT :: MonadUnliftIO m => GraphulaIdempotentT m a -> m a+runGraphulaIdempotentT action = mask $ \unmasked -> do+  finalizersRef <- liftIO . newIORef $ pure ()+  x <-+    unmasked+    $ runReaderT (runGraphulaIdempotentT' action) finalizersRef+    `catch` rollbackRethrow finalizersRef+  rollback finalizersRef $ pure x+ where+  rollback :: MonadIO m => IORef (m a) -> m b -> m b+  rollback finalizersRef x = do+    finalizers <- liftIO $ readIORef finalizersRef+    finalizers >> x++  rollbackRethrow :: MonadIO m => IORef (m a) -> SomeException -> m b+  rollbackRethrow finalizersRef (e :: SomeException) =+    rollback finalizersRef (throwIO e)
− src/Graphula/Internal.hs
@@ -1,237 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE NoStarIsType #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE TypeOperators #-}-{-# LANGUAGE UndecidableInstances #-}--module Graphula.Internal-  ( MonadGraphulaBackend(..)-  , GHasDependencies(..)-  , KeySourceType(..)-  , GenerateKeyInternal(..)-  , NoConstraint-  )-where--import Data.IORef (IORef)-import Data.Kind (Constraint, Type)-import Database.Persist (Key)-import Generics.Eot (Proxy(..), Void)-import GHC.TypeLits (ErrorMessage(..), TypeError)-import Test.QuickCheck (Arbitrary(..), Gen)-import Test.QuickCheck.Random (QCGen)--class MonadGraphulaBackend m where-  type Logging m :: Type -> Constraint-  -- ^ A constraint provided to log details of the graph to some form of-  --   persistence. This is used by 'runGraphulaLogged' to store graph nodes as-  --   'Show'n 'Text' values-  askGen :: m (IORef QCGen)-  logNode :: Logging m a => a -> m ()--data Match t-  = NoMatch t-  | Match t--type family DependenciesTypeInstance nodeTy depsTy where-  DependenciesTypeInstance nodeTy depsTy =-    'Text "‘type Dependencies " ':<>: 'ShowType nodeTy ':<>:-    'Text " = " ':<>: 'ShowType depsTy ':<>: 'Text "’"---- Walk through the fields of our node and match them up with fields from the dependencies.-type family FindMatches nodeTy depsTy as ds :: [Match Type] where-  -- Excess dependencies-  FindMatches nodeTy depsTy () (d, _ds) =-    TypeError-      ( 'Text "Excess dependency ‘" ':<>: 'ShowType d ':<>:-        'Text "’ in " ':$$: DependenciesTypeInstance nodeTy depsTy ':$$:-        'Text "Ordering of dependencies must match their occurrence in the target type ‘" ':<>:-        'ShowType nodeTy ':<>: 'Text "’"-      )--  -- No more fields or dependencies left-  FindMatches _nodeTy _depsTy () () = '[]--  -- Fields left, but no more dependencies-  FindMatches nodeTy depsTy (a, as) () = 'NoMatch a ': FindMatches nodeTy depsTy as ()--  -- Field matches dependency, keep going-  FindMatches nodeTy depsTy (a, as) (a, ds) = 'Match a ': FindMatches nodeTy depsTy as ds--  -- Field does not match dependency, keep going-  FindMatches nodeTy depsTy (a, as) (d, ds) = 'NoMatch a ': FindMatches nodeTy depsTy as (d, ds)--class GHasDependencies nodeTyProxy depsTyProxy node deps where-  genericDependsOn :: nodeTyProxy -> depsTyProxy -> node -> deps -> node--class GHasDependenciesRecursive fieldsProxy node deps where-  genericDependsOnRecursive :: fieldsProxy -> node -> deps -> node---- This instance head only matches EoT representations of--- datatypes with no constructors and no dependencies-instance {-# OVERLAPPING #-} GHasDependencies (Proxy nodeTy) (Proxy depsTy) Void (Either () Void) where-  genericDependsOn _ _ node _ = node---- This instance warns the user if they give dependencies--- to a datatype with no constructors-instance-  {-# OVERLAPPABLE #-}-  ( TypeError-    ( 'Text "A datatype with no constructors can't use the dependencies in" ':$$:-      DependenciesTypeInstance nodeTy depsTy-    )-  ) => GHasDependencies (Proxy nodeTy) (Proxy depsTy) Void (Either deps rest) where-  genericDependsOn _ _ _ _ = error "Impossible"---- This instance head only matches EoT representations of--- datatypes with a single constructor-instance-  ( FindMatches nodeTy depsTy node deps ~ fields-  , GHasDependenciesRecursive (Proxy fields) node deps-  ) => GHasDependencies (Proxy nodeTy) (Proxy depsTy) (Either node Void) (Either deps Void) where-  genericDependsOn _ _ (Left node) (Left deps) =-    Left (genericDependsOnRecursive (Proxy :: Proxy fields) node deps)-  genericDependsOn _ _ _ _ = error "Impossible" -- EoT never generates an actual `Right (x :: Void)` here---- This instance matches a sum type as both node and dependencies.--- We use this to report an error to the user.-instance-  ( TypeError-    ( 'Text "Cannot automatically find dependencies for sum type in" ':$$:-      DependenciesTypeInstance nodeTy depsTy-    )-  ) => GHasDependencies (Proxy nodeTy) (Proxy depsTy) (Either left (Either right rest)) (Either deps Void) where-  genericDependsOn _ _ _ _ = error "Impossible"---- This instance matches a sum type as the node.--- This is also an error.-instance-  ( TypeError-    ( 'Text "Cannot automatically use a sum type as dependencies in" ':$$:-      DependenciesTypeInstance nodeTy depsTy-    )-  ) => GHasDependencies (Proxy nodeTy) (Proxy depsTy) (Either node Void) (Either left (Either right rest)) where-  genericDependsOn _ _ _ _ = error "Impossible"---- This instance matches a sum type as the dependencies.--- This is also an error.-instance-  ( TypeError-    ( 'Text "Cannot automatically find dependencies for sum type or use a sum type as a dependency in" ':$$:-      DependenciesTypeInstance nodeTy depsTy-    )-  ) => GHasDependencies (Proxy nodeTy) (Proxy depsTy) (Either left1 (Either right1 rest1)) (Either left2 (Either right2 rest2)) where-  genericDependsOn _ _ _ _ = error "Impossible"---- Don't let the user specify `Void` as a dependency-instance-  ( TypeError-    ( 'Text "Use ‘()’ instead of ‘Void’ for datatypes with no dependencies in" ':$$:-      DependenciesTypeInstance nodeTy depsTy-    )-  ) => GHasDependencies (Proxy nodeTy) (Proxy depsTy) node Void where-  genericDependsOn _ _ _ _ = error "Impossible"--instance-  ( a ~ dep-  , GHasDependenciesRecursive (Proxy fields) as deps-  ) => GHasDependenciesRecursive (Proxy ('Match a ': fields)) (a, as) (dep, deps) where-  genericDependsOnRecursive _ (_, as) (dep, deps) =-    (dep, genericDependsOnRecursive (Proxy :: Proxy fields) as deps)--instance-  ( GHasDependenciesRecursive (Proxy fields) as deps-  ) => GHasDependenciesRecursive (Proxy ('NoMatch a ': fields)) (a, as) deps where-  genericDependsOnRecursive _ (a, as) deps =-    (a, genericDependsOnRecursive (Proxy :: Proxy fields) as deps)---- Without the kind-signature for '[], ghc will fail to find this--- instance for nullary constructors-instance GHasDependenciesRecursive (Proxy ('[] :: [Match Type])) () () where-  genericDependsOnRecursive _ _ _ = ()--data KeySourceType-  = SourceDefault-  -- ^ Generate keys using the database's @DEFAULT@ strategy-  | SourceArbitrary-  -- ^ Generate keys using the @'Arbitrary'@ instance for the @'Key'@-  | SourceExternal-  -- ^ Always explicitly pass an external key---- | Handle key generation for @'SourceDefault'@ and @'SourceArbitrary'@------ Ths could be a single-parameter class, but carrying the @a@ around--- lets us give a better error message when @'node'@ is called instead--- of @'nodeKeyed'@.----class GenerateKeyInternal (s :: KeySourceType) a where-  type KeyConstraint s a :: Constraint-  generateKey :: KeyConstraint s a => Gen (Maybe (Key a))--instance GenerateKeyInternal 'SourceDefault a where-  type KeyConstraint 'SourceDefault a = NoConstraint a-  generateKey = pure Nothing--instance GenerateKeyInternal 'SourceArbitrary a where-  type KeyConstraint 'SourceArbitrary a = Arbitrary (Key a)-  generateKey = Just <$> arbitrary---- | Explicit instance for @'SourceExternal'@ to give an actionable error message------ Rendered:------ @--- Cannot generate a value of type ‘X’ using ‘node’ since------   instance HasDependencies X where---     type KeySource X = 'SourceExternal------ Possible fixes include:--- • Use ‘nodeKeyed’ instead of ‘node’--- • Change ‘KeySource X’ to 'SourceDefault or 'SourceArbitrary--- @----instance TypeError-  ( 'Text "Cannot generate a value of type "-    ':<>: Quote ('ShowType a)-    ':<>: 'Text " using "-    ':<>: Quote ('Text "node")-    ':<>: 'Text " since"-    ':$$: 'Text ""-    ':$$: 'Text "  instance HasDependencies "-    ':<>: 'ShowType a-    ':<>: 'Text " where"-    ':$$: 'Text "    "-    ':<>: 'Text "type KeySource "-    ':<>: 'ShowType a-    ':<>: 'Text  " = "-    ':<>: 'ShowType 'SourceExternal-    ':$$: 'Text ""-    ':$$: 'Text "Possible fixes include:"-    ':$$: 'Text "• Use "-    ':<>: Quote ('Text "nodeKeyed")-    ':<>: 'Text " instead of "-    ':<>: Quote ('Text "node")-    ':$$: 'Text "• Change "-    ':<>: Quote ('Text "KeySource " ':<>: 'ShowType a)-    ':<>: 'Text " to "-    ':<>: 'Text "'SourceDefault"-    ':<>: 'Text " or "-    ':<>: 'Text "'SourceArbitrary"-  ) => GenerateKeyInternal 'SourceExternal a where-  type KeyConstraint 'SourceExternal a = NoConstraint a-  generateKey = error "unreachable"--type family Quote t where-  Quote t = 'Text "‘" ':<>: t ':<>: 'Text "’"---- | Graphula accepts constraints for various uses. Frontends do not always--- utilize these constraints. 'NoConstraint' is a universal class that all--- types inhabit. It has no behavior and no additional constraints.-class NoConstraint a-instance NoConstraint a
src/Graphula/Key.hs view
@@ -3,18 +3,42 @@ {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-}++-- | Convenience functions for working with 'Key' dependencies module Graphula.Key   ( onlyKey   , keys   , Keys-  )-where+  ) where  import Database.Persist import GHC.TypeLits (ErrorMessage(..), TypeError) import Graphula (Only(..), only)  class EntityKeys a where+  -- | Type-class for turning a tuple of 'Entity' into a tuple of 'Key'+  --+  -- For example, given:+  --+  -- @+  -- instance 'HasDependencies' Course where+  --   type Dependencies Course = (SchoolId, TeacherId)+  -- @+  --+  -- You would have to do,+  --+  -- @+  -- course <- 'node' @Course (entityKey school, entityKey teacher) mempty+  -- @+  --+  -- This type-class allows you to do:+  --+  -- @+  -- course <- 'node' @Course ('keys' (school, teacher)) mempty+  -- @+  --+  -- The type class instances currently scale up 4-tuple 'Dependencies'.+  --   type Keys a   keys :: a -> Keys a @@ -29,6 +53,7 @@   type Keys (Entity a) = Key a   keys = entityKey +-- | Equivalent to @'Only' . 'entityKey'@ onlyKey :: Entity a -> Only (Key a) onlyKey = keys . only 
+ src/Graphula/Logged.hs view
@@ -0,0 +1,124 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE NoStarIsType #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE TypeFamilyDependencies #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE UndecidableSuperClasses #-}++-- | A version of 'GraphulaT' that logs the generated graph+module Graphula.Logged+  ( GraphulaLoggedT+  , runGraphulaLoggedT+  , runGraphulaLoggedWithFileT+  , runGraphulaLoggedUsingT+  ) where++import Prelude++import Control.Monad.IO.Unlift+import Control.Monad.Reader (MonadReader, ReaderT, ask, runReaderT)+import Control.Monad.Trans (MonadTrans, lift)+import Data.Foldable (traverse_)+import Data.IORef (IORef, modifyIORef', newIORef, readIORef)+import Data.Sequence (Seq, empty, (|>))+import Data.Text (Text, pack)+import qualified Data.Text.IO as T+import Graphula.Class+import System.Directory (createDirectoryIfMissing, getTemporaryDirectory)+import System.IO (Handle, IOMode(..), hClose, openFile)+import System.IO.Temp (openTempFile)+import Test.HUnit.Lang+  (FailureReason(..), HUnitFailure(..), formatFailureReason)+import UnliftIO.Exception (bracket, catch, throwIO)++newtype GraphulaLoggedT m a = GraphulaLoggedT+  { runGraphulaLoggedT' :: ReaderT (IORef (Seq Text)) m a+  }+  deriving newtype+    ( Functor+    , Applicative+    , Monad+    , MonadIO+    , MonadReader (IORef (Seq Text))+    )++instance MonadTrans GraphulaLoggedT where+  lift = GraphulaLoggedT . lift++instance (MonadGraphulaBackend m, MonadIO m) => MonadGraphulaBackend (GraphulaLoggedT m) where+  type Logging (GraphulaLoggedT m) = Show+  askGen = lift askGen+  logNode n = do+    graphLog <- ask+    liftIO $ modifyIORef' graphLog (|> pack (show n))++instance (Monad m, MonadGraphulaFrontend m) => MonadGraphulaFrontend (GraphulaLoggedT m) where+  insert mKey = lift . insert mKey+  remove = lift . remove++-- | Run the graph while logging to a temporary file+runGraphulaLoggedT :: MonadUnliftIO m => GraphulaLoggedT m a -> m a+runGraphulaLoggedT = runGraphulaLoggedUsingT logFailTemp++-- | 'runGraphulaLoggedT', but to the specified file+runGraphulaLoggedWithFileT+  :: MonadUnliftIO m => FilePath -> GraphulaLoggedT m a -> m a+runGraphulaLoggedWithFileT = runGraphulaLoggedUsingT . logFailFile++-- | 'runGraphulaLoggedT', but using the custom action to accumulate+runGraphulaLoggedUsingT+  :: MonadUnliftIO m+  => (IORef (Seq Text) -> HUnitFailure -> m a)+  -> GraphulaLoggedT m a+  -> m a+runGraphulaLoggedUsingT logFail action = do+  graphLog <- liftIO $ newIORef empty+  runReaderT (runGraphulaLoggedT' action) graphLog `catch` logFail graphLog++logFailUsing+  :: MonadIO m+  => IO (FilePath, Handle)+  -> IORef (Seq Text)+  -> HUnitFailure+  -> m a+logFailUsing f graphLog hunitfailure =+  flip rethrowHUnitLogged hunitfailure =<< logGraphToHandle graphLog f++logFailFile :: MonadIO m => FilePath -> IORef (Seq Text) -> HUnitFailure -> m a+logFailFile path = logFailUsing ((path, ) <$> openFile path WriteMode)++logFailTemp :: MonadIO m => IORef (Seq Text) -> HUnitFailure -> m a+logFailTemp = logFailUsing $ do+  tmp <- (++ "/graphula") <$> getTemporaryDirectory+  createDirectoryIfMissing True tmp+  openTempFile tmp "fail-.graphula"++logGraphToHandle+  :: (MonadIO m) => IORef (Seq Text) -> IO (FilePath, Handle) -> m FilePath+logGraphToHandle graphLog openHandle = liftIO $ bracket+  openHandle+  (hClose . snd)+  (\(path, handle) -> do+    nodes <- readIORef graphLog+    path <$ traverse_ (T.hPutStrLn handle) nodes+  )++rethrowHUnitLogged :: MonadIO m => FilePath -> HUnitFailure -> m a+rethrowHUnitLogged path =+  rethrowHUnitWith ("Graph dumped in temp file: " ++ path)++rethrowHUnitWith :: MonadIO m => String -> HUnitFailure -> m a+rethrowHUnitWith message (HUnitFailure l r) =+  throwIO . HUnitFailure l . Reason $ message ++ "\n\n" ++ formatFailureReason r
+ src/Graphula/NoConstraint.hs view
@@ -0,0 +1,14 @@+{-# LANGUAGE FlexibleInstances #-}++-- | An empty 'Constraint'+--+-- Graphula accepts constraints for various uses. Frontends do not always+-- utilize these constraints. 'NoConstraint' is a universal class that all types+-- inhabit. It has no behavior and no additional constraints.+--+module Graphula.NoConstraint+  ( NoConstraint+  ) where++class NoConstraint a+instance NoConstraint a
+ src/Graphula/Node.hs view
@@ -0,0 +1,204 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE InstanceSigs #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE NoStarIsType #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilyDependencies #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE UndecidableSuperClasses #-}++module Graphula.Node+  (+  -- * Generating+    node+  , nodeKeyed++  -- * 'NodeOptions'+  , NodeOptions+  , edit+  , ensure++  -- * Exceptions+  , GenerationFailure(..)+  ) where++import Prelude++import Control.Monad (guard, (<=<))+import Data.Proxy (Proxy(..))+import Data.Semigroup.Generic (gmappend, gmempty)+import Data.Traversable (for)+import Data.Typeable (TypeRep, Typeable, typeRep)+import Database.Persist (Entity(..), Key, PersistEntity, PersistEntityBackend)+import Database.Persist.Sql (SqlBackend)+import GHC.Generics (Generic)+import Graphula.Arbitrary+import Graphula.Class+import Graphula.Dependencies+import Test.QuickCheck (Arbitrary(..))+import UnliftIO.Exception (Exception, throwIO)++-- | Options for generating an individual node+--+--+-- 'NodeOptions' can be created and combined with the Monoidal operations '(<>)'+-- and 'mempty'.+--+-- > a1 <- node @A () mempty+-- > a2 <- node @A () $ edit $ \a -> a { someField = True }+-- > a3 <- node @A () $ ensure $ (== True) . someField+--+newtype NodeOptions a = NodeOptions+  { nodeOptionsEdit :: Kendo Maybe a+  }+  deriving stock Generic++instance Semigroup (NodeOptions a) where+  (<>) = gmappend+  {-# INLINE (<>) #-}++instance Monoid (NodeOptions a) where+  mempty = gmempty+  {-# INLINE mempty #-}++-- | Like @'Endo'@ but uses Kliesli composition+newtype Kendo m a = Kendo { appKendo :: a -> m a }+    deriving stock Generic++instance Monad m => Semigroup (Kendo m a) where+  Kendo f <> Kendo g = Kendo $ f <=< g+  {-# INLINE (<>) #-}++instance Monad m => Monoid (Kendo m a) where+  mempty = Kendo pure+  {-# INLINE mempty #-}++-- | Modify the node after it's been generated+--+-- > a <- node @A () $ edit $ \a -> a { someField = True }+--+edit :: (a -> a) -> NodeOptions a+edit f = mempty { nodeOptionsEdit = Kendo $ Just . f }++-- | Require a node to satisfy the specified predicate+--+-- > a <- node @A () $ ensure $ (== True) . someField+--+-- N.B. ensuring a condition that is infrequently met can be innefficient.+--+ensure :: (a -> Bool) -> NodeOptions a+ensure f = mempty { nodeOptionsEdit = Kendo $ \a -> a <$ guard (f a) }++-- | Generate a node with a default (Database-provided) key+--+-- > a <- node @A () mempty+--+node+  :: forall a m+   . ( MonadGraphula m+     , Logging m a+     , Arbitrary a+     , HasDependencies a+     , GenerateKey a+     , PersistEntityBackend a ~ SqlBackend+     , PersistEntity a+     , Typeable a+     )+  => Dependencies a+  -> NodeOptions a+  -> m (Entity a)+node = nodeImpl $ generate $ generateKey @(KeySource a) @a++-- | Generate a node with an explictly-given key+--+-- > let someKey = UUID.fromString "..."+-- > a <- nodeKeyed @A someKey () mempty+--+nodeKeyed+  :: forall a m+   . ( MonadGraphula m+     , Logging m a+     , Arbitrary a+     , HasDependencies a+     , PersistEntityBackend a ~ SqlBackend+     , PersistEntity a+     , Typeable a+     )+  => Key a+  -> Dependencies a+  -> NodeOptions a+  -> m (Entity a)+nodeKeyed key = nodeImpl $ pure $ Just key++nodeImpl+  :: forall a m+   . ( MonadGraphula m+     , Logging m a+     , Arbitrary a+     , HasDependencies a+     , PersistEntityBackend a ~ SqlBackend+     , PersistEntity a+     , Typeable a+     )+  => m (Maybe (Key a))+  -> Dependencies a+  -> NodeOptions a+  -> m (Entity a)+nodeImpl genKey dependencies NodeOptions {..} = attempt 100 10 $ do+  initial <- generate arbitrary+  for (appKendo nodeOptionsEdit initial) $ \edited -> do+    -- N.B. dependencies setting always overrules edits+    let hydrated = edited `dependsOn` dependencies+    logNode hydrated+    mKey <- genKey+    pure (mKey, hydrated)++data GenerationFailure+  = GenerationFailureMaxAttemptsToConstrain TypeRep+  -- ^ Could not satisfy constraints defined using 'ensure'+  | GenerationFailureMaxAttemptsToInsert TypeRep+  -- ^ Could not satisfy database constraints on 'insert'+  deriving stock (Show, Eq)++instance Exception GenerationFailure++attempt+  :: forall a m+   . ( MonadGraphula m+     , PersistEntityBackend a ~ SqlBackend+     , PersistEntity a+     , Typeable a+     )+  => Int+  -> Int+  -> m (Maybe (Maybe (Key a), a))+  -> m (Entity a)+attempt maxEdits maxInserts source = loop 0 0+ where+  loop :: Int -> Int -> m (Entity a)+  loop numEdits numInserts+    | numEdits >= maxEdits = die GenerationFailureMaxAttemptsToConstrain+    | numInserts >= maxInserts = die GenerationFailureMaxAttemptsToInsert+    | otherwise = source >>= \case+      Nothing -> loop (succ numEdits) numInserts+      --               ^ failed to edit, only increments this+      Just (mKey, value) -> insert mKey value >>= \case+        Nothing -> loop (succ numEdits) (succ numInserts)+        --               ^ failed to insert, but also increments this. Are we+        --                 sure that's what we want?+        Just a -> pure a++  die :: (TypeRep -> GenerationFailure) -> m (Entity a)+  die e = throwIO $ e $ typeRep (Proxy :: Proxy a)
− test/Graphula/UUIDKey.hs
@@ -1,47 +0,0 @@-{-# LANGUAGE DerivingStrategies #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedStrings #-}--module Graphula.UUIDKey-  ( UUIDKey-  )-where--import Prelude--import Data.Aeson (FromJSON, ToJSON)-import qualified Data.Text as Text-import Data.UUID (UUID)-import qualified Data.UUID as UUID-import Database.Persist-import Database.Persist.Sql-import Test.QuickCheck (Arbitrary(..), Gen, getLarge)-import Web.HttpApiData (FromHttpApiData, ToHttpApiData)-import Web.PathPieces (PathPiece(..))---- | Example non-serial key-newtype UUIDKey = UUIDKey { unUUIDKey :: UUID }-  deriving newtype (Eq, Show, Ord, Read, FromJSON, ToJSON, ToHttpApiData, FromHttpApiData)--instance Arbitrary UUIDKey where-  arbitrary = UUIDKey <$> uuid-    where uuid = UUID.fromWords <$> large <*> large <*> large <*> large--large :: (Integral a, Bounded a) => Gen a-large = getLarge <$> arbitrary--instance PathPiece UUIDKey where-  toPathPiece = Text.pack . UUID.toString . unUUIDKey-  fromPathPiece = fmap UUIDKey . UUID.fromString . Text.unpack--instance PersistField UUIDKey where-  toPersistValue = PersistText . Text.pack . UUID.toString . unUUIDKey-  fromPersistValue = \case-    PersistText t -> case UUID.fromString $ Text.unpack t of-      Just x -> Right $ UUIDKey x-      Nothing -> Left "Invalid UUID"-    _ -> Left "Not PersistText"--instance PersistFieldSql UUIDKey where-  sqlType _ = SqlOther "uuid"
test/README.lhs view
@@ -1,6 +1,7 @@-# Graphula Core+# Graphula -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.+Graphula is a simple interface for generating persistent data and linking its+dependencies. We use this interface to generate fixtures for automated testing.   <!--@@ -24,7 +25,6 @@  module Main (module Main) where -import Control.Monad (replicateM_) import Control.Monad.IO.Class import Control.Monad.IO.Unlift import Control.Monad.Logger (NoLoggingT)@@ -32,12 +32,11 @@ import Control.Monad.Trans.Resource (ResourceT) import Database.Persist.Sqlite import Database.Persist.TH-import Data.Typeable import GHC.Generics (Generic) import Graphula-import Graphula.UUIDKey import Test.Hspec import Test.QuickCheck+import Test.QuickCheck.Arbitrary.Generic import Text.Markdown.Unlit ()  instance (ToBackendKey SqlBackend a) => Arbitrary (Key a) where@@ -47,115 +46,98 @@  ## Arbitrary Data -Graphula utilizes `QuickCheck` to generate random data. We need to declare `Arbitrary` instances for our types.+Graphula utilizes `QuickCheck` to generate random data. We need to declare+`Arbitrary` instances for our models.  ```haskell share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|-A-  a String-  b Int+School+  name String   deriving Show Eq Generic -B-  a AId-  b String+Teacher+  schoolId SchoolId+  name String   deriving Show Eq Generic -C-  a AId-  b BId-  c String+Course+  schoolId SchoolId+  teacherId TeacherId+  name String   deriving Show Eq Generic -D-  Id UUIDKey-  a Int-  b String+Student+  name String   deriving Show Eq Generic -E-  Id DId sqltype=uuid-  a String+Question+  content String   deriving Show Eq Generic -F-  a Bool-  UniqueFA a+Answer+  questionId QuestionId+  studentId StudentId+  yes Bool+  UniqueAnswer questionId studentId   deriving Show Eq Generic |] -instance Arbitrary A where-  arbitrary = A <$> arbitrary <*> arbitrary+instance Arbitrary School where+  arbitrary = genericArbitrary -instance Arbitrary B where-  arbitrary = B <$> arbitrary <*> arbitrary+instance Arbitrary Teacher where+  arbitrary = genericArbitrary -instance Arbitrary C where-  arbitrary = C <$> arbitrary <*> arbitrary <*> arbitrary+instance Arbitrary Course where+  arbitrary = genericArbitrary -instance Arbitrary D where-  arbitrary = D <$> arbitrary <*> arbitrary+instance Arbitrary Student where+  arbitrary = genericArbitrary -instance Arbitrary E where-  arbitrary = E <$> arbitrary+instance Arbitrary Question where+  arbitrary = genericArbitrary -instance Arbitrary F where-  arbitrary = F <$> arbitrary+instance Arbitrary Answer where+  arbitrary = genericArbitrary ```  ## 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.+We declare dependencies via the `HasDependencies` typeclass and its associated+type `Dependencies`. If a model does not have any dependencies, we only need to+declare an empty instance.  ```haskell-instance HasDependencies A-instance HasDependencies F-```+instance HasDependencies School -For single dependencies we use the `Only` type.+instance HasDependencies Student -```haskell-instance HasDependencies B where-  type Dependencies B = Only AId+instance HasDependencies Question ``` -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.+For single-dependency models, we use the `Only` type.  ```haskell-instance HasDependencies C where-  type Dependencies C = (AId, BId)+instance HasDependencies Teacher where+  type Dependencies Teacher = Only SchoolId ``` -## 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.+Multi-dependency models use tuples. Declare these dependencies in the order they+appear in the model's type definition. `HasDependencies` leverages generic+programming to inject dependencies for you.  ```haskell-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 Course where+  type Dependencies Course = (SchoolId, TeacherId) -```haskell-instance HasDependencies E where-  type KeySource E = 'SourceExternal+instance HasDependencies Answer where+  type Dependencies Answer = (QuestionId, StudentId) ``` -By default, `HasDependencies` instances use `type KeySource _ = 'SourceDefault`, which means-that graphula will expect the database to provide a key.--## Serialization+## Logging failures -Graphula allows logging of graphs via `runGraphulaLogged`. Graphula dumps graphs-to a temp file on test failure.+`runGraphulaLogged` will dump generated data to a temporary file. Or+`runGraphulaLoggedWithFileT` can be used to pass an explicit path.  ```haskell loggingSpec :: IO ()@@ -164,17 +146,20 @@     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"+      student <- node @Student () mempty+      question <- node @Question () mempty+      answer <- node @Answer+        (entityKey question, entityKey student)+        $ edit $ \a -> a { answerYes = True } -  failingGraph-    `shouldThrow` anyException+      -- Test failures will cause the graph to be logged (not any exception)+      liftIO $ answerYes (entityVal answer) `shouldBe` False -  n <- lines <$> readFile "test.graphula"+  failingGraph `shouldThrow` anyException++  n <- lines <$> readFile logFile   n `shouldSatisfy` (not . null) ``` @@ -184,69 +169,23 @@ 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+    school <- node @School () mempty+    teacher <- node @Teacher (Only $ entityKey school) mempty+    course <- node @Course (entityKey school, entityKey teacher) mempty+    student <- node @Student () $ edit $ \s -> s { studentName = "Pat" }+    question <- node @Question () mempty+    answer <- node @Answer+      (entityKey question, entityKey student)+      $ edit $ \a -> a { answerYes = True } -    -- 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.--```haskell-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:--```haskell-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:--```haskell-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)))+      -- Typically, you would run some other function like "fetch correct+      -- answers at school" and assert you found the correct answers you+      -- generated. In this example we just assert some things about the data+      -- directly:+      teacherSchoolId (entityVal teacher) `shouldBe` entityKey school+      courseTeacherId (entityVal course) `shouldBe` entityKey teacher+      answerYes (entityVal answer) `shouldBe` True ```  <!--@@ -256,9 +195,6 @@   describe "graphula" . parallel $ do     it "generates and links arbitrary graphs of data" simpleSpec     it "allows logging graphs" loggingSpec-    it "attempts to retry node generation on insertion failure" insertionFailureSpec-    it "attempts to retry node generation on a database constraint violation" constraintFailureSpec-    it "attempts to retry node generation on unsatisfiable predicates" ensureFailureSpec  runDB :: MonadUnliftIO m => ReaderT SqlBackend (NoLoggingT (ResourceT m)) a -> m a runDB f = runSqlite "test.db" $ do