{-| Module : Data.Aeson.Schema.TH.Enum Maintainer : Brandon Chinn Stability : experimental Portability : portable Template Haskell functions for Enum types. -} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE TemplateHaskell #-} module Data.Aeson.Schema.TH.Enum ( genFromJSONEnum , genToJSONEnum , mkEnum ) where import Control.Monad (forM, unless) import Data.Aeson (FromJSON(..), ToJSON(..), Value(..)) import Data.Char (toLower) import Data.Maybe (mapMaybe) import qualified Data.Text as Text import Language.Haskell.TH import Language.Haskell.TH.Syntax (lift) -- | Make an enum type with the given constructors, that can be parsed from JSON. -- -- The 'FromJSON' instance will match to a string value matching the constructor name, -- case-insensitive. -- -- @ -- mkEnum \"State" [\"OPEN", \"CLOSED"] -- -- -- generates equivalent of: -- -- data State = OPEN | CLOSED deriving (...) -- -- genFromJSONEnum ''State -- -- genToJSONEnum ''State -- @ mkEnum :: String -> [String] -> Q [Dec] mkEnum name vals = concat <$> sequence [ (:[]) <$> dataDec , mkFromJSON name' vals' , mkToJSON name' vals' ] where name' = mkName name vals' = map mkName vals dataDec = dataD (pure []) name' [] Nothing (map toCon vals') [derivClause Nothing deriveClasses] deriveClasses = [ [t| Eq |] , [t| Ord |] , [t| Show |] , [t| Enum |] ] toCon val = normalC val [] -- | Generate an instance of 'FromJSON' for the given data type. -- -- Prefer using 'mkEnum'; this function is useful for data types in which you want greater control -- over the actual data type. -- -- The 'FromJSON' instance will match to a string value matching the constructor name, -- case-insensitive. -- -- @ -- data State = Open | CLOSED deriving (Show,Enum) -- genFromJSONEnum ''State -- -- -- outputs: -- -- Just Open -- -- Just Open -- -- Just CLOSED -- -- Just CLOSED -- main = mapM_ print -- [ decodeState \"open" -- , decodeState \"OPEN" -- , decodeState \"closed" -- , decodeState \"CLOSED" -- ] -- where -- decodeState :: String -> Maybe State -- decodeState = decode . show -- @ genFromJSONEnum :: Name -> Q [Dec] genFromJSONEnum name = getEnumConstructors name >>= mkFromJSON name -- | Generate an instance of 'ToJSON' for the given data type. -- -- Prefer using 'mkEnum'; this function is useful for data types in which you want greater control -- over the actual data type. -- -- The 'ToJSON' instance will encode the enum as a string matching the constructor name. -- -- @ -- data State = Open | CLOSED deriving (Show,Enum) -- genToJSONEnum ''State -- -- -- outputs: -- -- \"Open" -- -- \"CLOSED" -- main = mapM_ print -- [ encode Open -- , encode CLOSED -- ] -- @ genToJSONEnum :: Name -> Q [Dec] genToJSONEnum name = getEnumConstructors name >>= mkToJSON name {- Helpers -} getEnumConstructors :: Name -> Q [Name] getEnumConstructors name = do -- check if 'name' is an Enum ClassI _ instances <- reify ''Enum let instanceNames = flip mapMaybe instances $ \case InstanceD _ _ (AppT _ (ConT n)) _ -> Just n _ -> Nothing unless (name `elem` instanceNames) $ fail $ "Not an Enum type: " ++ show name -- extract constructor names reify name >>= \case TyConI (DataD _ _ _ _ cons _) -> forM cons $ \case NormalC con [] -> return con con -> fail $ "Invalid constructor: " ++ show con info -> fail $ "Invalid data type: " ++ show info mkFromJSON :: Name -> [Name] -> Q [Dec] mkFromJSON name cons = do let toPattern = litP . stringL . map toLower . nameBase toMatch con = match (toPattern con) (normalB [| pure $(conE con) |]) [] t <- newName "t" let parseEnum = caseE [| Text.unpack $ Text.toLower $(varE t) |] $ map toMatch cons ++ [match wildP (normalB $ appE badParse $ varE t) []] [d| instance FromJSON $(conT name) where parseJSON (String $(varP t)) = $parseEnum parseJSON v = $badParse v |] where badParse = let prefix = litE $ stringL $ "Bad " ++ nameBase name ++ ": " in [| fail . ($prefix ++) . show |] mkToJSON :: Name -> [Name] -> Q [Dec] mkToJSON name cons = [d| instance ToJSON $(conT name) where toJSON = $(lamCaseE $ map encodeConstructor cons) |] where encodeConstructor con = match (conP con []) (normalB [| String $ Text.pack $(lift $ nameBase con) |]) []