packages feed

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