# SPDX-License-Identifier: BSD-2-Clause """ This module provides functions to document specification items. """ # Copyright (C) 2020 embedded brains GmbH (http://www.embedded-brains.de) # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. import re from typing import Any, Dict, Iterator, List, Optional, Pattern, Set, Tuple from rtemsspec.sphinxcontent import get_reference, get_label, \ SphinxContent, SphinxMapper from rtemsspec.items import Item, ItemCache, ItemGetValueContext from rtemsspec.specverify import NAME _DocumenterMap = Dict[str, "_Documenter"] _PRIMITIVE_TYPES = { "bool": "{} {} be a boolean.", "float": "{} {} be a floating-point number.", "int": "{} {} be an integer number.", "list-str": "{} {} be a list of strings.", "none": "The attribute shall have no value.", "optional-str": "{} {} be an optional string.", "str": "{} {} be a string.", } _MANDATORY_ATTRIBUTES = { "all": "All explicit attributes shall be specified.", "at-least-one": "At least one of the explicit attributes shall be specified.", "at-most-one": "At most one of the explicit attributes shall be specified.", "exactly-one": "Exactly one of the explicit attributes shall be specified.", "none": "None of the explicit attributes is mandatory, " "they are all optional.", } def _a_or_an(value: str) -> str: if value[0].lower() in ["a", "e", "i", "o", "u"]: return "an" return "a" class _AssertContext: """ This class provides a context to document assert expressions. """ def __init__(self, content: SphinxContent, ops: Dict[str, Any]): self.content = content self.ops = ops self._comma = "" def comma(self): """ Adds a comma to the content if necessary. """ if not self.content.lines[-1].endswith(","): self.content.lines[-1] += self._comma self._comma = "," def paste(self, text: str): """ Pastes a text to the content. """ self.content.paste(text) def _negate(negate: bool) -> str: if negate: return "not " return "" def _value(value: Any) -> str: if isinstance(value, str): return f"\"``{value}``\"" if isinstance(value, bool): if value: return "true" return "false" return str(value) def _list(ctx: _AssertContext, assert_info: List[str]) -> None: ctx.content.add_list([f"{_value(value)}," for value in assert_info[:-2]]) try: ctx.content.add_list_item(f"{_value(assert_info[-2])}, and") except IndexError: pass try: ctx.content.add_list_item(f"{_value(assert_info[-1])}") except IndexError: pass def _document_op_and_or(ctx: _AssertContext, negate: bool, assert_info: Any, and_or: str) -> None: if len(assert_info) == 1: _document_assert(ctx, negate, assert_info[0]) else: if negate or ctx.content.lines[-1].endswith("* "): ctx.paste(f"shall {_negate(negate)}meet") intro = "" for element in assert_info: ctx.comma() with ctx.content.list_item(intro): _document_assert(ctx, False, element) intro = and_or def _document_op_and(ctx: _AssertContext, negate: bool, assert_info: Any) -> None: _document_op_and_or(ctx, negate, assert_info, "and, ") def _document_op_not(ctx: _AssertContext, negate: bool, assert_info: Any) -> None: _document_assert(ctx, not negate, assert_info) def _document_op_or(ctx: _AssertContext, negate: bool, assert_info: Any) -> None: _document_op_and_or(ctx, negate, assert_info, "or, ") def _document_op_eq(ctx: _AssertContext, negate: bool, assert_info: Any) -> None: if negate: _document_op_ne(ctx, False, assert_info) else: ctx.paste(f"shall be equal to {_value(assert_info)}") def _document_op_ne(ctx: _AssertContext, negate: bool, assert_info: Any) -> None: if negate: _document_op_eq(ctx, False, assert_info) else: ctx.paste(f"shall be not equal to {_value(assert_info)}") def _document_op_le(ctx: _AssertContext, negate: bool, assert_info: Any) -> None: if negate: _document_op_gt(ctx, False, assert_info) else: ctx.paste(f"shall be less than or equal to {_value(assert_info)}") def _document_op_lt(ctx: _AssertContext, negate: bool, assert_info: Any) -> None: if negate: _document_op_ge(ctx, False, assert_info) else: ctx.paste(f"shall be less than {_value(assert_info)}") def _document_op_ge(ctx: _AssertContext, negate: bool, assert_info: Any) -> None: if negate: _document_op_lt(ctx, False, assert_info) else: ctx.paste(f"shall be greater than or equal to {_value(assert_info)}") def _document_op_gt(ctx: _AssertContext, negate: bool, assert_info: Any) -> None: if negate: _document_op_le(ctx, False, assert_info) else: ctx.paste(f"shall be greater than {_value(assert_info)}") def _document_op_uid(ctx: _AssertContext, negate: bool, _assert_info: Any) -> None: if negate: ctx.paste("shall be an invalid item UID") else: ctx.paste("shall be a valid item UID") def _document_op_re(ctx: _AssertContext, negate: bool, assert_info: Any) -> None: ctx.paste(f"shall {_negate(negate)}match with " f"the regular expression \"``{assert_info}``\"") def _document_op_in(ctx: _AssertContext, negate: bool, assert_info: Any) -> None: ctx.paste(f"shall {_negate(negate)}be an element of") _list(ctx, assert_info) def _document_op_contains(ctx: _AssertContext, negate: bool, assert_info: Any) -> None: ctx.paste(f"shall {_negate(negate)}contain an element of") _list(ctx, assert_info) def _document_assert(ctx: _AssertContext, negate: bool, assert_info: Any) -> None: if isinstance(assert_info, bool): if negate: assert_info = not assert_info ctx.paste(f"shall be {_value(assert_info)}") elif isinstance(assert_info, list): _document_op_or(ctx, negate, assert_info) else: key = next(iter(assert_info)) ctx.ops[key](ctx, negate, assert_info[key]) _DOCUMENT_OPS = { "and": _document_op_and, "contains": _document_op_contains, "eq": _document_op_eq, "ge": _document_op_ge, "gt": _document_op_gt, "in": _document_op_in, "le": _document_op_le, "lt": _document_op_lt, "ne": _document_op_ne, "not": _document_op_not, "or": _document_op_or, "re": _document_op_re, "uid": _document_op_uid, } def _maybe_document_assert(content: SphinxContent, type_info: Any) -> None: if "assert" in type_info: content.paste("The value ") _document_assert(_AssertContext(content, _DOCUMENT_OPS), False, type_info["assert"]) content.lines[-1] += "." class _Documenter: # pylint: disable=too-many-instance-attributes def __init__(self, item: Item, documenter_map: _DocumenterMap, config: dict): self._name = item["spec-type"] self.section = item["spec-name"] self._info_map = item["spec-info"] self._item = item self._documenter_map = documenter_map self.used_by = set() # type: Set[str] self._label_prefix = config["label-prefix"] self._mapper = SphinxMapper(item) self._mapper.add_get_value("spec:/spec-name", self._get_ref_specification_type) self._description = self._substitute(item["spec-description"]) assert self._name not in documenter_map documenter_map[self._name] = self def _get_ref_specification_type(self, ctx: ItemGetValueContext) -> Any: return get_reference(self._label_prefix + get_label(ctx.value[ctx.key])) def _substitute(self, text: str) -> str: if text: return self._mapper.substitute(text) return text def get_section_reference(self) -> str: """ Returns the section reference. """ return get_reference(self._label_prefix + get_label(self.section)) def get_a_section_reference(self) -> str: """ Returns a section reference. """ return f"{_a_or_an(self.section)} {self.get_section_reference()}" def get_list_element_type(self) -> str: """ Returns the list element type if this is a list only type. """ if len(self._info_map) == 1 and "list" in self._info_map: return self._info_map["list"]["spec-type"] return "" def get_list_phrase(self, value: str, shall: str, type_name: str) -> str: """ Returns a list phrase. """ if type_name in _PRIMITIVE_TYPES: type_phrase = _PRIMITIVE_TYPES[type_name].format( "Each list element", "shall") else: documenter = self._documenter_map[type_name] ref = documenter.get_a_section_reference() type_phrase = f"Each list element shall be {ref}." return f"{value} {shall} be a list. {type_phrase}" def get_value_type_phrase(self, value: str, shall: str, type_name: str) -> str: """ Returns a value type phrase. """ if type_name in _PRIMITIVE_TYPES: return _PRIMITIVE_TYPES[type_name].format(value, shall) documenter = self._documenter_map[type_name] element_type_name = documenter.get_list_element_type() if element_type_name: return self.get_list_phrase(value, shall, element_type_name) return (f"{value} {shall} be " f"{documenter.get_a_section_reference()}.") def refinements(self, ignore: Pattern[Any]) -> Iterator["_Documenter"]: """ Yields the refinements of this type. """ refinement_set = set( self._documenter_map[child["spec-type"]] for child in self._item.children("spec-refinement") if ignore.search(child["spec-type"]) is None) yield from sorted(refinement_set, key=lambda x: x.section) def refines(self) -> Iterator[Tuple["_Documenter", str, str]]: """ Yields the types refined by type. """ refines = [(self._documenter_map[link.item["spec-type"]], link["spec-key"], link["spec-value"]) for link in self._item.links_to_parents() if link.role == "spec-refinement"] yield from sorted(refines, key=lambda x: x[0].section) def hierarchy(self, content: SphinxContent, ignore) -> None: """ Documents the item type hierarchy. """ with content.list_item(self.get_section_reference()): for refinement in self.refinements(ignore): refinement.hierarchy(content, ignore) def _document_attributes(self, content: SphinxContent, attributes: Any) -> None: for key in sorted(attributes): info = attributes[key] content.add(key) with content.indent(): content.wrap( self.get_value_type_phrase("The attribute value", "shall", info["spec-type"])) content.paste(self._substitute(info["description"])) def document_dict(self, content: SphinxContent, _variant: str, shall: str, info: Any) -> None: """ Documents an attribute set. """ if shall == "may": content.paste("The value may be a set of attributes.") content.paste(self._substitute(info["description"])) has_explicit_attributes = len(info["attributes"]) > 0 if has_explicit_attributes: mandatory_attributes = info["mandatory-attributes"] if isinstance(mandatory_attributes, str): content.paste(_MANDATORY_ATTRIBUTES[mandatory_attributes]) else: assert isinstance(mandatory_attributes, list) mandatory_attribute_count = len(mandatory_attributes) if mandatory_attribute_count == 1: content.paste(f"Only the ``{mandatory_attributes[0]}`` " "attribute is mandatory.") elif mandatory_attribute_count > 1: content.paste("The following explicit " "attributes are mandatory:") for attribute in sorted(mandatory_attributes): content.add_list_item(f"``{attribute}``") content.ensure_blank_line() else: content.ensure_blank_line() content.paste("The explicit attributes for this type are:") self._document_attributes(content, info["attributes"]) if "generic-attributes" in info: if has_explicit_attributes: content.wrap("In addition to the explicit attributes, " "generic attributes may be specified.") else: content.paste("Generic attributes may be specified.") content.paste( self.get_value_type_phrase( "Each generic attribute key", "shall", info["generic-attributes"]["key-spec-type"])) content.paste( self.get_value_type_phrase( "Each generic attribute value", "shall", info["generic-attributes"]["value-spec-type"])) content.paste( self._substitute(info["generic-attributes"]["description"])) def document_value(self, content: SphinxContent, variant: str, shall: str, info: Any) -> None: """ Documents a value. """ content.paste(self.get_value_type_phrase("The value", shall, variant)) content.paste(self._substitute(info["description"])) _maybe_document_assert(content, info) def document_list(self, content: SphinxContent, _variant: str, shall: str, info: Any) -> None: """ Documents a list value. """ content.paste( self.get_list_phrase("The value", shall, info["spec-type"])) content.paste(self._substitute(info["description"])) def document_none(self, content: SphinxContent, _variant: str, shall: str, _info: Any) -> None: """ Documents a none value. """ # pylint: disable=no-self-use content.paste(f"There {shall} by be no value (null).") def _add_description(self, content: SphinxContent) -> None: refines = [ f"{documenter.get_section_reference()} though the " f"``{key}`` attribute if the value is ``{value}``" for documenter, key, value in self.refines() ] if len(refines) == 1: content.wrap(f"This type refines the {refines[0]}.") content.paste(self._description) else: content.add_list(refines, "This type refines the following types:", add_blank_line=True) content.wrap(self._description) if self._description: content.add_blank_line() def document(self, content: SphinxContent, ignore: Pattern[Any], names: Optional[Set[str]] = None) -> None: """ Document this type. """ if self.get_list_element_type(): return content.register_license_and_copyrights_of_item(self._item) with content.section(self.section, self._label_prefix): last = content.lines[-1] self._add_description(content) if len(self._info_map) == 1: if last == content.lines[-1]: content.add_blank_line() key, info = next(iter(self._info_map.items())) _DOCUMENT[key](self, content, key, "shall", info) else: content.add("A value of this type shall be of one of " "the following variants:") for key in sorted(self._info_map): with content.list_item(""): _DOCUMENT[key](self, content, key, "may", self._info_map[key]) content.add_list([ refinement.get_section_reference() for refinement in self.refinements(ignore) ], "This type is refined by the following types:") content.add_list(sorted(self.used_by), "This type is used by the following types:") example = self._item["spec-example"] if example: content.add("Please have a look at the following example:") with content.directive("code-block", "yaml"): content.add(example) if names: names.remove(self._name) for refinement in self.refinements(ignore): refinement.document(content, ignore, names) def _add_used_by(self, type_name: str) -> None: if type_name not in _PRIMITIVE_TYPES: documenter = self._documenter_map[type_name] element_type_name = documenter.get_list_element_type() if element_type_name: type_name = element_type_name if type_name not in _PRIMITIVE_TYPES: documenter = self._documenter_map[type_name] documenter.used_by.add(self.get_section_reference()) def resolve_used_by(self) -> None: """ Resolves type uses in attribute sets. """ info = self._info_map.get("dict", None) if info is not None: for attribute in info["attributes"].values(): self._add_used_by(attribute["spec-type"]) if "generic-attributes" in info: self._add_used_by(info["generic-attributes"]["key-spec-type"]) self._add_used_by( info["generic-attributes"]["value-spec-type"]) _DOCUMENT = { "bool": _Documenter.document_value, "dict": _Documenter.document_dict, "float": _Documenter.document_value, "int": _Documenter.document_value, "list": _Documenter.document_list, "none": _Documenter.document_none, "str": _Documenter.document_value, } def _create_str_documenter(item_cache: ItemCache, name: str, description: str, documenter_map: _DocumenterMap, config: dict) -> None: type_name = name.lower() _Documenter( Item( item_cache, f"/spec/{type_name}", { "SPDX-License-Identifier": "CC-BY-SA-4.0 OR BSD-2-Clause", "copyrights": [ "Copyright (C) 2020 embedded brains GmbH " "(http://www.embedded-brains.de)" ], "spec-description": None, "spec-example": None, "spec-info": { "str": { "description": description } }, "spec-name": name, "spec-type": type_name, }), documenter_map, config) def document(config: dict, item_cache: ItemCache) -> None: """ Documents specification items according to the configuration. :param config: A dictionary with configuration entries. :param item_cache: The specification item cache. """ documenter_map = {} # type: _DocumenterMap root_item = item_cache[config["root-type"]] _create_str_documenter( item_cache, "Name", "A string is a valid name if it matches with the " f"``{NAME.pattern.replace('$', '$$')}`` regular expression.", documenter_map, config) _create_str_documenter( item_cache, "UID", "The string shall be a valid absolute or relative item UID.", documenter_map, config) root_documenter = _Documenter(root_item, documenter_map, config) ignore = re.compile(config["ignore"]) for member in root_item.children("spec-member"): if ignore.search(member["spec-type"]) is None: _Documenter(member, documenter_map, config) content = SphinxContent() content.add_automatically_generated_warning() for documenter in documenter_map.values(): documenter.resolve_used_by() documenter_names = set(documenter_map) content.section_label_prefix = config["section-label-prefix"] with content.section(config["section-name"]): with content.section(config["hierarchy-subsection-name"]): content.add(config["hierarchy-text"]) root_documenter.hierarchy(content, ignore) with content.section(config["item-types-subsection-name"]): root_documenter.document(content, ignore, documenter_names) with content.section(config["value-types-subsection-name"]): documenters = [documenter_map[name] for name in documenter_names] for documenter in sorted(documenters, key=lambda x: x.section): documenter.document(content, ignore) content.add_licence_and_copyrights() content.write(config["doc-target"])