optparse-th-0.1.0.0: src/Options/TH.hs
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE CPP #-}
-- | This module is designed to provide a @TemplateHaskell@ alternative to
-- "Options.Generic".
module Options.TH
( deriveParseRecord,
module Options.Generic,
)
where
import Data.Foldable (asum)
import Control.Applicative
import Data.List (foldl')
import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty qualified as NonEmpty
import Data.Text qualified as T
import Data.Traversable
import Language.Haskell.TH
import Language.Haskell.TH.Syntax
import Options.Applicative qualified as Options
import Options.Generic
-- | This function derives 'ParseRecord' for you without incurring a 'Generic'
-- dependency.
--
-- The main barrier here to fully supporting the library is that the API for
-- 'ParseRecord' does not expose the function that provides modifiers by
-- default. So we can provide an instance of 'ParseRecord', but we can't provide
-- a replacement of 'parseRecordWithModifiers', because that function is defined
-- as a top-level that delegates directly to the generic.
--
-- @
-- parseRecordWithModifiers
-- :: (Generic a, GenericParseRecord (Rep a))
-- => Modifiers
-- -> Parser
-- parseRecordWithModifiers modifiers =
-- fmap GHC.Generics.to (genericParseRecord modifiers)
-- @
--
-- This means that we need to shift the options to the compile-time site,
-- instead of the runtime site.
--
-- Likewise, we cannot provide an instance of 'Unwrappable', because it's not
-- a class - it's a type alias for 'Generic' stuff. So we need to create
-- a separate top-level function that does the unwrap.
--
-- @since 0.1.0.0
deriveParseRecord :: Modifiers -> Name -> Q [Dec]
deriveParseRecord modifiers tyName = do
tyInfo <- reify tyName
datatype <-
getDatatypeForInfo tyName tyInfo
liftA2 (<>) (datatypeToInstanceDec modifiers datatype) (datatypeToUnwrapRecordDec tyName datatype)
datatypeToUnwrapRecordDec :: Name -> Datatype -> Q [Dec]
datatypeToUnwrapRecordDec typeName datatype = do
if datatypeIsWrapped datatype
then do
let fnName =
mkName ("unwrapRecord" <> nameBase typeName)
fnType =
[t|$(conT typeName) Wrapped -> $(conT typeName) Unwrapped|]
fnExpr = mkUnwrapRecordExpr datatype
fnSig = SigD fnName <$> fnType
(:)
<$> fnSig
<*> [d|
$(varP fnName) = $(fnExpr)
|]
else do
pure []
mkUnwrapRecordExpr :: Datatype -> Q Exp
mkUnwrapRecordExpr datatype = do
let mkMatch con =
case con of
NormalC name bangTyps -> do
(pat, namesAndTypes) <- do
namesAndTypes <-
for bangTyps \(_bang, typ) -> do
n <- newName "x"
pure (n, typ)
pure
( mkConP name (map (\(varName, _fieldType) -> VarP varName) namesAndTypes)
, namesAndTypes
)
body <- do
let constr = ConE name
fields <- traverse unwrapFields namesAndTypes
pure $ NormalB $ foldl' AppE constr fields
let decs =
[]
pure $
Match pat body decs
RecC name varBangTypes -> do
(pat, varNames) <- do
varNames <-
for varBangTypes \(_fieldName, _bang, typ) -> do
n <- newName "x"
pure (n, typ)
pure
( mkConP name (map (\(varName, _fieldType) -> VarP varName) varNames)
, varNames
)
body <- do
let constr = ConE name
fields <- traverse unwrapFields varNames
pure $ NormalB $ foldl' AppE constr fields
let decs =
[]
pure $
Match pat body decs
_ ->
fail $
unlines
[ "Unexpected constructor in mkUnwrapRecordExpr: "
, " " <> show con
]
matches <-
traverse mkMatch (datatypeConstructors datatype)
pure $ LamCaseE (NonEmpty.toList matches)
-- | Test the type of the field. If it is unwrappable, unwrap it until it isn't.
unwrapFields :: (Name, Type) -> Q Exp
unwrapFields (varName, varTyp) =
case varTyp of
AppT
( AppT
(ConT ((== ''(Options.Generic.:::)) -> True))
(VarT _)
)
rest -> do
tryUnwrapping varName rest
_ ->
varE varName
-- | The goal of this function is to test to see if the constructor is
-- unwrappable: that is, one of <?>, <!>, or <#>.
--
-- If it is unwrappable, then we call the relevant function. Note that we have
-- to try multiple times, since you can put a wrapper in any order.
tryUnwrapping :: Name -> Type -> Q Exp
tryUnwrapping varName = go []
where
go fns varTyp = do
case varTyp of
( AppT
(ConT ((== ''(Options.Generic.<?>)) -> True))
rest
)
`AppT` _helpText ->
do
go (VarE 'unHelpful : fns) rest
AppT
( AppT
(ConT ((== ''(Options.Generic.<!>)) -> True))
rest
)
_defVal ->
do
go (VarE 'unDefValue : fns) rest
AppT
( AppT
(ConT ((== ''(Options.Generic.<#>)) -> True))
rest
)
_shortLabel ->
go (VarE 'unShortName : fns) rest
_ -> do
foldr (\fn acc -> pure fn `appE` acc) (varE varName) fns
getDatatypeForInfo :: Name -> Info -> Q Datatype
getDatatypeForInfo tyName tyInfo =
case tyInfo of
TyConI dec ->
case dec of
DataD _xct name bndrs _mkind constructors _derivs -> do
case constructors of
[] ->
fail $
unlines
[ "A `ParseRecord` instance can't be generated for the following type:"
, " " <> show tyName
, "... because it has no constructors."
]
(c : cs) -> do
pure
Datatype
{ datatypeName =
name
, datatypeConstructors =
c :| cs
, datatypeIsWrapped =
not (null bndrs)
}
NewtypeD _cxt name bndrs _mkind constructor _derivs -> do
pure
Datatype
{ datatypeName =
name
, datatypeConstructors =
pure constructor
, datatypeIsWrapped =
not (null bndrs)
}
_ ->
fail $
unlines
[ "Internal error: Options.TH.getDatatypeForInfo."
, ""
, "This is not your fault. Open a bug report and include the following error for the context in the report: " <> show dec
, "The GHC API provided a `TyConI` wrapping a declaration that was not a `data` or `newtype` declaration, which should never happen."
]
_ -> do
fail $
mconcat
[ "Expected a type constructor in 'deriveParseRecord', got: "
, "\n\t"
, show tyInfo
]
data Datatype = Datatype
{ datatypeName :: Name
, datatypeIsWrapped :: Bool
, datatypeConstructors :: NonEmpty Con
}
datatypeToInstanceDec :: Modifiers -> Datatype -> Q [Dec]
datatypeToInstanceDec mods Datatype {..} = do
let saturatedType =
if datatypeIsWrapped
then ConT datatypeName `AppT` ConT ''Wrapped
else ConT datatypeName
parseRecordExpr <-
case datatypeConstructors of
singleConstructor :| [] -> do
makeSingleCommand mods singleConstructor
subcommands -> do
[|asum|] `appE` listE (NonEmpty.toList (fmap (makeSubcommand mods) subcommands))
[d|
instance ParseRecord $(pure saturatedType) where
parseRecord =
Options.helper <*> $(pure parseRecordExpr)
|]
-- | This function should be called on a single constructor. No subcommand
-- should be created.
makeSingleCommand :: Modifiers -> Con -> Q Exp
makeSingleCommand Modifiers {..} con = do
case con of
NormalC conName bangTypes -> do
-- In this case, we want to create a parser that parses the arguments as
-- positional arguments.
baseCase <- [e|pure $(conE conName)|]
let apps expr (_bang, _type) = do
let label = Nothing @Text
shortName = Nothing @Char
infixE (Just expr) (varE '(<*>)) (Just [e|parseFields Nothing $(lift label) $(lift shortName) Nothing|])
foldl' apps (pure baseCase) bangTypes
RecC conName varBangTypes -> do
-- In this case, we want to create a parser that will use the field names.
baseCase <- [e|pure $(conE conName)|]
let apps expr (fieldName, _bang, _type) = do
let fieldNameString =
nameBase fieldName
label =
Just . T.pack . fieldNameModifier $ fieldNameString
shortName =
shortNameModifier fieldNameString
infixE (Just expr) (varE '(<*>)) (Just [e|parseFields Nothing $(lift label) $(lift shortName) Nothing|])
foldl' apps (pure baseCase) varBangTypes
_ ->
fail $
unlines
[ "Expected either a normal or record constructor, got: "
, "\t" <> show con
, "Other constructors are not supported yet."
]
getConName :: Con -> Q Name
getConName = \case
NormalC n _ ->
pure n
RecC n _ ->
pure n
other ->
fail $
unlines
[ "Expected a normal or record constructor, got unsupported constructor: "
, "\t" <> show other
]
-- | This function should be called with a datatype consisting of multiple
-- constructors. The constructor will be converted into a subcommand name.
makeSubcommand :: Modifiers -> Con -> Q Exp
makeSubcommand modifiers@Modifiers {..} con = do
conName <- getConName con
singleCommandParserExpr <- makeSingleCommand modifiers con
let conNameString =
nameBase conName
name =
constructorNameModifier conNameString
subparserFieldsExpr <-
[e|
Options.command
$(lift name)
( Options.info (Options.helper <*> $(pure singleCommandParserExpr)) mempty
)
<> Options.metavar $(lift name)
|]
[e|Options.subparser $(pure subparserFieldsExpr)|]
#if MIN_VERSION_template_haskell(2,18,0)
mkConP :: Name -> [Pat] -> Pat
mkConP name pats = ConP name [] pats
#else
mkConP :: Name -> [Pat] -> Pat
mkConP name pats = ConP name pats
#endif