lorentz-0.15.1: src/Lorentz/Util/TH.hs
-- SPDX-FileCopyrightText: 2021 Oxhead Alpha
-- SPDX-License-Identifier: LicenseRef-MIT-OA
-- | Lorentz template-haskell and quasiquote utilities.
module Lorentz.Util.TH
( entrypointDoc
, errorDocArg
, typeDoc
) where
import Prelude
import Data.Char (isSpace)
import Language.Haskell.TH (Dec, Q, conE, conT, litE, litT, mkName, strTyLit, stringL, varE)
import Language.Haskell.TH.Quote (QuasiQuoter(..))
import Text.ParserCombinators.ReadP (ReadP, choice, eof, munch1, readP_to_S, skipSpaces, string)
import Text.Read.Lex (Lexeme(..), lex)
import Lorentz.Doc
import Lorentz.Entrypoints
import Lorentz.Errors
-- | QuasiQuote that helps generating @ParameterHasEntrypoints@ instance.
--
-- Usage:
--
-- @
-- [entrypointDoc| Parameter \<parameter-type> [\<root-annotation>] |]
-- [entrypointDoc| Parameter plain |]
-- [entrypointDoc| Parameter plain "root"|]
-- @
--
-- See this [tutorial](https://indigo-lang.gitlab.io/contract-docs/) which
-- includes this quasiquote.
--
entrypointDoc :: QuasiQuoter
entrypointDoc = mkParserQQ "entrypointDoc"
"Parameter <parameter-type> [<root-annotation>]"
[ "Parameter plain"
, "Parameter recursive"
, "Parameter plain \"root\""
] do
skipSpaces
typeName <- conT . mkName <$> hsIdent
skipSpaces
paramType <- conT <$> choice
[ string "plain" $> ''EpdPlain
, string "delegate" $> ''EpdDelegate
, string "recursive" $> ''EpdRecursive
, string "none" $> ''EpdNone
]
skipSpaces
mbRootAnn <- optional $ litT . strTyLit <$> hsString
skipSpaces
eof
let epd = maybe paramType (\ann -> [t|EpdWithRoot $ann $paramType|]) mbRootAnn
pure $ [d|
instance ParameterHasEntrypoints $typeName where
type ParameterEntrypointsDerivation $typeName = $epd
|]
-- | QuasiQuote that helps generating @CustomErrorHasDoc@ instance.
--
-- Usage:
--
-- @
-- [errorDocArg| \<error-name> \<error-type> \<error-description> [\<error-arg-type>] |]
-- [errorDocArg| "errorName" exception "Error description" |]
-- [errorDocArg| "errorName" contract-internal "Error description" () |]
-- [errorDocArg| "errorName" bad-argument "Error description" Integer |]
-- @
--
-- The default argument type is 'NoErrorArg'. Only a type name can be used,
-- if you need complex type, define a type synonym.
--
-- See this [tutorial](https://indigo-lang.gitlab.io/contract-docs/) which
-- includes this quasiquote.
--
errorDocArg :: QuasiQuoter
errorDocArg = mkParserQQ "errorDocArg"
"<error-name> <error-type> <error-description> [<error-arg-type>]"
[ "\"errorName\" exception \"Error description\""
, "\"myError\" bad-argument \"An error happened\" ()"
, "\"ctrError\" contract-internal \"Internal counter error\" Integer"
] do
skipSpaces
errorName <- litT . strTyLit <$> hsString
skipSpaces
errorClass <- conE <$> choice
[ string "exception" $> 'ErrClassActionException
, string "bad-argument" $> 'ErrClassBadArgument
, string "contract-internal" $> 'ErrClassContractInternal
, string "unknown" $> 'ErrClassUnknown
]
skipSpaces
errorDesc <- litE . stringL <$> hsString
skipSpaces
errorArg <- optional $ conT . mkName <$> hsIdent
skipSpaces
eof
let errorArgType = fromMaybe [t|NoErrorArg|] errorArg
pure [d|
type instance ErrorArg $errorName = $errorArgType
instance CustomErrorHasDoc $errorName where
customErrClass = $errorClass
customErrDocMdCause = $errorDesc
|]
-- | QuasiQuote that helps generating @TypeHasDoc@ instance.
--
-- Usage:
--
-- @
-- [typeDoc| \<type> \<description> [\<field naming strategy>] |]
-- [typeDoc| Storage "This is storage description" |]
-- [typeDoc| Storage "This is storage description" stripFieldPrefix |]
-- @
--
-- @field naming strategy@ is optional, and is a function with signature @Text
-- -> Text@. Common strategies include 'id' and @stripFieldPrefix@. If
-- unspecified, ultimately defaults to 'id'.
--
-- See this [tutorial](https://indigo-lang.gitlab.io/contract-docs/) which
-- includes this quasiquote.
--
typeDoc :: QuasiQuoter
typeDoc = mkParserQQ "typeDoc"
"<type> <description> [<field naming strategy>]"
[ "Storage \"This is storage description\""
, "Storage \"This is storage description\" stripFieldPrefix"
] do
skipSpaces
typeName <- conT . mkName <$> hsIdent
skipSpaces
desc <- litE . stringL <$> hsString
skipSpaces
fnstrategy <- optional $ varE . mkName <$> hsIdent
skipSpaces
eof
pure $ liftA2 (<>)
[d|
instance TypeHasDoc $typeName where
typeDocMdDescription = $desc
|]
case fnstrategy of
Nothing -> mempty
Just strat' ->
[d|
instance TypeHasFieldNamingStrategy $typeName where
typeFieldNamingStrategy = $strat'
|]
--------------------------------------------------
-- Helper
--------------------------------------------------
failQQ :: MonadFail m => Text -> Text -> m a
failQQ qq errTxt =
fail $ "Lorentz.Util.TH." <> toString (qq <> ": " <> errTxt)
failQQType :: MonadFail m => Text -> Text -> m a
failQQType qq typeTxt = failQQ qq $ "This QuasiQuoter cannot be used as a " <> typeTxt
mkParserQQ :: Text -> Text -> [Text] -> ReadP (Q [Dec]) -> QuasiQuoter
mkParserQQ qqName format examples parser = QuasiQuoter
{ quoteExp = const $ failQQType qqName "expression"
, quotePat = const $ failQQType qqName "pattern"
, quoteType = const $ failQQType qqName "type"
, quoteDec = go
}
where
parse = readP_to_S parser
mkSample text = " - [" <> qqName <> "| " <> text <> " |]"
go input = case parse input of
[(res, "")] -> res
_ -> failQQ qqName $ errTemplate <> toText input
errTemplate = unlines $
[ "Invalid arguments."
, " Expected arguments to be in the format of:"
, " - [" <> qqName <> "| " <> format <> " |]"
, " Examples:"
] <> map mkSample examples <>
[ " But instead got: " ]
hsIdent :: ReadP String
hsIdent = munch1 (not . isSpace)
hsString :: ReadP String
hsString = do
String x <- lex
pure x