[#180] Add contract documentation tutorial to Indigo website

Problem: The first problem is that documenting entrypoints requires defining
instances of a big typeclass, which is scary-looking for beginners,
especially if they are not Haskell-friendly.

The second problem is that, while there are ways to print and create contracts
from ghci explained in the tutorial, there is no explanation of docs in it or
method to interact with them.

Solution: Add documentation tutorial including the interacting docs via the
REPL.
parent b00c8246
Pipeline #183968598 passed with stages
in 3 minutes and 27 seconds
Unreleased
==========
<!-- Append new entries here -->
* [!533](https://gitlab.com/morley-framework/morley/-/merge_requests/533)
Add a tutorial on how to add documentation to a contract.
+ Create helper functions: `saveDocumentation` and `printDocumentation`
which can generate the documentation via the REPL.
+ Add short-handed doc item statements such as: `anchor`, `description`,
and `example`.
0.2.0
=====
* [!542](https://gitlab.com/morley-framework/morley/-/merge_requests/542)
......
......@@ -4,7 +4,7 @@ cabal-version: 2.2
--
-- see: https://github.com/sol/hpack
--
-- hash: 0f8ae3ebb8ee090fcd5c68052be0c19d4a032f3a866a09d3e5015c8141e1d978
-- hash: cb1f0a5f598f1529a24da5f97bcdf9f4fa1f16b92abf1d5dacd6f1ac567a39b7
name: indigo
version: 0.2.0
......@@ -79,6 +79,7 @@ library
, reflection
, singletons
, vinyl
, with-utf8
mixins:
base hiding (Prelude)
default-language: Haskell2010
......
......@@ -32,6 +32,7 @@ library:
- reflection
- singletons
- vinyl
- with-utf8
tests:
indigo-test:
......
......@@ -74,6 +74,11 @@ module Indigo.Frontend.Language
, contractGeneralDefault
, finalizeParamCallingDoc
-- * Short-handed doc item
, anchor
, description
, example
-- * Side-effects operations
, transferTokens
, setDelegate
......@@ -120,6 +125,7 @@ import qualified Lorentz.Run as L
import qualified Michelson.Typed as MT
import qualified Michelson.Typed.Arith as M
import Michelson.Typed.Haskell.Instr.Sum (CaseClauseParam(..), CtorField(..))
import Util.Markdown (toAnchor)
import Util.TypeLits (AppendSymbol)
import Util.TypeTuple.Class
......@@ -646,6 +652,18 @@ finalizeParamCallingDoc
=> (Var (ExprType param) -> IndigoM x) -> (param -> IndigoM x)
finalizeParamCallingDoc = oneIndigoM ... FinalizeParamCallingDoc
-- | Put a 'DDescription' doc item.
description :: Markdown -> IndigoM ()
description = doc . DDescription
-- | Put a 'DAnchor' doc item.
anchor :: Text -> IndigoM ()
anchor = doc . DAnchor . toAnchor
-- | Put a 'DEntrypointExample' doc item.
example :: forall a. NiceParameter a => a -> IndigoM ()
example = doc . mkDEntrypointExample
----------------------------------------------------------------------------
-- Contract call
----------------------------------------------------------------------------
......
......@@ -38,5 +38,6 @@ import Lorentz.Run as L hiding (Contract(..))
import Lorentz.StoreClass as L
import Lorentz.UParam as L
import Lorentz.UStore as L
import Lorentz.Util.TH as L
import Lorentz.Value as L
import Lorentz.Zip as L ()
......@@ -6,11 +6,16 @@
module Indigo.Print
( printIndigoContract
, renderIndigoDoc
, printAsMichelson
, saveAsMichelson
, printDocumentation
, saveDocumentation
) where
import Data.Text.Lazy.IO.Utf8 (writeFile)
import Indigo.Compilation
import Indigo.Internal.Object
import Indigo.Lorentz
......@@ -30,6 +35,17 @@ printIndigoContract forceSingleLine ctr = printLorentzContract forceSingleLine $
defaultContract $
compileIndigoContract @param @st ctr
-- | Generate an Indigo contract documentation.
renderIndigoDoc
:: forall param st .
( IsObject st
, NiceParameterFull param
)
=> IndigoContract param st
-> LText
renderIndigoDoc ctr =
renderLorentzDocWithGitRev DGitRevisionUnknown $ compileIndigoContract @param @st ctr
-- | Prints the pretty-printed Michelson code of an Indigo contract to
-- the standard output.
--
......@@ -56,4 +72,27 @@ saveAsMichelson
-> FilePath
-> m ()
saveAsMichelson cntr filePath =
withFile filePath WriteMode (`hPutStrLn` (printIndigoContract @param @st False cntr))
writeFile filePath (printIndigoContract @param @st False cntr)
-- | Print the generated documentation to the standard output.
printDocumentation
:: forall param st m . ( IsObject st
, NiceParameterFull param
, MonadIO m
)
=> IndigoContract param st
-> m ()
printDocumentation ctr =
putStrLn $ renderIndigoDoc @param @st ctr
-- | Save the generated documentation to the given file.
saveDocumentation
:: forall param st m . ( IsObject st
, NiceParameterFull param
, MonadIO m, MonadMask m
)
=> IndigoContract param st
-> FilePath
-> m ()
saveDocumentation ctr filePath = do
writeFile filePath (renderIndigoDoc @param @st ctr)
<!--
SPDX-FileCopyrightText: 2020 Tocqueville Group
SPDX-License-Identifier: LicenseRef-MIT-TQ
-->
# Add documentation to the contract
In this chapter, we will show how to add documentation to our smart contract
directly in the code.
We will use a contract that resembles the one in section [Functions and Procedures](functions.md).
You can load the file below into the REPL to follow along with the tutorial.
## Contract Name and Description
First let's get started with something simple, we can specify the contract name and description by
using `contractName` and `description` like below:
{!haskell 30-35 src/Indigo/Tutorial/ContractDocumentation/FunctionsWithDocs.hs!}
You can generate the documentation of this contract by using:
```haskell
saveDocumentation @Parameter @Storage myContract "my-documentation.md"
```
!!! note "Note"
You can also see the documentation immediately in the REPL using:
```
printDocumentation @Parameter @Storage myContract
```
If there is no error, you will be able to see a file named `my-documentation.md` .
We can see that, at the top of the documentation, there are the name and description of our contract.
There is also Table of Contents which is generated automatically for us.
{!markdown 7-18 src/Indigo/Tutorial/ContractDocumentation/documentation.md!}
Beside `contractName` and `description`, there are also other doc items that we could use:
- `anchor <link>`: Create custom anchor in the generated documentation.
- `example <value>`: Use inside an entrypoint to specify an example value for that entrypoint.
There are also many other things that are generated automatically for us, including:
`Entrypoints` and `Definitions`.
## Entrypoints' documentation
We can use `description` and `example` as described above to add some information about
our entrypoints in the documentation.
{!haskell 41-55 src/Indigo/Tutorial/ContractDocumentation/FunctionsWithDocs.hs!}
{!haskell 57-73 src/Indigo/Tutorial/ContractDocumentation/FunctionsWithDocs.hs!}
Now we should have description that looks like this:
{!markdown 68-70 src/Indigo/Tutorial/ContractDocumentation/documentation.md!}
{!markdown 96-98 src/Indigo/Tutorial/ContractDocumentation/documentation.md!}
!!! note "Note"
There are a lot of other useful things that are generated as well, such as:
<br> - __How to call the entrypoint__: Instruction on how to call the entrypoint
<br> - __Type__: Haskell and Michelson Types.
<br> - __Example__: Example value for Michelson
<br>&nbsp;&nbsp;- We can modify this value with `example` as described above
## Custom error messages
When handling an entrypoint's input, there are times when the user gives an unexpected value.
In this case, we would like to fail with a nice error message.
This is where custom error messages come into play.
We can define custom error messages like below:
{!haskell 89-89 src/Indigo/Tutorial/ContractDocumentation/FunctionsWithDocs.hs!}
As you can see, there are 3 parts to defining a custom error message:
- `"isNotZeroError"` is the name of the error.
- `exception` is the type of the error. There are 4 types of error
you can specify:
- `exception`: Normal expected error. Examples: "insufficient balance", "wallet does not exist".
- `bad-argument`: Invalid argument passed to entrypoint. Examples: your entrypoint accepts an enum represented as `nat`, and unknown value is provided.
- `contract-internal`: Unexpected error. Most likely it means that there is a bug in the contract or the contract has been deployed incorrectly.
- `unknown`: When the error type is beside the other 3.
- ``"Fail when the input to `IsZero` entrypoint is not zero."`` is the description of the error.
This is how you would use your custom error message.
{!haskell 55-55 src/Indigo/Tutorial/ContractDocumentation/FunctionsWithDocs.hs!}
The error message entry will be generated like below:
{!markdown 236-243 src/Indigo/Tutorial/ContractDocumentation/documentation.md!}
## Generate documentations for storage
Let's say that we have a storage type like this:
{!haskell 21-26 src/Indigo/Tutorial/ContractDocumentation/FunctionsWithDocs.hs!}
We may want to document this type in our documentation. Here's how we can do it:
{!haskell 28-28 src/Indigo/Tutorial/ContractDocumentation/FunctionsWithDocs.hs!}
We also need to add `docStorage` in the contract code as well:
{!haskell 30-39 src/Indigo/Tutorial/ContractDocumentation/FunctionsWithDocs.hs!}
This will generate the doc entry like below:
{!markdown 50-58 src/Indigo/Tutorial/ContractDocumentation/documentation.md!}
Your final code should look like this:
{!haskell 6-* src/Indigo/Tutorial/ContractDocumentation/FunctionsWithDocs.hs!}
......@@ -82,7 +82,7 @@ for failing:
- `failCustom` that expects a `Label` and an error message to compose a custom
failure value
- `failCustom_` that has only one `Label` argument, and is a specialization of
the previous one
the previous one. See an example on how to use this in: [Custom error messages](contract-docs.md#custom-error-messages)
- `failUnexpected_` that has one argument: an expression for an error message,
just like `assert`
- `assertCustom` that works just like `assert`, but with the same inputs as
......
......@@ -4,7 +4,7 @@ cabal-version: 2.2
--
-- see: https://github.com/sol/hpack
--
-- hash: 15ee20c146c6151652dd48bbad911623b446d39950940c768e84c649e95bef23
-- hash: e31c4b7e010f70091ec71cb704d7cb0489c810ccd340d170ca0e87f22f14cd27
name: indigo-tutorial
version: 0.1.0.0
......@@ -29,6 +29,7 @@ source-repository head
library
exposed-modules:
Indigo.Tutorial.Basics.Example
Indigo.Tutorial.ContractDocumentation.FunctionsWithDocs
Indigo.Tutorial.Expressions.Math
Indigo.Tutorial.Functions.Functions
Indigo.Tutorial.GettingStarted.Example
......
......@@ -41,6 +41,7 @@ nav:
- Imperative statements: statements.md
- Functions and Procedures: functions.md
- Side Effects and Errors: side-effects.md
- Contract documentation: contract-docs.md
- References:
- Types: types.md
- Expressions: operators.md
-- SPDX-FileCopyrightText: 2020 Tocqueville Group
--
-- SPDX-License-Identifier: LicenseRef-MIT-TQ
{-# OPTIONS -Wno-orphans #-}
module Indigo.Tutorial.ContractDocumentation.FunctionsWithDocs
( myContract
) where
import Indigo
data Parameter
= IsZero Integer
| HasDigitOne Natural
deriving stock (Generic, Show)
deriving anyclass (IsoValue)
[entrypointDoc| Parameter plain |]
data Storage = Storage
{ sLastInput :: Natural
, sTotal :: Natural
}
deriving stock (Generic, Show)
deriving anyclass (IsoValue, HasAnnotation)
[typeDoc| Storage "Contract storage description."|]
myContract :: IndigoContract Parameter Storage
myContract param = defContract $ contractName "My Contract" do
contractGeneralDefault
description
"This documentation describes an example on how to functions and \
\procedures of Indigo."
docStorage @Storage
entryCaseSimple param
( #cIsZero #= checkZero
, #cHasDigitOne #= checkHasDigitOne
)
checkZero
:: forall tp.
( tp :~> Integer
, HasStorage Storage
)
=> IndigoEntrypoint tp
checkZero val = do
description "Increment storage by 1 if the input is zero, otherwise fail."
example (0 int)
result <- new$ (val == 0 int)
if result then
incrementStorage
else
failCustom_ @() #isNotZeroError
checkHasDigitOne
:: forall tp.
( tp :~> Natural
, HasStorage Storage
)
=> IndigoEntrypoint tp
checkHasDigitOne val = do
description "Increment storage by 1 if the input has one digit."
example (1 int)
currentVal <- new$ val
setStorageField @Storage #sLastInput currentVal
base <- new$ 10 nat
checkRes <- new$ False
while (val > base && checkRes == False) do
currentVal =: currentVal / base
remainder <- new$ val % base
checkRes =: remainder == 1 nat
updateStorage checkRes
updateStorage :: HasStorage Storage => Var Bool -> IndigoProcedure
updateStorage result = defFunction do
if result then
setStorageField @Storage #sTotal $ 0 nat
else
incrementStorage
incrementStorage :: HasStorage Storage => IndigoProcedure
incrementStorage = defFunction do
currentTotal <- getStorageField @Storage #sTotal
setStorageField @Storage #sTotal (currentTotal + (1 nat))
-- | Custom error which can be used via: @failCustom_ #[email protected]
[errorDoc| "isNotZeroError" exception "Fail when the input to `IsZero` entrypoint is not zero."|]
<!--
SPDX-FileCopyrightText: 2020 Tocqueville Group
SPDX-License-Identifier: LicenseRef-MIT-TQ
-->
# My Contract
This documentation describes an example on how to functions and procedures of Indigo.
## Table of contents
- [Storage](#storage)
- [Storage](#storage-Storage)
- [Entrypoints](#entrypoints)
- [isZero](#entrypoints-isZero)
- [hasDigitOne](#entrypoints-hasDigitOne)
**[Definitions](#definitions)**
- [My Contract](#my-contract)
- [Table of contents](#table-of-contents)
- [Storage](#storage)
- [`Storage`](#storage-1)
- [Entrypoints](#entrypoints)
- [`isZero`](#iszero)
- [`hasDigitOne`](#hasdigitone)
- [Definitions](#definitions)
- [Types](#types)
- [`()`](#)
- [`Integer`](#integer)
- [`Natural`](#natural)
- [`Text`](#text)
- [Errors](#errors)
- [`InternalError`](#internalerror)
- [`IsNotZeroError`](#isnotzeroerror)
## Storage
<a name="storage-Storage"></a>
---
### `Storage`
Contract storage description.
**Structure:**
* ***lastInput*** :[`Natural`](#types-Natural)
* ***total*** :[`Natural`](#types-Natural)
**Final Michelson representation:** `pair nat nat`
## Entrypoints
<a name="entrypoints-isZero"></a>
---
### `isZero`
Increment storage by 1 if the input is zero, otherwise fail.
**Argument:**
+ **In Haskell:** [`Integer`](#types-Integer)
+ **In Michelson:** `int`
+ **Example:** <span id="example-id">`0`</span>
<details>
<summary><b>How to call this entrypoint</b></summary>
0. Construct an argument for the entrypoint.
1. Call contract's `isZero` entrypoint passing the constructed argument.
</details>
<p>
**Possible errors:**
* [`IsNotZeroError`](#errors-IsNotZeroError) — Fail when the input to `IsZero` entrypoint is not zero.
<a name="entrypoints-hasDigitOne"></a>
---
### `hasDigitOne`
Increment storage by 1 if the input has one digit.
**Argument:**
+ **In Haskell:** [`Natural`](#types-Natural)
+ **In Michelson:** `nat`
+ **Example:** <span id="example-id">`1`</span>
<details>
<summary><b>How to call this entrypoint</b></summary>
0. Construct an argument for the entrypoint.
1. Call contract's `hasDigitOne` entrypoint passing the constructed argument.
</details>
<p>
# Definitions
## Types
<a name="types-lparenrparen"></a>
---
### `()`
Unit primitive.
**Structure:** ()
**Final Michelson representation:** `unit`
<a name="types-Integer"></a>
---
### `Integer`
Signed number.
**Final Michelson representation:** `int`
<a name="types-Natural"></a>
---
### `Natural`
Unsigned number.
**Final Michelson representation:** `nat`
<a name="types-Text"></a>
---
### `Text`
Michelson string.
This has to contain only ASCII characters with codes from [32; 126] range; additionally, newline feed character is allowed.
**Final Michelson representation:** `string`
## Errors
Our contract implies the possibility of error scenarios, this section enlists
all values which the contract can produce via calling `FAILWITH` instruction
on them. In case of error, no changes to contract state will be applied.
Each entrypoint also contains a list of errors which can be raised during its
execution; only for no-throw entrypoints this list will be omitted.
Errors in these lists are placed in the order in which the corresponding
properties are checked unless the opposite is specified. I.e., if for a
given entrypoint call two different errors may take place, the one which
appears in the list first will be thrown.
Most of the errors are represented according to the same
`(error tag, error argument)` pattern. See the list of errors below
for details.
We distinquish several error classes:
+ **Action exception**: given action cannot be performed with
regard to the current contract state.