registry-options-0.1.0.0: src/Data/Registry/Options/TH.hs
{-# OPTIONS_GHC -fno-warn-orphans #-}
-- | TemplateHaskell functions for creating commands
module Data.Registry.Options.TH where
import Control.Monad.Fail
import Data.List (elemIndex, foldr1)
import Data.Registry.Options.Help
import Data.Registry.Options.OptionDescription (OptionDescription)
import Data.Registry.Options.Text
import Data.String
import Data.Text qualified as T
import Language.Haskell.TH
import Language.Haskell.TH.Lift
import Language.Haskell.TH.Syntax
import Protolude hiding (Type)
deriveLift ''OptionDescription
deriveLift ''Help
-- | Make a command parser for a given data type
-- - the data type name is used to get the command name to parse
-- - each alternative in the data type defines an alternative parser
--
-- Usage: @$(makeCommand ''MyDataType [shortDescription "copy a file"]) <: otherParsers@
-- The type of the resulting parser is @Parser "dataType" MyDataType@
makeCommand :: Name -> [HelpUpdate] -> ExpQ
makeCommand = makeParserWith defaultParserConfiguration True
-- | Make a command parser with some specific parser options
makeCommandWith :: ParserConfiguration -> Name -> [HelpUpdate] -> ExpQ
makeCommandWith parserOptions = makeParserWith parserOptions True
-- | Make a Parser for a given data type, without using the data type as a command name
makeParser :: Name -> ExpQ
makeParser n = makeParserWith defaultParserConfiguration False n []
-- | Options for creating a command parser
data ParserConfiguration = ParserConfiguration
{ -- | make the name a the command from a qualified data type name
makeCommandName :: Text -> Text,
-- | make the type of a field from the command data type, and the qualified field type (if it exists)
makeFieldType :: Text -> Maybe Text -> Text
}
-- | Default parser configuration
-- if the data type is @mypackage.DataType { dataTypeFieldName :: FieldType }@ then
-- - @makeCommandName -> "type"@
-- - @makeFieldType -> "fieldName"@
defaultParserConfiguration :: ParserConfiguration
defaultParserConfiguration =
ParserConfiguration
{ makeCommandName = T.toLower . dropPrefix . dropQualifier,
makeFieldType = \typeName -> maybe "Command" (T.toLower . T.drop (T.length typeName) . dropQualifier)
}
-- | Main TemplateHaskell function for creating a command parser
makeParserWith :: ParserConfiguration -> Bool -> Name -> [HelpUpdate] -> ExpQ
makeParserWith parserOptions isCommand typeName help = do
info <- reify typeName
case info of
-- newtype data constructor
TyConI (NewtypeD _context _name _typeVars _kind c@(NormalC _ [(_, _)]) _deriving) ->
makeSingleConstructor parserOptions isCommand typeName help c
-- regular data constructor with just one field
TyConI (NewtypeD _context _name _typeVars _kind c@(RecC _ [(_, _, _)]) _deriving) ->
makeSingleConstructor parserOptions isCommand typeName help c
-- list of data constructors
TyConI (DataD _context _name _typeVars _kind constructors _deriving) -> do
case constructors of
[c] ->
makeSingleConstructor parserOptions isCommand typeName help c
c : cs -> do
fs <- for (c : cs) fieldsOf
addToRegistry $
[funOf $ makeConstructorsParser parserOptions typeName (c : cs) $ makeHelp help]
<> ( if isCommand
then []
else
(uncurry (makeFieldParser parserOptions typeName) <$> concat fs)
<> (uncurry (makeNoDefaultValues parserOptions typeName) <$> concat fs)
)
[] -> do
qReport True "can not make a Parser for a data type with no constructors"
fail "parser creation failed: cannot create a parser for a data type with no constructors"
other -> do
qReport True ("cannot create a parser for: " <> show other)
fail "parser creation failed"
-- | Make a parser for a single constructor, either a newtype with or without a field name
-- or a regular data constructor
makeSingleConstructor :: ParserConfiguration -> Bool -> Name -> [HelpUpdate] -> Con -> ExpQ
makeSingleConstructor parserOptions isCommand typeName help c = do
fs <- fieldsOf c
cName <- nameOf c
addToRegistry $
[funOf $ makeConstructorParser parserOptions isCommand typeName c $ makeHelp help]
<> ( if isCommand
then []
else
(uncurry (makeFieldParser parserOptions cName) <$> fs)
<> (uncurry (makeNoDefaultValues parserOptions cName) <$> fs)
)
-- | Add a list of parser functions to the registry
addToRegistry :: [ExpQ] -> ExpQ
addToRegistry [] = fail "parsers creation failed"
addToRegistry [g] = g
addToRegistry (g : gs) = g `append` addToRegistry gs
-- | Take an expression representing a function and apply @fun@ in front, in order
-- to add it to a registry
funOf :: ExpQ -> ExpQ
funOf = appE (varE (mkName "fun"))
-- | Make a Parser for a single Constructor, where each field of the constructor is parsed separately
-- \(os: FieldConfiguration) (p0::Parser fieldName0 Text) (p1::Parser fieldName1 Bool) -> Constructor <$> coerce p0 <*> coerce p1
makeConstructorParser :: ParserConfiguration -> Bool -> Name -> Con -> Help -> ExpQ
makeConstructorParser parserOptions isCommand typeName c help = do
let isOptionalCommand = helpDefaultSubcommand help
fs <- fieldsOf c
cName <- nameOf c
let parserParameters =
( \((mFieldName, t), n) -> do
let fieldNameType = fieldNameTypeT parserOptions cName mFieldName
sigP (varP (mkName $ "_p" <> show n)) (conT "Parser" `appT` fieldNameType `appT` pure t)
)
<$> zip fs [(0 :: Int) ..]
let parserType = conT "Parser" `appT` fieldNameTypeT parserOptions cName Nothing `appT` conT typeName
let commandName = makeCommandName parserOptions (show cName)
let parserWithHelp =
varE "addParserHelp"
`appE` runQ [|help {helpCommandName = Just commandName}|]
`appE` applyParser parserOptions isCommand isOptionalCommand cName [0 .. (length fs - 1)]
lamE parserParameters (sigE parserWithHelp parserType)
-- | Make a Parser for a several Constructors, where each field of each the constructor is parsed separately
-- and an alternative is taken between all the parsers
-- \(os: FieldConfiguration) (p0::Parser fieldName1 Text) (p1::Parser fieldName1 Bool) (p2::Parser fieldName2 Bool) ->
-- (Constructor1 <$> coerce p0 <*> coerce p1) <|> (Constructor2 <$> coerce p1 <*> coerce p3)
makeConstructorsParser :: ParserConfiguration -> Name -> [Con] -> Help -> ExpQ
makeConstructorsParser parserOptions typeName cs help = do
-- take the fields of all the constructors
-- and make a parameter list with the corresponding parsers
fs <- join <$> for cs fieldsOf
let parserParameters =
( \((mFieldName, t), n) -> do
let fieldNameType = fieldNameTypeT parserOptions typeName mFieldName
sigP (varP (mkName $ "_p" <> show n)) (conT "Parser" `appT` fieldNameType `appT` pure t)
)
<$> zip fs [(0 :: Int) ..]
let appliedParsers =
( \c -> do
cName <- nameOf c
cFields <- fieldsOf c
constructorTypes <- indexConstructorTypes fs cFields
applyParser parserOptions False False cName constructorTypes
)
<$> cs
let commandName = makeCommandName parserOptions (show typeName)
let commandNameParser = varE "commandNameParser" `appE` stringE (toS commandName)
let parserAlternatives =
varE "*>"
`appE` commandNameParser
`appE` (varE "addParserHelp" `appE` runQ [|help {helpCommandName = Just commandName}|] `appE` foldr1 (\p r -> varE "<|>" `appE` p `appE` r) appliedParsers)
-- the string type for the final parser is entirely derived from the data type name
let parserTypeName = fieldNameTypeT parserOptions typeName Nothing
let parserType = conT "Parser" `appT` parserTypeName `appT` conT typeName
lamE parserParameters (sigE parserAlternatives parserType)
-- | Apply a constructor to parsers for each of its fields
-- The resulting parser is a command parser @Parser "Command" DataType@ for a command
-- @ConstructorName <$> coerce p0 <*> coerce p1 ...@
applyParser :: ParserConfiguration -> Bool -> Bool -> Name -> [Int] -> ExpQ
applyParser parserOptions isCommand isOptionalCommand cName ns = do
let commandName = makeCommandName parserOptions (show cName)
let commandNameParser = if isCommand then varE "commandNameParser" `appE` stringE (toS commandName) else varE "unitParser"
-- a default subcommand might be optional, in that case it is ok if the command name is not parsed
let commandParser = varE "*>" `appE` (if isOptionalCommand then varE "<|>" `appE` commandNameParser `appE` varE "unitParser" else commandNameParser)
let cons = commandParser `appE` (varE "pure" `appE` conE cName)
case ns of
[] -> cons
(n : rest) ->
foldr (\i r -> varE "<*>" `appE` r `appE` parseAt i) (varE "<*>" `appE` cons `appE` parseAt n) (reverse rest)
where
parseAt i = varE "coerceParser" `appE` varE (mkName $ "_p" <> show i)
-- | Get the types of all the fields of a constructor
typesOf :: Con -> Q [Type]
typesOf (NormalC _ types) = pure (snd <$> types)
typesOf (RecC _ types) = pure $ (\(_, _, t) -> t) <$> types
typesOf other = do
qReport True ("we can only create a parser for normal constructors and records, got: " <> show other)
fail "parser creation failed"
-- | Get the types of all the fields of a constructor
fieldsOf :: Con -> Q [(Maybe Name, Type)]
fieldsOf (NormalC _ types) = pure $ (\(_, t) -> (Nothing, t)) <$> types
fieldsOf (RecC _ types) = pure $ (\(n, _, t) -> (Just n, t)) <$> types
fieldsOf other = do
qReport True ("we can only create a parser for normal constructors and records, got: " <> show other)
fail "parser creation failed"
-- | Return the name of a constructor
nameOf :: Con -> Q Name
nameOf (NormalC n _) = pure n
nameOf (RecC n _) = pure n
nameOf other = do
qReport True ("we can only create a parser for normal constructors and records, got: " <> show other)
fail "parser creation failed"
-- | Given the list of all possible fields and their types, across all the alternatives of an ADT,
-- return the indices for a specific subset
indexConstructorTypes :: [(Maybe Name, Type)] -> [(Maybe Name, Type)] -> Q [Int]
indexConstructorTypes allFields constructorFields =
for constructorFields $ \f ->
case elemIndex f allFields of
Just n -> pure n
Nothing -> fail $ "the field " <> show f <> " cannot be found in the list of all the fields " <> show allFields
-- | Make a Parser for a given field
makeFieldParser :: ParserConfiguration -> Name -> Maybe Name -> Type -> ExpQ
makeFieldParser parserOptions constructorName mFieldName fieldType = do
let fieldNameType = fieldNameTypeT parserOptions constructorName mFieldName
let fieldName = maybe (conE "Positional") (const $ conE "NonPositional") mFieldName
varE "fun"
`appE` lamE
[sigP (varP "ps") (conT "FieldConfiguration")]
( (varE "parseField" `appTypeE` fieldNameType `appTypeE` pure fieldType)
`appE` varE "ps"
`appE` fieldName
`appE` stringE (toS $ displayType fieldType)
)
-- | Add no default values for a given field name to the registry
makeNoDefaultValues :: ParserConfiguration -> Name -> Maybe Name -> Type -> ExpQ
makeNoDefaultValues parserOptions constructorName mFieldName fieldType =
varE "setNoDefaultValues" `appTypeE` fieldNameTypeT parserOptions constructorName mFieldName `appTypeE` pure fieldType
-- | Return the singleton string type for a given field parser
fieldNameTypeT :: ParserConfiguration -> Name -> Maybe Name -> Q Type
fieldNameTypeT parserOptions constructorName mFieldName =
litT . strTyLit . toS $ makeFieldType parserOptions (dropQualifier . show $ constructorName) (show <$> mFieldName)
-- | Append an expression to a registry
append :: ExpQ -> ExpQ -> ExpQ
append = appOf "<+"
-- | Apply an operator (described as Text) to 2 expressions
appOf :: Text -> ExpQ -> ExpQ -> ExpQ
appOf operator e1 e2 = infixE (Just e1) (varE (mkName $ toS operator)) (Just e2)
instance IsString Name where
fromString = mkName
-- | Display a type name
displayType :: Type -> Text
displayType = show . getTypeName
-- | Return the name of a type in the most frequent cases
getTypeName :: Type -> Name
getTypeName (ForallT _ _ ty) = getTypeName ty
getTypeName (VarT name) = name
getTypeName (ConT name) = name
getTypeName (TupleT n) = tupleTypeName n
getTypeName ArrowT = ''(->)
getTypeName ListT = ''[]
getTypeName (AppT t1 t2) = mkName (show (getTypeName t1) <> " " <> show (getTypeName t2))
getTypeName (SigT t _) = getTypeName t
getTypeName (UnboxedTupleT n) = unboxedTupleTypeName n
getTypeName t = panic $ "getTypeName: Unknown type: " <> show t