[DPP-673][Self-service error-codes] Generate error code directory compatible with both pdf and html docs (#11794)

CHANGELOG_BEGIN
CHANGELOG_END
This commit is contained in:
pbatko-da 2021-11-23 15:30:48 +01:00 committed by GitHub
parent 5a2c070da4
commit 55191847cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 310 additions and 286 deletions

View File

@ -18,7 +18,6 @@ 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",
],
)
@ -100,11 +99,14 @@ genrule(
"//ledger-api/grpc-definitions:docs",
"//:LICENSE",
"//:NOTICES",
":generate-docs-error-code-inventory-into-rst-file",
],
outs = ["source.tar.gz"],
cmd = """
cp -rL docs/source source
mkdir -p -- source/error-codes/self-service
cp -- $(location //docs:generate-docs-error-code-inventory-into-rst-file) source/app-dev/grpc/error_codes_inventory.rst
# Copy in Stdlib
mkdir -p source/daml/stdlib
tar xf $(location //compiler/damlc:daml-base-rst.tar.gz) \\
@ -200,7 +202,6 @@ genrule(
genrule(
name = "docs-no-pdf",
srcs = glob([
"sphinx_ext/**",
"configs/html/**",
"configs/static/pygments_daml_lexer.py",
"configs/static/typescript.py",
@ -208,7 +209,6 @@ genrule(
":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",
@ -219,7 +219,6 @@ 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 = ("""
@ -271,7 +270,7 @@ genrule(
# the PDF documentation due to issues with the FreeSerif font in the
# fontspec package. So, for now we ignore `FutureWarning`.
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=$$?
SPHINX_BUILD_OUTPUT=$$(../$(location @sphinx_nix//:bin/sphinx-build) -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"
@ -315,7 +314,6 @@ 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
@ -666,6 +664,15 @@ genrule(
visibility = ["//visibility:public"],
)
genrule(
name = "generate-docs-error-code-inventory-into-rst-file",
srcs = [],
outs = ["error_codes_inventory.rst"],
cmd = "$(location //ledger/error/generator:generate-docs-error-code-inventory-app) $(location error_codes_inventory.rst)",
tools = ["//ledger/error/generator:generate-docs-error-code-inventory-app"],
visibility = ["//visibility:public"],
)
exports_files([
"source/daml-script/template-root/src/ScriptExample.daml",
])

View File

@ -36,7 +36,6 @@ sys.path.insert(0, os.path.abspath('../../sphinx_ext'))
extensions = [
'sphinx.ext.extlinks',
'sphinx_copybutton',
'self_service_error_codes_extension',
]
# Add any paths that contain templates here, relative to this directory.

View File

@ -52,9 +52,6 @@ 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
@ -63,6 +60,10 @@ do
cp -L ../../bazel-bin/docs/DigitalAssetSDK.pdf $BUILD_DIR/gen/_downloads
fi
if [ "$arg" = "--gen" ]; then
bazel build //docs:generate-docs-error-code-inventory-into-rst-file
cp -L ../../bazel-bin/docs/error_codes_inventory.rst $BUILD_DIR/source/app-dev/grpc/error_codes_inventory.rst
# Hoogle
bazel build //compiler/damlc:daml-base-hoogle.txt
mkdir -p $BUILD_DIR/gen/hoogle_db

View File

@ -353,11 +353,14 @@ but there is no guarantee given that additional information will be preserved ac
Error Codes Inventory
**********************
.. list-all-error-codes::
.. This file is generated:
.. include:: error_codes_inventory.rst
Error Codes Migration Guide
---------------------------
*****************************
The Ledger API gRPC error codes change introduced in the Daml SDK 1.18 release involves breaking
compatibility with previous releases for some service Ledger API endpoints.
@ -366,7 +369,7 @@ The table below outlines all the cases and error conditions when a Ledger API se
gRPC status code in comparison to the pre-1.18 releases.
Common Ledger API changes
^^^^^^^^^^^^^^^^^^^^^^^^^
---------------------------
The table below outlines generic gRPC status code changes pertaining to the Ledger API
and apply to all ledger backends. For changes specific to a ledger backend, check the next subsections.
@ -517,7 +520,7 @@ and apply to all ledger backends. For changes specific to a ledger backend, chec
+-----------------------------------------------+-----------------------------------+----------------------------------+-------------------------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------+
Sandbox (classic)
^^^^^^^^^^^^^^^^^
---------------------------
The following gRPC status codes have changed for submission rejections in Sandbox classic.
@ -543,7 +546,7 @@ The following gRPC status codes have changed for submission rejections in Sandbo
Daml Sandbox and VMBC
^^^^^^^^^^^^^^^^^^^^^
---------------------------
The following gRPC status codes have changed for submission rejections in the Ledger API backed by KV-based ledgers (Daml Sandbox and VMBC).

View File

@ -7,11 +7,6 @@ gRPC
####
.. toctree::
:hidden:
error-codes
If you want to write an application for the ledger API in other languages, you'll need to use `gRPC <https://grpc.io>`__ directly.
If you're not familiar with gRPC and protobuf, we strongly recommend following the `gRPC quickstart <https://grpc.io/docs/quickstart/>`__ and `gRPC tutorials <https://grpc.io/docs/tutorials/>`__. This documentation is written assuming you already have an understanding of gRPC.
@ -100,3 +95,9 @@ INTERNAL, UNKNOWN (when returned by the Command Service)
Aside from the standard gRPC status codes, the failures returned by the Ledger API are enriched with details meant to help the application
or the application developer to handle the error autonomously (e.g. by retrying on a retryable error).
For more details on the rich error details see the :doc:`error-codes`
.. toctree::
:hidden:
error-codes

View File

@ -1,216 +0,0 @@
# 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 = {}
group_data = {}
CONFIG_OPT = 'error_codes_json_export'
def load_data(app, config):
global error_codes_data
global group_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"]}
group_data = {group["className"]: group for group in tmp["groups"]}
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["deprecation"]:
definition_node += build_indented_bold_and_non_bold_node(
bold_text="Deprecated: ",
non_bold_text=item['deprecation'])
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
def group_explanation_to_node(text: str) -> nodes.definition_list_item:
return text_node(nodes.paragraph, text)
# 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 grouping in error_data['hierarchicalGrouping']:
current = current[grouping['docName']]
current['explanation'] = group_data[grouping['className']]['explanation']
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, numeric_prefix: str, topic_prefix: str) -> None:
if 'explanation' in tree and tree['explanation']:
node += group_explanation_to_node(tree['explanation'])
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 in ['error-codes', 'explanation']:
continue
subtree_node_numeric_prefix = f"{numeric_prefix}{i}."
i += 1
topic = subtopic
if topic_prefix != "":
topic = topic_prefix + " / " + subtopic
subtree_node_header = subtree_node_numeric_prefix + " " + topic
subtree_node = text_node(n=nodes.rubric, txt = subtree_node_header)
dfs(tree=subtree, node=subtree_node, numeric_prefix=subtree_node_numeric_prefix, topic_prefix=topic)
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, numeric_prefix="", topic_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'])))

View File

@ -126,14 +126,11 @@ 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 = """
@ -142,8 +139,6 @@ 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
@ -175,7 +170,7 @@ genrule(
# the PDF documentation due to issues with the FreeSerif font in the
# fontspec package. So, for now we ignore `FutureWarning`.
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=$$?
SPHINX_BUILD_OUTPUT=$$(../$(location @sphinx_nix//:bin/sphinx-build) -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"

View File

@ -29,6 +29,26 @@ da_scala_binary(
],
)
da_scala_binary(
name = "generate-docs-error-code-inventory-app",
srcs = glob(["app/src/main/scala/**/*.scala"]),
main_class = "com.daml.error.generator.app.ErrorCodeInventoryDocsGenApp",
resources = glob(["src/main/resources/**/*"]),
scala_deps = [
"@maven//:io_circe_circe_core",
"@maven//:org_typelevel_cats_core",
],
visibility = ["//visibility:public"],
runtime_deps = [
# Add the KVErrors to the classpath so they can be picked up by the generator
"//ledger/participant-state/kvutils",
],
deps = [
"//ledger/error",
"//ledger/error/generator:lib",
],
)
da_scala_library(
name = "lib",
srcs = glob(["lib/src/main/scala/**/*.scala"]),

View File

@ -0,0 +1,204 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.error.generator.app
import java.nio.file.{Files, Paths, StandardOpenOption}
import com.daml.error.{ErrorClass, Grouping}
import com.daml.error.generator.{ErrorCodeDocumentationGenerator, ErrorDocItem, GroupDocItem}
import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer
/** Generates error codes inventory as a reStructuredText
*/
object ErrorCodeInventoryDocsGenApp {
def main(args: Array[String]): Unit = {
val text = {
val (errorDocItems, groupDocItems): (Seq[ErrorDocItem], Seq[GroupDocItem]) =
new ErrorCodeDocumentationGenerator().getDocItems
val groupSegmentsToExplanationMap: Map[List[Grouping], Option[String]] =
groupDocItems.map { groupDocItem: GroupDocItem =>
groupDocItem.errorClass.groupings -> groupDocItem.explanation.map(_.explanation)
}.toMap
val errorCodes: Seq[ErrorCodeValue] = errorDocItems.map { (errorDocItem: ErrorDocItem) =>
ErrorCodeValue(
category = errorDocItem.category,
errorGroupPath = errorDocItem.hierarchicalGrouping,
conveyance = errorDocItem.conveyance.getOrElse("").replace('\n', ' '),
code = errorDocItem.code,
deprecationO = errorDocItem.deprecation.map(_.deprecation.replace('\n', ' ')),
explanation = errorDocItem.explanation.fold("")(_.explanation).replace('\n', ' '),
resolution = errorDocItem.resolution.fold("")(_.resolution).replace('\n', ' '),
)
}
val root = ErrorGroupTree.empty()
// Build trie like structure of error groups and error codes.
errorCodes.foreach(errorCode =>
root.insertErrorCode(errorCode, groupSegmentsToExplanationMap)
)
// Traverse the trie to emit error code text.
ErrorGroupTree
.collectErrorCodesAsReStructuredTextSubsections(root)
.mkString("\n\n")
}
if (args.length >= 1) {
val outputFile = Paths.get(args(0))
val _ = Files.write(outputFile, text.getBytes, StandardOpenOption.CREATE_NEW)
} else {
println(text)
}
}
}
case class ErrorCodeValue(
code: String,
errorGroupPath: ErrorClass,
category: String,
explanation: String,
resolution: String,
conveyance: String,
deprecationO: Option[String],
)
class ErrorGroupTree(
val name: String,
val explanation: Option[String] = None,
children: mutable.Map[Grouping, ErrorGroupTree] =
new mutable.HashMap[Grouping, ErrorGroupTree](),
errorCodes: mutable.Map[String, ErrorCodeValue] = new mutable.HashMap[String, ErrorCodeValue](),
) {
def sortedSubGroups(): List[ErrorGroupTree] = {
children.values.toList.sortBy(_.name)
}
def sortedErrorCodes(): List[ErrorCodeValue] = {
errorCodes.values.toList.sortBy(_.code)
}
def insertErrorCode(
errorCode: ErrorCodeValue,
getExplanation: (List[Grouping]) => Option[String],
): Unit = {
insert(
remaining = errorCode.errorGroupPath.groupings,
path = Nil,
errorCode = errorCode,
getExplanation = getExplanation,
)
}
private def insert(
remaining: List[Grouping],
errorCode: ErrorCodeValue,
path: List[Grouping],
getExplanation: (List[Grouping]) => Option[String],
): Unit = {
remaining match {
case Nil =>
assert(!errorCodes.contains(errorCode.code), s"Code: ${errorCode.code} is already present!")
errorCodes.put(errorCode.code, errorCode): Unit
case headGroup :: tail =>
val newPath = path :+ headGroup
if (!children.contains(headGroup)) {
children.put(
headGroup,
new ErrorGroupTree(
name = headGroup.docName,
explanation = getExplanation(newPath),
),
)
}
children(headGroup).insert(
remaining = tail,
errorCode = errorCode,
path = newPath,
getExplanation,
)
}
}
}
object ErrorGroupTree {
def empty(): ErrorGroupTree = new ErrorGroupTree(
name = "",
explanation = None,
)
def collectErrorCodesAsReStructuredTextSubsections(root: ErrorGroupTree): List[String] = {
// in-order tree traversal
def iter(
tree: ErrorGroupTree,
path: List[String],
groupHierarchicalIndex: List[Int],
): List[String] = {
val newPath = path :+ tree.name
val textBuffer: mutable.ArrayBuffer[String] = new ArrayBuffer[String]()
// Add group text
textBuffer.addOne(s"""${groupHierarchicalIndex.mkString(".")}. ${newPath.mkString(" / ")}
|-------------------------------------------------------------------------------------------------------------------
|
|${tree.explanation.getOrElse("")}
|""".stripMargin)
// Add error codes in this group
textBuffer.addAll(
tree
.sortedErrorCodes()
.map(handleErrorCode)
)
// Recurse to sub-groups
textBuffer.addAll(
tree
.sortedSubGroups()
.zipWithIndex
.flatMap { case (subGroup: ErrorGroupTree, index: Int) =>
iter(
subGroup,
newPath,
groupHierarchicalIndex = groupHierarchicalIndex :+ (index + 1),
)
}
)
textBuffer.toList
}
root
.sortedSubGroups()
.zipWithIndex
.flatMap { case (subGroup, i) =>
iter(subGroup, path = List(), groupHierarchicalIndex = List(i + 1))
}
}
private def handleErrorCode(e: ErrorCodeValue): String = {
val deprecationText = e.deprecationO.fold("")(d => s"""
| **Deprecation**: ${d}
| """.stripMargin)
s"""${e.code}
|^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| $deprecationText
| **Explanation**: ${e.explanation}
|
| **Category**: ${e.category}
|
| **Conveyance**: ${e.conveyance}
|
| **Resolution**: ${e.resolution}
|
|""".stripMargin
}
}

View File

@ -23,7 +23,7 @@ object Main {
)(i =>
(
i.docName,
i.group.map(_.fullClassName),
i.fullClassName,
)
)
@ -41,12 +41,12 @@ object Main {
(
i.className,
i.category,
i.hierarchicalGrouping,
i.hierarchicalGrouping.groupings,
i.conveyance,
i.code,
i.deprecation.deprecation,
i.explanation.explanation,
i.resolution.resolution,
i.deprecation.fold("")(_.deprecation),
i.explanation.fold("")(_.explanation),
i.resolution.fold("")(_.resolution),
)
)
@ -57,7 +57,7 @@ object Main {
)(i =>
(
i.className,
i.explanation.explanation,
i.explanation.fold("")(_.explanation),
)
)
@ -66,10 +66,15 @@ object Main {
def main(args: Array[String]): Unit = {
val (errorCodes, groups) = new ErrorCodeDocumentationGenerator().getDocItems
val outputFile = Paths.get(args(0))
val output = Output(errorCodes, groups)
val outputText: String = output.asJson.spaces2
val outputBytes = outputText.getBytes
val _ = Files.write(outputFile, outputBytes, StandardOpenOption.CREATE_NEW)
if (args.length >= 1) {
val outputFile = Paths.get(args(0))
Files.write(outputFile, outputText.getBytes, StandardOpenOption.CREATE_NEW): Unit
} else {
println(outputText)
}
}
}

View File

@ -65,12 +65,12 @@ class ErrorCodeDocumentationGenerator(prefixes: Array[String] = Array("com.daml"
ErrorDocItem(
className = error.getClass.getName,
category = simpleClassName(error.category),
hierarchicalGrouping = error.parent.groupings.filter(_.docName.nonEmpty),
conveyance = error.errorConveyanceDocString.getOrElse(""),
hierarchicalGrouping = error.parent,
conveyance = error.errorConveyanceDocString,
code = error.id,
deprecation = deprecation.getOrElse(Deprecation("")),
explanation = explanation.getOrElse(Explanation("")),
resolution = resolution.getOrElse(Resolution("")),
deprecation = deprecation,
explanation = explanation,
resolution = resolution,
)
}
@ -79,8 +79,9 @@ class ErrorCodeDocumentationGenerator(prefixes: Array[String] = Array("com.daml"
getGroupDocumentationAnnotations(group)
GroupDocItem(
className = group.getClass.getName,
explanation = explanation.getOrElse(Explanation("")),
errorClass = group.errorClass,
className = group.fullClassName,
explanation = explanation,
)
}

View File

@ -3,7 +3,7 @@
package com.daml.error.generator
import com.daml.error.{Deprecation, Explanation, Grouping, Resolution}
import com.daml.error.{Deprecation, ErrorClass, Explanation, Resolution}
/** Contains error presentation data to be used for documentation rendering on the website.
*
@ -19,10 +19,10 @@ import com.daml.error.{Deprecation, Explanation, Grouping, Resolution}
case class ErrorDocItem(
className: String, // TODO error codes: Rename to `errorCodeName` or `errorCodeClassName` to prevent confusion
category: String,
hierarchicalGrouping: List[Grouping],
conveyance: String,
hierarchicalGrouping: ErrorClass,
conveyance: Option[String],
code: String,
deprecation: Deprecation,
explanation: Explanation,
resolution: Resolution,
deprecation: Option[Deprecation],
explanation: Option[Explanation],
resolution: Option[Resolution],
)

View File

@ -3,14 +3,16 @@
package com.daml.error.generator
import com.daml.error.Explanation
import com.daml.error.{ErrorClass, Explanation}
/** Contains error presentation data to be used for documentation rendering on the website.
*
* @param className The group class name (see [[com.daml.error.ErrorGroup]]).
* @param explanation The detailed error explanation.
* @param errorClass Hierarchical grouping of this error group.
*/
case class GroupDocItem(
className: String,
explanation: Explanation,
explanation: Option[Explanation],
errorClass: ErrorClass,
)

View File

@ -3,18 +3,22 @@
package com.daml.error
/** A grouping of errors.
/** A component of [[ErrorClass]]
*
* @param docName The name that will appear in the generated documentation for the grouping.
* @param group If the grouping is defined by an [[ErrorGroup]], the associated instance.
* @param fullClassName Full class name of the corresponding [[ErrorGroup]].
*/
case class Grouping(
docName: String,
group: Option[ErrorGroup],
)
fullClassName: String,
) {
require(
docName.trim.nonEmpty,
s"Grouping.docName must be non empty and must contain not only whitespace characters, but was: |${docName}|!",
)
}
/** The classes [[ErrorClass]] and [[ErrorGroup]] are used to hierarchically structure error codes (their
* hierarchical structure affects how they are displayed on the website)
/** Used to hierarchically structure error codes in the official documentation.
*/
case class ErrorClass(groupings: List[Grouping]) {
def extend(grouping: Grouping): ErrorClass =

View File

@ -21,6 +21,6 @@ abstract class ErrorGroup()(implicit parent: ErrorClass) {
s"Could not parse full class name: '${fullClassName}' for the error class name"
)
)
parent.extend(Grouping(name, Some(this)))
parent.extend(Grouping(docName = name, fullClassName = fullClassName))
}
}

View File

@ -11,29 +11,25 @@ class ErrorGroupSpec extends AnyFlatSpec with Matchers with BeforeAndAfter {
object ErrorGroupBar extends ErrorGroup()(ErrorClass.root())
object ErrorGroupsFoo {
private implicit val errorClass: ErrorClass = ErrorClass.root()
object ErrorGroupFoo1 extends ErrorGroup() {
object ErrorGroupFoo2 extends ErrorGroup() {
object ErrorGroupFoo3 extends ErrorGroup()
}
object ErrorGroupFoo1 extends ErrorGroup()(ErrorClass.root()) {
object ErrorGroupFoo2 extends ErrorGroup() {
object ErrorGroupFoo3 extends ErrorGroup()
}
}
it should "resolve correct error group names" in {
ErrorGroupsFoo.ErrorGroupFoo1.ErrorGroupFoo2.ErrorGroupFoo3.errorClass shouldBe ErrorClass(
ErrorGroupFoo1.ErrorGroupFoo2.ErrorGroupFoo3.errorClass shouldBe ErrorClass(
List(
Grouping("ErrorGroupFoo1", Some(ErrorGroupsFoo.ErrorGroupFoo1)),
Grouping("ErrorGroupFoo2", Some(ErrorGroupsFoo.ErrorGroupFoo1.ErrorGroupFoo2)),
Grouping("ErrorGroupFoo1", ErrorGroupFoo1.fullClassName),
Grouping("ErrorGroupFoo2", ErrorGroupFoo1.ErrorGroupFoo2.fullClassName),
Grouping(
"ErrorGroupFoo3",
Some(ErrorGroupsFoo.ErrorGroupFoo1.ErrorGroupFoo2.ErrorGroupFoo3),
ErrorGroupFoo1.ErrorGroupFoo2.ErrorGroupFoo3.fullClassName,
),
)
)
ErrorGroupBar.errorClass shouldBe ErrorClass(
List(Grouping("ErrorGroupBar", Some(ErrorGroupBar)))
List(Grouping("ErrorGroupBar", ErrorGroupBar.fullClassName))
)
}

View File

@ -8,7 +8,9 @@ import com.daml.logging.LoggingContext
@Explanation("Groups mild errors together")
object MildErrors
extends ErrorGroup()(parent = ErrorClass.root().extend(Grouping("Some grouping", None))) {
extends ErrorGroup()(
parent = ErrorClass.root().extend(Grouping("Some grouping", "full.class.Name123"))
) {
@Explanation("Test: Things like this always happen.")
@Resolution("Test: Why not ignore?")