packages feed

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