{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DefaultSignatures #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} module GraphQL.API.Enum ( GraphQLEnum(..) ) where import Protolude hiding (Enum, TypeError) import GraphQL.Internal.Name (Name, nameFromSymbol, NameError) import GraphQL.Internal.Output (GraphQLError(..)) import GHC.Generics (D, (:+:)(..)) import GHC.TypeLits (KnownSymbol, TypeError, ErrorMessage(..)) invalidEnumName :: forall t. NameError -> Either Text t invalidEnumName x = Left ("In Enum: " <> formatError x) -- TODO: Enums have a slightly more restricted set of names than 'Name' -- implies. Especially, they cannot be 'true', 'false', or 'nil'. The parser -- /probably/ guarantees this, so it should export this guarantee by providing -- an 'Enum' type. class GenericEnumValues (f :: Type -> Type) where genericEnumValues :: [Either NameError Name] -- XXX: Why is this 'Text' and not 'NameError'? genericEnumFromValue :: Name -> Either Text (f p) genericEnumToValue :: f p -> Name instance forall conName m p f nt. ( KnownSymbol conName , KnownSymbol m , KnownSymbol p , GenericEnumValues f ) => GenericEnumValues (M1 D ('MetaData conName m p nt) f) where genericEnumValues = genericEnumValues @f genericEnumFromValue name = M1 <$> genericEnumFromValue name genericEnumToValue (M1 gv) = genericEnumToValue gv instance forall conName f p b. ( KnownSymbol conName , GenericEnumValues f ) => GenericEnumValues (C1 ('MetaCons conName p b) U1 :+: f) where genericEnumValues = let name = nameFromSymbol @conName in name:genericEnumValues @f genericEnumFromValue vname = case nameFromSymbol @conName of Right name -> if name == vname then L1 <$> Right (M1 U1) else R1 <$> genericEnumFromValue vname Left x -> invalidEnumName x genericEnumToValue (L1 _) = case nameFromSymbol @conName of Right name -> name -- XXX: This is impossible to catch during validation, because we cannot -- validate type-level symbols, we can only validate values. We could -- show that the schema is invalid at the type-level and still decide to -- call this anyway. The error should rather say that the schema is -- invalid. -- -- Further, we don't actually have any schema-level validation, so -- "should have been caught during validation" is misleading. Left err -> panic ("Invalid name: " <> show err <> ". This should have been caught during validation. Please file a bug.") genericEnumToValue (R1 gv) = genericEnumToValue gv instance forall conName p b. (KnownSymbol conName) => GenericEnumValues (C1 ('MetaCons conName p b) U1) where genericEnumValues = let name = nameFromSymbol @conName in [name] genericEnumFromValue vname = case nameFromSymbol @conName of Right name -> if name == vname then Right (M1 U1) else Left ("Not a valid choice for enum: " <> show vname) Left x -> invalidEnumName x genericEnumToValue (M1 _) = let Right name = nameFromSymbol @conName in name -- TODO(tom): better type errors using `n`. Also type errors for other -- invalid constructors. instance forall conName p b sa sb. ( TypeError ('Text "Constructor not unary: " ':<>: 'Text conName) , KnownSymbol conName ) => GenericEnumValues (C1 ('MetaCons conName p b) (S1 sa sb)) where genericEnumValues = undefined genericEnumFromValue = undefined genericEnumToValue = undefined instance forall conName p b sa sb f. ( TypeError ('Text "Constructor not unary: " ':<>: 'Text conName) , KnownSymbol conName ) => GenericEnumValues (C1 ('MetaCons conName p b) (S1 sa sb) :+: f) where genericEnumValues = undefined genericEnumFromValue = undefined genericEnumToValue = undefined -- | For each enum type we need 1) a list of all possible values 2) a -- way to serialise and 3) deserialise. -- -- TODO: Update this comment to explain what a GraphQLEnum is, why you might -- want an instance, and any laws that apply to method relations. class GraphQLEnum a where -- TODO: Document each of these methods. enumValues :: [Either NameError Name] default enumValues :: (Generic a, GenericEnumValues (Rep a)) => [Either NameError Name] enumValues = genericEnumValues @(Rep a) enumFromValue :: Name -> Either Text a default enumFromValue :: (Generic a, GenericEnumValues (Rep a)) => Name -> Either Text a enumFromValue v = to <$> genericEnumFromValue v enumToValue :: a -> Name default enumToValue :: (Generic a, GenericEnumValues (Rep a)) => a -> Name enumToValue = genericEnumToValue . from