-- SPDX-FileCopyrightText: 2020 Tocqueville Group -- -- SPDX-License-Identifier: LicenseRef-MIT-TQ -- | Utilities for declaring and documenting entry points. module Lorentz.Entrypoints.Doc ( DEntrypoint (..) , DEntrypointReference (..) , EntryArrow (..) , PlainEntrypointsKind , diEntrypointToMarkdown , DEntrypointArg (..) , DType (..) , DeriveCtorFieldDoc (..) , ParamBuilder (..) , ParamBuildingDesc (..) , ParamBuildingStep (..) , mkPbsWrapIn , clarifyParamBuildingSteps , constructDEpArg , emptyDEpArg , mkUType , mkDEpUType , mkDEntrypointArgSimple , DocumentEntrypoints , documentEntrypoint , entryCase , entryCase_ , finalizeParamCallingDoc , areFinalizedParamBuildingSteps , entryCaseSimple_ , entryCaseSimple , RequireFlatParamEps , RequireFlatEpDerivation ) where import Control.Lens.Cons (_head) import Data.Char (toLower) import Data.Constraint (Dict(..)) import qualified Data.Kind as Kind import qualified Data.Map as Map import qualified Data.Text as T import Data.Vinyl.Core (RMap, rappend) import Fmt (Buildable(..), build, fmt, listF) import GHC.Generics ((:+:)) import qualified GHC.Generics as G import qualified Text.Show import Lorentz.ADT import Lorentz.Annotation import Lorentz.Base import Lorentz.Constraints import Lorentz.Doc import Lorentz.Entrypoints.Core import Lorentz.Entrypoints.Helpers import Lorentz.Entrypoints.Impl import Michelson.Printer (printUntypedValue) import Michelson.Printer.Util (RenderDoc(..), needsParens, printDocB) import Michelson.Typed (pattern DefEpName, EpName, mkUType, sampleValueFromUntype) import Michelson.Typed.Doc import Michelson.Typed.Haskell.Doc import Michelson.Typed.Haskell.Instr import qualified Michelson.Untyped as Untyped import Util.Label (Label) import Util.Markdown import Util.Type import Util.TypeLits import Util.TypeTuple -- | Gathers information about single entrypoint. -- -- We assume that entry points might be of different kinds, -- which is designated by phantom type parameter. -- For instance, you may want to have several groups of entry points -- corresponding to various parts of a contract - specifying different @kind@ -- type argument for each of those groups will allow you defining different -- 'DocItem' instances with appropriate custom descriptions for them. data DEntrypoint (kind :: Kind.Type) = DEntrypoint { depName :: Text , depSub :: SubDoc } -- | Default implementation of 'docItemToMarkdown' for entry points. diEntrypointToMarkdown :: HeaderLevel -> DEntrypoint level -> Markdown diEntrypointToMarkdown lvl (DEntrypoint name block) = mdSeparator <> mdHeader lvl (mdTicked . build . over _head toLower $ name) <> ( modifyExample block $ subDocToMarkdown (nextHeaderLevel lvl) $ filterDEntrypointExample block ) where filterDEntrypointExample (SubDoc subdoc) = SubDoc $ Map.delete (docItemPosition @DEntrypointExample) subdoc -- | Modify 'SubDoc' of an entrypoint to replace its example value with the one defined in -- 'DEntrypointExample' in an ad-hoc way. modifyExample :: SubDoc -> Markdown -> Markdown modifyExample (SubDoc sub) subDocMd = case (Map.lookup (docItemPosition @DEntrypointExample) sub) of Just (DocSection ((DocElem b _ ) :| _)) -> mdFindExampleIdAndReplace (docItemToMarkdown (HeaderLevel 0) $ b) subDocMd Nothing -> subDocMd mdFindExampleIdAndReplace :: Markdown -> Markdown -> Markdown mdFindExampleIdAndReplace replaceTxt inputText = build $ unlines $ (\w -> case T.isInfixOf ("id=\"" <> (fmt @Text exampleId) <> "\"") w of True -> fmt @Text $ " + " <> mdSubsection "Example" (mdAddId exampleId $ mdTicked replaceTxt) False -> w ) <$> lines (fmt @Text inputText) where exampleId = "example-id" -- | Default value for 'DEntrypoint' type argument. data PlainEntrypointsKind instance Show (DEntrypoint PlainEntrypointsKind) where show (DEntrypoint name _) = show name instance Eq (DEntrypoint PlainEntrypointsKind) where (DEntrypoint a1 _) == (DEntrypoint a2 _) = a1 == a2 instance Ord (DEntrypoint PlainEntrypointsKind) where (DEntrypoint a1 _) `compare` (DEntrypoint a2 _) = a1 `compare` a2 instance DocItem (DEntrypoint PlainEntrypointsKind) where type DocItemPlacement (DEntrypoint PlainEntrypointsKind) = 'DocItemInlined type DocItemReferenced (DEntrypoint PlainEntrypointsKind) = 'True docItemRef (DEntrypoint name _) = DocItemRefInlined $ DocItemId ("entrypoints-" <> (over _head toLower $ name)) docItemPos = 1000 docItemSectionName = Just "Entrypoints" docItemToMarkdown = diEntrypointToMarkdown docItemToToc lvl d@(DEntrypoint name _) = mdTocFromRef lvl (build . over _head toLower $ name) d data DEntrypointReference = DEntrypointReference Text Anchor instance DocItem DEntrypointReference where docItemPos = 13 docItemSectionName = Nothing docItemToMarkdown _ (DEntrypointReference name anchor) = "Copies behaviour of " <> mdLocalRef (mdTicked $ build name) anchor <> " entrypoint." -- | When describing the way of parameter construction - piece of incremental -- builder for this description. newtype ParamBuilder = ParamBuilder { unParamBuilder :: Markdown -> Markdown -- ^ Argument stands for previously constructed parameter piece, and -- returned value - a piece constructed after our step. } -- | Show what given 'ParamBuilder' does on a sample. pbSample :: ParamBuilder -> Markdown pbSample (ParamBuilder b) = b "ยท" instance Buildable ParamBuilder where build = pbSample instance Show ParamBuilder where show (ParamBuilder pb) = -- Using @'x'@ symbol here because unicode does not render well in 'show' "ParamBuilder " <> show (pb "x") instance Eq ParamBuilder where (==) = (==) `on` pbSample data ParamBuildingDesc = ParamBuildingDesc { pbdEnglish :: Markdown -- ^ Plain english description of this step. , pbdHaskell :: ParamBuilder -- ^ How to construct parameter in Haskell code. , pbdMichelson :: ParamBuilder -- ^ How to construct parameter working on raw Michelson. } deriving stock (Show, Eq) -- | Describes a parameter building step. -- -- This can be wrapping into (Haskell) constructor, or a more complex -- transformation. data ParamBuildingStep -- | Wraps something into constructor with given name. -- Constructor should be the one which corresponds to an entrypoint -- defined via field annotation, for more complex cases use 'PbsCustom'. = PbsWrapIn Text ParamBuildingDesc -- | Directly call an entrypoint marked with a field annotation. | PbsCallEntrypoint EpName -- | Other action. | PbsCustom ParamBuildingDesc -- | This entrypoint cannot be called, which is possible when an explicit -- default entrypoint is present. This is not a true entrypoint but just some -- intermediate node in @or@ tree and neither it nor any of its parents -- are marked with a field annotation. -- -- It contains dummy 'ParamBuildingStep's which were assigned before -- entrypoints were taken into account. | PbsUncallable [ParamBuildingStep] deriving stock (Show, Eq) instance Buildable ParamBuildingStep where build = \case PbsWrapIn ctor _desc -> "Wrap in `" <> build ctor <> "`" PbsCallEntrypoint ep -> "Call entrypoint " <> build ep PbsCustom desc -> "Custom: \"" <> pbdEnglish desc <> "\"" PbsUncallable steps -> "Uncallable; dummy steps: " <> listF steps -- | Make a 'ParamBuildingStep' that tells about wrapping an argument into -- a constructor with given name and uses given 'ParamBuilder' as description of -- Michelson part. mkPbsWrapIn :: Text -> ParamBuilder -> ParamBuildingStep mkPbsWrapIn ctorName michDesc = PbsWrapIn ctorName ParamBuildingDesc { pbdEnglish = "Wrap into " <> mdTicked (build ctorName) <> " constructor." , pbdHaskell = ParamBuilder $ \p -> build ctorName <> " (" <> p <> ")" , pbdMichelson = michDesc } -- | Describes argument of an entrypoint. data DEntrypointArg = DEntrypointArg { epaArg :: Maybe DType -- ^ Argument of the entrypoint. Pass 'Nothing' if no argument is required. , epaBuilding :: [ParamBuildingStep] -- ^ Describes a way to lift an entrypoint argument into full parameter -- which can be passed to the contract. -- -- Steps are supposed to be applied in the order opposite to one in which -- they are given. -- E.g. suppose that an entrypoint is called as @Run (Service1 arg)@; -- then the first step (actual last) should describe wrapping into @Run@ -- constructor, and the second step (actual first) should be about wrapping -- into @Service1@ constructor. , epaType :: Untyped.Type -- ^ Untyped representation of entrypoint, used for printing its michelson -- type representation. } constructDEpArg :: forall arg. ( TypeHasDoc arg , HasAnnotation arg , KnownValue arg ) => DEntrypointArg constructDEpArg = DEntrypointArg { epaArg = Just $ DType (Proxy @arg) , epaBuilding = [] , epaType = mkDEpUType @arg } emptyDEpArg :: DEntrypointArg emptyDEpArg = DEntrypointArg { epaArg = Nothing , epaBuilding = [] , epaType = Untyped.Type Untyped.TUnit Untyped.noAnn } mkDEpUType :: forall t. (KnownValue t, HasAnnotation t) => Untyped.Type mkDEpUType = mkUType (getAnnotation @t FollowEntrypoint) mkDEntrypointArgSimple :: forall t. ( KnownValue t , HasAnnotation t , TypeHasDoc t ) => DEntrypointArg mkDEntrypointArgSimple = DEntrypointArg { epaArg = Just $ DType (Proxy @t) , epaBuilding = [] , epaType = mkDEpUType @t } -- | Go over contract code and update every occurrence of 'DEntrypointArg' -- documentation item, modifying param building steps. modifyParamBuildingSteps :: ([ParamBuildingStep] -> [ParamBuildingStep]) -> (inp :-> out) -> (inp :-> out) modifyParamBuildingSteps f = iMapAnyCode $ modifyInstrDoc (\di -> Just di{ epaBuilding = f (epaBuilding di) }) -- | Go over contract code and update every occurrence of 'DEntrypointArg' -- documentation item, adding the given step to its "how to build parameter" -- description. clarifyParamBuildingSteps :: ParamBuildingStep -> (inp :-> out) -> (inp :-> out) clarifyParamBuildingSteps pbs = modifyParamBuildingSteps (pbs :) instance DocItem DEntrypointArg where docItemPos = 20 docItemSectionName = Nothing docItemDependencies (DEntrypointArg mdty _ _) = [SomeDocDefinitionItem dty | Just dty <- pure mdty] docItemToMarkdown _ (DEntrypointArg mdty psteps et) = mconcat . Prelude.map (<> "\n\n") $ [ mdSubsection "Argument" $ case mdty of Nothing -> "none (pass unit)" Just (DType (dty :: Proxy ep)) -> mconcat . Prelude.intersperse "\n" $ [ mempty , " + " <> mdSubsection "In Haskell" (typeDocMdReference dty (WithinParens False)) , " + " <> mdSubsection "In Michelson" (mdTicked $ printDocB False . renderDoc needsParens $ et) , " + " <> mdSubsection "Example" (mdAddId "example-id" $ mdTicked $ build $ printUntypedValue True $ sampleValueFromUntype et ) ], mdSpoiler "How to call this entrypoint" $ "\n0. Construct an argument for the entrypoint.\n" <> howToCall ] where howToCall = mconcat . Prelude.intersperse "\n" $ -- Markdown re-enumerates enumerated lists automatically Prelude.map ("1. " <>) $ reverse psteps <&> \case PbsWrapIn _ pbd -> renderPbDesc pbd PbsCallEntrypoint ep -> case ep of DefEpName -> "Call the contract (default entrypoint) with the constructed \ \argument." _ -> "Call contract's " <> mdTicked (build ep) <> " entrypoint \ \passing the constructed argument." PbsCustom pbd -> renderPbDesc pbd PbsUncallable _ -> "Feel sad: this entrypoint *cannot* be called and is enlisted \ \here only to describe the parameter structure." -- We could hide such entrypoints, but then in case of incorrect -- use of 'entryCase's or a bug in documentation, understanding -- what's going on would be hard renderPbDesc ParamBuildingDesc{..} = mconcat . Prelude.intersperse "\n" $ [ pbdEnglish , " + " <> mdSubsection "In Haskell" (mdTicked $ pbSample pbdHaskell) , " + " <> mdSubsection "In Michelson" (mdTicked $ pbSample pbdMichelson) ] -- | Pick a type documentation from 'CtorField'. class (KnownSymbol con) => DeriveCtorFieldDoc con (cf :: CtorField) where deriveCtorFieldDoc :: DEntrypointArg instance (KnownSymbol con) => DeriveCtorFieldDoc con 'NoFields where deriveCtorFieldDoc = emptyDEpArg instance (TypeHasDoc ty, HasAnnotation ty, KnownValue ty, KnownSymbol con) => DeriveCtorFieldDoc con ('OneField ty) where deriveCtorFieldDoc = constructDEpArg @ty -- | Add necessary documentation to entry points. documentEntrypoints :: forall a kind inp out. DocumentEntrypoints kind a => Rec (CaseClauseL inp out) (CaseClauses a) -> Rec (CaseClauseL inp out) (CaseClauses a) documentEntrypoints = gDocumentEntrypoints @kind @(G.Rep a) (ParamBuilder id) -- | Constraint for 'documentEntrypoints'. type DocumentEntrypoints kind a = (Generic a, GDocumentEntrypoints kind (G.Rep a)) -- | Traverse entry points and add parameter building step (which describes -- necessity to wrap parameter into some constructor of the given datatype) -- to all parameters described within given code. class GDocumentEntrypoints (kind :: Kind.Type) (x :: Kind.Type -> Kind.Type) where -- | Add corresponding parameter building step. -- -- First argument is accumulator for Michelson description of the building step. gDocumentEntrypoints :: ParamBuilder -> Rec (CaseClauseL inp out) (GCaseClauses x) -> Rec (CaseClauseL inp out) (GCaseClauses x) instance GDocumentEntrypoints kind x => GDocumentEntrypoints kind (G.D1 i x) where gDocumentEntrypoints = gDocumentEntrypoints @kind @x instance ( GDocumentEntrypoints kind x, GDocumentEntrypoints kind y , RSplit (GCaseClauses x) (GCaseClauses y) ) => GDocumentEntrypoints kind (x :+: y) where gDocumentEntrypoints (ParamBuilder michDesc) clauses = let (lclauses, rclauses) = rsplit @CaseClauseParam @(GCaseClauses x) clauses in gDocumentEntrypoints @kind @x (ParamBuilder $ \a -> michDesc $ "Left (" <> a <> ")") lclauses `rappend` gDocumentEntrypoints @kind @y (ParamBuilder $ \a -> michDesc $ "Right (" <> a <> ")") rclauses instance ( 'CaseClauseParam ctor cf ~ GCaseBranchInput ctor x , KnownSymbol ctor , DocItem (DEntrypoint kind) , DeriveCtorFieldDoc ctor cf ) => GDocumentEntrypoints kind (G.C1 ('G.MetaCons ctor _1 _2) x) where gDocumentEntrypoints michDesc (CaseClauseL clause :& RNil) = let entrypointName = toText $ symbolVal (Proxy @ctor) psteps = mkPbsWrapIn entrypointName michDesc addDoc instr = clarifyParamBuildingSteps psteps $ docGroup (SomeDocItem . DEntrypoint @kind entrypointName) $ doc (deriveCtorFieldDoc @ctor @cf) # instr in CaseClauseL (addDoc clause) :& RNil -- | Like 'case_', to be used for pattern-matching on a parameter -- or its part. -- -- Modifies documentation accordingly. Including description of -- entrypoints' arguments, thus for them you will need to supply -- 'TypeHasDoc' instance. entryCase_ :: forall dt entrypointKind out inp. ( InstrCaseC dt , RMap (CaseClauses dt) , DocumentEntrypoints entrypointKind dt ) => Proxy entrypointKind -> Rec (CaseClauseL inp out) (CaseClauses dt) -> dt & inp :-> out entryCase_ _ = case_ . documentEntrypoints @dt @entrypointKind -- | Version of 'entryCase_' for tuples. entryCase :: forall dt entrypointKind out inp clauses. ( CaseTC dt out inp clauses , DocumentEntrypoints entrypointKind dt ) => Proxy entrypointKind -> IsoRecTuple clauses -> dt & inp :-> out entryCase p = entryCase_ p . recFromTuple -- | Wrapper for documenting single entrypoint which parameter -- isn't going to be unwrapped from some datatype. -- -- @entryCase@ unwraps a datatype, however, sometimes we want to -- have entrypoint parameter to be not wrapped into some datatype. documentEntrypoint :: forall kind epName param s out. ( KnownSymbol epName , DocItem (DEntrypoint kind) , TypeHasDoc param , HasAnnotation param , KnownValue param ) => param & s :-> out -> param & s :-> out documentEntrypoint instr = let entrypointName = toText $ symbolVal (Proxy @epName) in docGroup (SomeDocItem . DEntrypoint @kind entrypointName) $ doc (constructDEpArg @param) # instr -- | Provides arror for convenient entrypoint documentation class EntryArrow kind name body where -- | Lift entrypoint implementation. -- -- Entrypoint names should go with "e" prefix. (#->) :: (Label name, Proxy kind) -> body -> body instance ( name ~ ("e" `AppendSymbol` epName) , body ~ (param & s :-> out) , KnownSymbol epName , DocItem (DEntrypoint kind) , TypeHasDoc param , HasAnnotation param , KnownValue param ) => EntryArrow kind name body where (#->) _ = documentEntrypoint @kind @epName -- | Modify param building steps with respect to entrypoints that given -- parameter declares. -- -- Each contract with entrypoints should eventually call this function, -- otherwise, in case if contract uses built-in entrypoints feature, -- the resulting parameter building steps in the generated documentation -- will not consider entrypoints and thus may be incorrect. -- -- Calling this twice over the same code is also prohibited. finalizeParamCallingDoc :: forall cp inp out. (NiceParameterFull cp, RequireSumType cp, HasCallStack) => (cp : inp :-> out) -> (cp : inp :-> out) finalizeParamCallingDoc = modifyParamBuildingSteps modifySteps where -- We do not actually need it, requiring this constraint only to avoid -- misapplication of our function. _needSumType :: Dict (RequireSumType cp) _needSumType = Dict epDescs :: [Some1 EpCallingDesc] epDescs = -- Reversing the list because if element @e1@ of this list is prefix of -- another element @e2@, we want @e2@ to appear eariler than @e1@ to -- match against it first. But without reverse exactly the opposite -- holds due to [order of entrypoints children] property. reverse $ pepDescsWithDef @cp modifySteps :: [ParamBuildingStep] -> [ParamBuildingStep] modifySteps pbs | areFinalizedParamBuildingSteps pbs = error "Applying finalization second time" | otherwise = fromMaybe [PbsUncallable pbs] . listToMaybe . catMaybes $ epDescs <&> \epDesc -> tryShortcut epDesc pbs -- Further we check whether given 'EpCallingStep's form prefix of -- 'ParamBuildingStep's; if so, we can apply only part of building -- steps and then call the entrypoint directly match :: [EpCallingStep] -> [ParamBuildingStep] -> Bool match cSteps pbSteps = and $ zip cSteps (prolong pbSteps) <&> \case (EpsWrapIn ctor, Just (PbsWrapIn ctor2 _)) | ctor == ctor2 -> True _ -> False where prolong :: [a] -> [Maybe a] prolong l = map Just l ++ repeat Nothing tryShortcut :: Some1 EpCallingDesc -> [ParamBuildingStep] -> Maybe [ParamBuildingStep] tryShortcut (Some1 EpCallingDesc{ epcdSteps = cSteps, epcdEntrypoint = ep }) pbSteps | match cSteps pbSteps = let truncated = drop (length cSteps) pbSteps callEpStep = PbsCallEntrypoint ep in Just $ callEpStep : truncated | otherwise = Nothing -- | Whether 'finalizeParamCallingDoc' has already been applied to these steps. areFinalizedParamBuildingSteps :: [ParamBuildingStep] -> Bool areFinalizedParamBuildingSteps = -- Currently, 'finalizeParamCallingDoc' puts either 'PbsCallEntrypoint' or -- 'PbsUncallable' to list, and only it, and we rely on this behaviour here. -- If something changes so that these heuristics do not work, we can always -- insert special markers which would tell us whether finalization has been -- applied. let hasFinalizationTraces = \case PbsWrapIn{} -> False PbsCallEntrypoint{} -> True PbsCustom{} -> False PbsUncallable{} -> True in any hasFinalizationTraces entryCaseSimple_ :: forall cp out inp. ( InstrCaseC cp , RMap (CaseClauses cp) , DocumentEntrypoints PlainEntrypointsKind cp , NiceParameterFull cp , RequireFlatParamEps cp ) => Rec (CaseClauseL inp out) (CaseClauses cp) -> cp & inp :-> out entryCaseSimple_ = finalizeParamCallingDoc . entryCase_ (Proxy @PlainEntrypointsKind) where _reqFlat = Dict @(RequireFlatEpDerivation cp (GetParameterEpDerivation cp)) -- | Version of 'entryCase' for contracts with flat parameter, use it when you -- need only one 'entryCase' all over the contract implementation. -- -- This method calls 'finalizeParamCallingDoc' inside. entryCaseSimple :: forall cp out inp clauses. ( CaseTC cp out inp clauses , DocumentEntrypoints PlainEntrypointsKind cp , NiceParameterFull cp , RequireFlatParamEps cp ) => IsoRecTuple clauses -> cp & inp :-> out entryCaseSimple = entryCaseSimple_ . recFromTuple type family RequireFlatParamEps cp :: Constraint where RequireFlatParamEps cp = ( RequireFlatEpDerivation cp (GetParameterEpDerivation cp) , RequireSumType cp ) -- Checking this is not strictly necessary, but let's try it type family RequireFlatEpDerivation cp deriv :: Constraint where RequireFlatEpDerivation _ EpdNone = () RequireFlatEpDerivation _ EpdPlain = () RequireFlatEpDerivation cp deriv = TypeError ( 'Text "Parameter is not flat" ':$$: 'Text "For parameter `" ':<>: 'ShowType cp ':<>: 'Text "`" ':$$: 'Text "With entrypoints derivation way `" ':<>: 'ShowType deriv ':<>: 'Text "`" ) --------------------------- -- Helper --------------------------- -- | Surrouned a markdown text in a span tag with given id. mdAddId :: Markdown -> Markdown -> Markdown mdAddId idTxt txt = " idTxt <> "\">" <> txt <> ""