lorentz-0.13.0: src/Lorentz/Util/TH.hs
-- SPDX-FileCopyrightText: 2020 Tocqueville Group
--
-- SPDX-License-Identifier: LicenseRef-MIT-TQ
-- | Lorentz template-haskell and quasiquote utilities.
module Lorentz.Util.TH
( entrypointDoc
, errorDoc
, typeDoc
) where
import Data.Text (stripPrefix, stripSuffix)
import Language.Haskell.TH
import Language.Haskell.TH.Quote
import Language.Haskell.TH.Syntax
import Prelude hiding (lift)
import Lorentz.Doc
import Lorentz.Entrypoints
import Lorentz.Errors
-- | QuasiQuote that helps generating @ParameterHasEntrypoints@ instance.
--
-- Usage:
--
-- @
-- [entrypointDoc| Parameter \<parameter-type> \<optional-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 = QuasiQuoter
{ quoteExp = const $ failQQType qqName "expression"
, quotePat = const $ failQQType qqName "pattern"
, quoteType = const $ failQQType qqName "type"
, quoteDec = go
}
where
qqName = "entrypointDoc"
go :: String -> Q [Dec]
go input =
let
mkEpdWithRoot :: Text -> Text -> TypeQ
mkEpdWithRoot epd r =
appT (appT (conT $ mkName "EpdWithRoot") (litT $ strTyLit $ toString $ stripQuote r))
(conT $ mkName (toString epd))
extract :: [Text] -> Either Text (Text, TypeQ)
extract a =
case a of
[x, "plain"] -> Right (x, conT $ mkName $ "EpdPlain")
[x, "delegate"] -> Right (x, conT $ mkName $ "EpdDelegate")
[x, "recursive"] -> Right (x, conT $ mkName $ "EpdRecursive")
[x, "none"] -> Right (x, conT $ mkName $ "EpdNone")
[x, "plain", r] -> Right (x, mkEpdWithRoot "EpdPlain" r)
[x, "delegate", r] -> Right (x, mkEpdWithRoot "EpdDelegate" r)
[x, "recursive", r] -> Right (x, mkEpdWithRoot "EpdRecursive" r)
i -> Left $ unlines
[ "Invalid arguments."
, " Expected arguments to be in the format of:"
, " - [" <> qqName <> "| Parameter <parameter-type> <optional-root-annotation> |]"
, " Examples:"
, " - [" <> qqName <> "| Parameter plain |]"
, " - [" <> qqName <> "| Parameter recursive |]"
, " - [" <> qqName <> "| Parameter plain \"root\" |]"
, " But instead got: " <> unwords i
]
in case extract $ words $ toText input of
Right (param, paramValue) -> [d|
instance ParameterHasEntrypoints $(conT $ mkName $ toString param) where
type ParameterEntrypointsDerivation $(conT $ mkName $ toString param) = $(paramValue)
|]
Left err -> failQQ qqName err
-- | QuasiQuote that helps generating @CustomErrorHasDoc@ instance.
--
-- Usage:
--
-- @
-- [errorDoc| \<error-name> \<error-type> \<error-description> |]
-- [errorDoc| "errorName" exception "Error description" |]
-- @
--
-- See this [tutorial](https://indigo-lang.gitlab.io/contract-docs/) which
-- includes this quasiquote.
--
errorDoc :: QuasiQuoter
errorDoc = QuasiQuoter
{ quoteExp = const $ failQQType qqName "expression"
, quotePat = const $ failQQType qqName "pattern"
, quoteType = const $ failQQType qqName "type"
, quoteDec = go
}
where
qqName = "errorDoc"
errMsg i = unlines
[ "Invalid arguments."
, " Expected arguments to be in the format of:"
, " - [" <> qqName <> "| <error-name> <error-type> <error-description> |]"
, " Examples:"
, " - [" <> qqName <> "| \"errorName\" exception \"Error description\" |]"
, " - [" <> qqName <> "| \"myError\" bad-argument \"An error happened\" |]"
, " But instead got: " <> unwords i
]
go :: String -> Q [Dec]
go input =
let
extract :: [Text] -> Either Text (Text, ExpQ, Text)
extract i = case i of
errorName:errorClassString:errorDesc ->
case readMaybe @ErrorClass (toString errorClassString) of
Just errorClass -> Right
( stripQuote $ errorName
, lift errorClass
, stripQuote . unwords $ errorDesc
)
Nothing -> Left . errMsg $ i
_ -> Left . errMsg $ i
in case extract $ words $ toText input of
Right (errorName, errorClassVal, errorDesc) ->
[d|
type instance ErrorArg $(litT . strTyLit $ toString $ errorName) = ()
instance CustomErrorHasDoc $(litT . strTyLit $ toString $ errorName) where
customErrClass = $(errorClassVal)
customErrDocMdCause = $(litE $ stringL $ toString $ errorDesc)
|]
Left err -> failQQ qqName err
-- | QuasiQuote that helps generating @TypeHasDoc@ instance.
--
-- Usage:
--
-- @
-- [typeDoc| \<type> \<description> |]
-- [typeDoc| Storage "This is storage description" |]
-- @
--
-- See this [tutorial](https://indigo-lang.gitlab.io/contract-docs/) which
-- includes this quasiquote.
--
typeDoc :: QuasiQuoter
typeDoc = QuasiQuoter
{ quoteExp = const $ failQQType qqName "expression"
, quotePat = const $ failQQType qqName "pattern"
, quoteType = const $ failQQType qqName "type"
, quoteDec = go
}
where
qqName = "typeDoc"
go :: String -> Q [Dec]
go input =
case words $ toText $ input of
(param:value) ->
[d|
instance TypeHasDoc $(conT $ mkName $ toString $ param) where
typeDocMdDescription = $(litE $ stringL $ toString $ stripQuote $ unwords value)
|]
i ->
failQQ qqName $ unlines
[ "Invalid arguments."
, " Expected arguments to be in the format of:"
, " - [" <> qqName <> "| <type> <description> |]"
, " Example:"
, " - [" <> qqName <> "| Storage \"This is storage description\" |]"
, " But instead got: " <> unwords i
]
--------------------------------------------------
-- 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
stripQuote :: Text -> Text
stripQuote txt =
let
h = stripPrefix "\"" txt ?: txt
g = stripSuffix "\"" h ?: h
in g