Skip to content

Commit

Permalink
[DPP-592] Generate docs for self-service error codes. (#11129)
Browse files Browse the repository at this point in the history
Adding `Daml Documentation >> Building applications >> "Self-Service Error Codes (Experimental)` section in the HTML documentation. 

The section is populated automatically with error code information retrieved from the classpath.
The process of generating documentation for error codes looks like this:
- first we find error codes information from the classpath,
- then we save it to a json file,
- then the json file is made available to a custom Sphinx extension,
- then the custom Sphinx extension generates documentation wherever we put a new custom Sphinx directive.

Try it out with
`./docs/scripts/live-preview.sh`
or
`./docs/scripts/preview.sh`


CHANGELOG_BEGIN
CHANGELOG_END
  • Loading branch information
pbatko-da authored Oct 20, 2021
1 parent 8ff347d commit d3dad75
Show file tree
Hide file tree
Showing 13 changed files with 367 additions and 16 deletions.
27 changes: 24 additions & 3 deletions docs/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ exports_files(
"source/tools/export/output-root/Export.daml",
"source/tools/export/output-root/args.json",
"source/tools/export/output-root/daml.yaml",
"sphinx_ext/self_service_error_codes_extension.py",
],
)

Expand Down Expand Up @@ -198,13 +199,15 @@ genrule(
genrule(
name = "docs-no-pdf",
srcs = glob([
"sphinx_ext/**",
"configs/html/**",
"configs/static/pygments_daml_lexer.py",
"configs/static/typescript.py",
]) + [
":sources",
":theme",
":hoogle_db.tar.gz",
"//docs:generate-error-codes-json",
"//language-support/java:javadoc",
"//language-support/ts/daml-react:docs",
"//language-support/ts/daml-ledger:docs",
Expand All @@ -215,6 +218,7 @@ genrule(
"@daml-cheat-sheet//:site",
":scripts/check-closing-quotes.sh",
":scripts/check-closing-quotes.sh.allow",
"//docs:error_codes_export.json",
],
outs = ["html-only.tar.gz"],
cmd = ("""
Expand Down Expand Up @@ -265,9 +269,16 @@ genrule(
# Unfortunately, an update is not so easy because Sphinx 2.3.1 breaks
# the PDF documentation due to issues with the FreeSerif font in the
# fontspec package. So, for now we ignore `FutureWarning`.
WARNINGS=$$(../$(location @sphinx_nix//:bin/sphinx-build) -c docs/configs/html docs/source html 2>&1 | \\
grep -Pi "(?<!future)warning:" || true)
SPHINX_BUILD_EXIT_CODE=0
SPHINX_BUILD_OUTPUT=$$(../$(location @sphinx_nix//:bin/sphinx-build) -D error_codes_json_export=../$(location //docs:error_codes_export.json) -c docs/configs/html docs/source html 2>&1) || SPHINX_BUILD_EXIT_CODE=$$?
if [ "$$SPHINX_BUILD_EXIT_CODE" -ne 0 ]; then
>&2 echo "## SPHINX-BUILD OUTPUT:"
>&2 echo "$$SPHINX_BUILD_OUTPUT"
>&2 echo "## SPHINX-BUILD OUTPUT END"
exit 1
fi
# NOTE: appending ' || true' to force exit code of 0 from grep because grep normally exits with 1 if no lines are selected:
WARNINGS=$$(echo "$$SPHINX_BUILD_OUTPUT" | grep -Pi "(?<!future)warning:" || true)
if [ "$$WARNINGS" != "" ]; then
echo "$$WARNINGS"
exit 1
Expand Down Expand Up @@ -303,6 +314,7 @@ genrule(
tools = [
"@sphinx_nix//:bin/sphinx-build",
"//bazel_tools/sh:mktgz",
"//docs:generate-error-codes-json",
] + (["@glibc_locales//:locale-archive"] if is_linux else []),
) if not is_windows else None

Expand Down Expand Up @@ -644,6 +656,15 @@ pkg_tar(
visibility = ["//visibility:public"],
)

genrule(
name = "generate-error-codes-json",
srcs = [],
outs = ["error_codes_export.json"],
cmd = "$(location //ledger/error:export-error-codes-json-app) $(location error_codes_export.json)",
tools = ["//ledger/error:export-error-codes-json-app"],
visibility = ["//visibility:public"],
)

exports_files([
"source/daml-script/template-root/src/ScriptExample.daml",
])
5 changes: 3 additions & 2 deletions docs/configs/html/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import sys
import glob
sys.path.insert(0, os.path.abspath('../static'))

sys.path.insert(0, os.path.abspath('../../sphinx_ext'))
# -- General configuration ------------------------------------------------

# If your documentation needs a minimal Sphinx version, state it here.
Expand All @@ -35,7 +35,8 @@
# ones.
extensions = [
'sphinx.ext.extlinks',
'sphinx_copybutton'
'sphinx_copybutton',
'self_service_error_codes_extension',
]

# Add any paths that contain templates here, relative to this directory.
Expand Down
5 changes: 4 additions & 1 deletion docs/scripts/live-preview.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ TEMPLATES_DIR=$BUILD_DIR/source/_templates
mkdir -p $TEMPLATES_DIR
tar -zxf $BAZEL_BIN/templates/templates-tarball.tar.gz -C $TEMPLATES_DIR --strip-components=1

# Error codes: create JSON file with error codes information
bazel build //docs:generate-error-codes-json

for arg in "$@"
do
if [ "$arg" = "--pdf" ]; then
Expand Down Expand Up @@ -85,4 +88,4 @@ DATE=$(date +"%Y-%m-%d")
echo { \"$DATE\" : \"$DATE\" } > $BUILD_DIR/gen/versions.json

pipenv install
pipenv run sphinx-autobuild -c $BUILD_DIR/configs/html $BUILD_DIR/source $BUILD_DIR/gen
pipenv run sphinx-autobuild -D error_codes_json_export=../../bazel-bin/docs/error_codes_export.json -c $BUILD_DIR/configs/html $BUILD_DIR/source $BUILD_DIR/gen
10 changes: 10 additions & 0 deletions docs/source/error-codes/self-service/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.. Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
.. SPDX-License-Identifier: Apache-2.0
Self-Service Error Codes (Experimental)
#######################################

.. toctree::
:hidden:

.. list-all-error-codes::
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ Daml Documentation
triggers/index
tools/trigger-service/index
tools/auth-middleware/index
error-codes/self-service/index

.. toctree::
:titlesonly:
Expand Down
199 changes: 199 additions & 0 deletions docs/sphinx_ext/self_service_error_codes_extension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

from collections import defaultdict
from docutils import nodes
from docutils.parsers.rst import Directive
from sphinx.writers.html import HTMLTranslator
from typing import Dict, Any

import json

error_codes_data = {}

CONFIG_OPT = 'error_codes_json_export'

def load_data(app, config):
global error_codes_data

if CONFIG_OPT in config:
file_name = config[CONFIG_OPT]
try:
with open(file_name) as f:
tmp = json.load(f)
error_codes_data = {error["code"]: error for error in tmp["errorCodes"]}
except EnvironmentError:
print(f"Failed to open file: '{file_name}'")
raise


def setup(app):
app.add_config_value(CONFIG_OPT, '', 'env')
app.connect('config-inited', load_data)
app.add_node(error_code_node)
app.add_directive('list-all-error-codes', ListAllErrorCodesDirective)
# Callback functions for populating error code section after the doctree is resolved
app.connect('doctree-resolved', process_error_code_nodes)
# Overwriting standard Sphinx translator to allow linking of return types
app.set_translator('html', PatchedHTMLTranslator)


class error_code_node(nodes.General, nodes.Element):
def __init__(self, codes, expl="", res=""):
nodes.Element.__init__(self)
print("found error node codes: %s" % codes)
self.codes = codes
self.expl = expl
self.res = res


class ListAllErrorCodesDirective(Directive):
has_contents = True
required_arguments = 0
final_argument_whitespace = True
def run(self):
return [error_code_node([], None, None)]


def text_node(n, txt):
# Doubling the parameter, as TextElements want the raw text and the text
return n(txt, txt)


def process_error_code_nodes(app, doctree, fromDocName):
def build_indented_bold_and_non_bold_node(bold_text: str, non_bold_text: str):
bold = text_node(n=nodes.strong, txt=bold_text)
non_bold = text_node(n=nodes.inline, txt=non_bold_text)
both = nodes.definition('', bold)
both += non_bold
return both

def item_to_node(item: Dict[str, Any]) -> nodes.definition_list_item:
node = nodes.definition_list_item()
term_node = text_node(nodes.term, "%s" % (item["code"]))
definition_node = nodes.definition('', text_node(nodes.paragraph, ''))
if item["explanation"]:
definition_node += build_indented_bold_and_non_bold_node(
bold_text="Explanation: ",
non_bold_text=item['explanation'])
definition_node += build_indented_bold_and_non_bold_node(
bold_text="Category: ",
non_bold_text=item['category'])
if item["conveyance"]:
definition_node += build_indented_bold_and_non_bold_node(
bold_text="Conveyance: ",
non_bold_text=item['conveyance'])
if item["resolution"]:
definition_node += build_indented_bold_and_non_bold_node(
bold_text="Resolution: ",
non_bold_text=item['resolution'])
permalink_node = build_permalink(
app=app,
fromDocName=fromDocName,
term=item["code"],
# NOTE: This is the path to the docs file in Sphinx's source dir
docname='error-codes/self-service/index',
node_to_permalink_to=term_node)
node += [permalink_node, definition_node]

return node

# A node of this tree is a dict that can contain
# 1. further nodes and/or
# 2. 'leaves' in the form of a list of error (code) data
# Thus, the resulting tree is very similar to a trie
def build_hierarchical_tree_of_error_data(data) -> defaultdict:
create_node = lambda: defaultdict(create_node)
root = defaultdict(create_node)
for error_data in data:
current = root
for group in error_data["hierarchicalGrouping"]:
current = current[group]
if 'error-codes' in current:
current['error-codes'].append(error_data)
else:
current['error-codes'] = [error_data]
return root

# DFS to traverse the error code data tree from `build_hierarchical_tree_of_error_data`
# While traversing the tree, the presentation of the error codes on the documentation is built
def dfs(tree, node, prefix: str) -> None:
if 'error-codes' in tree:
dlist = nodes.definition_list()
for code in tree['error-codes']:
dlist += item_to_node(item=code)
node += dlist
i = 1
for subtopic, subtree in tree.items():
if subtopic == 'error-codes':
continue
subprefix = f"{prefix}{i}."
i += 1
subtree_node = text_node(n=nodes.rubric, txt = subprefix + " " + subtopic)
dfs(tree=subtree, node=subtree_node, prefix=subprefix)
node += subtree_node

for node in doctree.traverse(error_code_node):
# Valid error codes given to the .. error-codes:: directive as argument
# given_error_codes = [error_codes_data[code] for code in node.codes if code in error_codes_data]
# Code for manually overwriting the explanation or resolution of an error code

section = nodes.section()
root = nodes.rubric(rawsource = "", text = "")
section += root
tree = build_hierarchical_tree_of_error_data(data=error_codes_data.values())
dfs(tree=tree, node=root, prefix="")
node.replace_self(new=[section])


# Build a permalink/anchor to a specific command/metric
def build_permalink(app, fromDocName, term, docname, node_to_permalink_to):
reference_node = nodes.reference('', '')
reference_node['refuri'] = app.builder.get_relative_uri(fromDocName, docname) + '#' + term

reference_node += node_to_permalink_to

target_node = nodes.target('', '', ids=[term])
node_to_permalink_to += target_node
return reference_node


class PatchedHTMLTranslator(HTMLTranslator):
# We overwrite this method as otherwise an assertion fails whenever we create a reference whose parent is
# not a TextElement. Concretely, this enables using method `build_return_type_node` for creating links from
# return types to the appropriate scaladocs
# Similar to solution from https://stackoverflow.com/a/61669375

def visit_reference(self, node):
# type: (nodes.Node) -> None
atts = {'class': 'reference'}
if node.get('internal') or 'refuri' not in node:
atts['class'] += ' internal'
else:
atts['class'] += ' external'
if 'refuri' in node:
atts['href'] = node['refuri'] or '#'
if self.settings.cloak_email_addresses and \
atts['href'].startswith('mailto:'):
atts['href'] = self.cloak_mailto(atts['href'])
self.in_mailto = 1
else:
assert 'refid' in node, \
'References must have "refuri" or "refid" attribute.'
atts['href'] = '#' + node['refid']
if not isinstance(node.parent, nodes.TextElement):
# ---------------------
# Commenting out this assertion is the only change compared to Sphinx version 3.4.3
# assert len(node) == 1 and isinstance(node[0], nodes.image)
# ---------------------
atts['class'] += ' image-reference'
if 'reftitle' in node:
atts['title'] = node['reftitle']
if 'target' in node:
atts['target'] = node['target']
self.body.append(self.starttag(node, 'a', '', **atts))

if node.get('secnumber'):
self.body.append(('%s' + self.secnumber_suffix) %
'.'.join(map(str, node['secnumber'])))

19 changes: 15 additions & 4 deletions ledger/daml-on-sql/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,14 @@ genrule(
srcs = [
"README.rst",
"//docs:theme",
"//docs:sphinx_ext/self_service_error_codes_extension.py",
"//docs:configs/html/conf.py",
"//docs:configs/static/pygments_daml_lexer.py",
"//docs:configs/static/typescript.py",
"//docs:scripts/check-closing-quotes.sh",
"//docs:scripts/check-closing-quotes.sh.allow",
"//docs:generate-error-codes-json",
"//docs:error_codes_export.json",
],
outs = ["html.tar.gz"],
cmd = """
Expand All @@ -139,6 +142,8 @@ genrule(
mkdir -p build/docs/configs/html
cp $(location //docs:configs/html/conf.py) build/docs/configs/html/conf.py
mkdir -p build/docs/sphinx_ext
cp $(location //docs:sphinx_ext/self_service_error_codes_extension.py) build/docs/sphinx_ext/self_service_error_codes_extension.py
mkdir -p build/docs/configs/static
cp $(location //docs:configs/static/pygments_daml_lexer.py) build/docs/configs/static/pygments_daml_lexer.py
cp $(location //docs:configs/static/typescript.py) build/docs/configs/static/typescript.py
Expand Down Expand Up @@ -169,14 +174,20 @@ genrule(
# Unfortunately, an update is not so easy because Sphinx 2.3.1 breaks
# the PDF documentation due to issues with the FreeSerif font in the
# fontspec package. So, for now we ignore `FutureWarning`.
WARNINGS=$$(../$(location @sphinx_nix//:bin/sphinx-build) -c docs/configs/html docs/source html 2>&1 | \\
grep -Pi "(?<!future)warning:" || true)
SPHINX_BUILD_EXIT_CODE=0
SPHINX_BUILD_OUTPUT=$$(../$(location @sphinx_nix//:bin/sphinx-build) -D error_codes_json_export=../$(location //docs:error_codes_export.json) -c docs/configs/html docs/source html 2>&1) || SPHINX_BUILD_EXIT_CODE=$$?
if [ "$$SPHINX_BUILD_EXIT_CODE" -ne 0 ]; then
>&2 echo "## SPHINX-BUILD OUTPUT:"
>&2 echo "$$SPHINX_BUILD_OUTPUT"
>&2 echo "## SPHINX-BUILD OUTPUT END"
exit 1
fi
# NOTE: appending ' || true' to force exit code of 0 from grep because grep normally exits with 1 if no lines are selected:
WARNINGS=$$(echo "$$SPHINX_BUILD_OUTPUT" | grep -Pi "(?<!future)warning:" || true)
if [ "$$WARNINGS" != "" ]; then
echo "$$WARNINGS"
exit 1
fi
../$(execpath //bazel_tools/sh:mktgz) ../$@ html
""".format(sdk = sdk_version),
tools = [
Expand Down
Loading

0 comments on commit d3dad75

Please sign in to comment.