graphula-2.0.0.1: src/Graphula/Internal.hs
{-# 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