# SPDX-License-Identifier: BSD-2-Clause """ Functions for application configuration documentation generation. """ # Copyright (C) 2019, 2020 embedded brains GmbH & Co. KG # # 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. from typing import Any, Callable, Dict, List, Optional from rtemsspec.content import Content, CContent, get_value_double_colon, \ get_value_doxygen_function, get_value_doxygen_group, \ get_value_doxygen_ref, get_value_hash, get_value_header_file, \ get_value_plural from rtemsspec.sphinxcontent import GenericContent, SphinxContent, \ SphinxInterfaceMapper from rtemsspec.items import EmptyItem, Item, ItemCache, ItemGetValueContext, \ ItemMapper ItemMap = Dict[str, Item] _FEATURE = "This configuration option is a boolean feature define." _OPTION_TYPES = { "feature": _FEATURE, "feature-enable": _FEATURE, "integer": "This configuration option is an integer define.", "initializer": "This configuration option is an initializer define." } _OPTION_DEFAULT_CONFIG = { "feature": lambda item: item["default"], "feature-enable": lambda item: """If this configuration option is undefined, then the described feature \ is not enabled.""" } class _ContentAdaptor: """ The content adaptor provides a specialized interface to a content class. By default, Sphinx content is generated. """ def __init__(self, mapper: ItemMapper, content: Any) -> None: self.mapper = mapper self.content = content def substitute(self, text: Optional[str]) -> str: """ Substitutes the optional text using the item mapper. """ return self.mapper.substitute(text) def add_group(self, uid: str, name: str, description: str) -> None: """ Adds an option group. """ self.content.add_automatically_generated_warning() self.content.add(f".. Generated from spec:{uid}") self.content.add_header(name, level=2) self.content.add(description) def _add_rubric(self, name: str, text: GenericContent, wrap: bool = False) -> None: if not text: return self.content.add_rubric(f"{name}:") if wrap: self.content.wrap(text) else: self.content.add(text) def add_option(self, uid: str, name: str, index_entries: List[str]) -> None: """ Adds an option. """ self.content.add(f".. Generated from spec:{uid}") with self.content.directive("raw", "latex"): self.content.add("\\clearpage") self.content.add_index_entries([name] + index_entries) self.content.add_label(name) self.content.add_header(name, level=3) self._add_rubric("CONSTANT", f"``{name}``") def add_option_type(self, option_type: str) -> None: """ Adds an option type. """ self._add_rubric("OPTION TYPE", option_type) def add_option_default_value(self, value: str) -> None: """ Adds an option default value. """ self._add_rubric("DEFAULT VALUE", value) def add_option_default_config(self, config: str) -> None: """ Adds an option default configuration. """ self._add_rubric("DEFAULT CONFIGURATION", config) def add_option_description(self, description: str) -> None: """ Adds a option description. """ self._add_rubric("DESCRIPTION", description) def add_option_notes(self, notes: str) -> None: """ Adds option notes. """ self._add_rubric("NOTES", notes) def add_option_constraints(self, lines: List[str]) -> None: """ Adds a option value constraints. """ self._add_rubric("CONSTRAINTS", lines, wrap=True) def add_licence_and_copyrights(self) -> None: """ Adds the license and copyrights. """ self.content.add_licence_and_copyrights() def register_license_and_copyrights_of_item(self, item: Item) -> None: """ Registers the license and copyrights of the item. """ self.content.register_license_and_copyrights_of_item(item) def write(self, filename: str): """ Writes the content to the file specified by the path. """ self.content.write(filename) class _SphinxContentAdaptor(_ContentAdaptor): def __init__(self, mapper: ItemMapper) -> None: super().__init__(mapper, SphinxContent()) class _DoxygenContentAdaptor(_ContentAdaptor): # pylint: disable=attribute-defined-outside-init def __init__(self, mapper: ItemMapper) -> None: super().__init__(mapper, CContent()) self._reset() def _reset(self) -> None: self._name = "" self._option_type = "" self._default_value = "" self._default_config = "" self._notes = "" self._description = "" def add_group(self, uid: str, name: str, description: str) -> None: identifier = f"RTEMSApplConfig{name.replace(' ', '')}" self.content.add(f"/* Generated from spec:{uid} */") with self.content.defgroup_block(identifier, name): self.content.add("@ingroup RTEMSApplConfig") self.content.doxyfy(description) self.content.add("@{") def add_option(self, uid: str, name: str, _index_entries: List[str]) -> None: self.content.add(f"/* Generated from spec:{uid} */") self.content.open_doxygen_block() self._name = name def add_option_type(self, option_type: str) -> None: self._option_type = option_type def add_option_default_value(self, value: str) -> None: self._default_value = value def add_option_default_config(self, config: str) -> None: self._default_config = config def add_option_description(self, description: str) -> None: self._description = description def add_option_notes(self, notes: str) -> None: self._notes = notes def add_option_constraints(self, lines: List[str]) -> None: self.content.add_brief_description(self._option_type) self.content.add(f"@anchor {self._name}") self.content.doxyfy(self._description) self.content.add_paragraph("Default Value", self._default_value) self.content.add_paragraph("Default Configuration", self._default_config) self.content.add_paragraph("Constraints", lines) self.content.add_paragraph("Notes", self._notes) self.content.close_comment_block() self.content.append(f"#define {self._name}") self._reset() def add_licence_and_copyrights(self) -> None: self.content.add("/** @} */") def _generate_feature(content: _ContentAdaptor, item: Item, option_type: str) -> None: content.add_option_default_config( content.substitute(_OPTION_DEFAULT_CONFIG[option_type](item))) def _get_constraints(content: _ContentAdaptor, item: Item, enabled: List[str]) -> List[str]: constraints: List[str] = [] for parent in item.parents("constraint"): if not parent.is_enabled(enabled): continue content.register_license_and_copyrights_of_item(parent) constraints.append( content.substitute(parent["text"]).replace( "application configuration option", "configuration option")) return constraints def _generate_constraints(content: _ContentAdaptor, item: Item, enabled: List[str]) -> None: constraints = _get_constraints(content, item, enabled) if len(constraints) > 1: constraint_list = Content("BSD-2-Clause", False) prologue = ("The following constraints apply " "to this configuration option:") constraint_list.add_list(constraints, prologue) constraints = constraint_list.lines content.add_option_constraints(constraints) def _generate_initializer_or_integer(content: _ContentAdaptor, item: Item, _option_type: str) -> None: default_value = item["default-value"] if not isinstance(default_value, str) or " " not in default_value: default_value = f"The default value is {default_value}." content.add_option_default_value(content.substitute(default_value)) _OPTION_GENERATORS = { "feature": _generate_feature, "feature-enable": _generate_feature, "initializer": _generate_initializer_or_integer, "integer": _generate_initializer_or_integer } def _document_option(item: Item, enabled: List[str], content: _ContentAdaptor) -> None: option_type = item["appl-config-option-type"] content.add_option_type(_OPTION_TYPES[option_type]) _OPTION_GENERATORS[option_type](content, item, option_type) content.add_option_description(content.substitute(item["description"])) content.add_option_notes(content.substitute(item["notes"])) _generate_constraints(content, item, enabled) def document_option(item: Item, enabled: List[str], mapper: ItemMapper) -> SphinxContent: """ Documents the option specified by the item using the item mapper and enabled set. """ adaptor = _SphinxContentAdaptor(mapper) _document_option(item, enabled, adaptor) return adaptor.content def _generate(group: Item, options: ItemMap, enabled: List[str], content: _ContentAdaptor) -> None: content.register_license_and_copyrights_of_item(group) content.add_group(group.uid, group["name"], content.substitute(group["description"])) for item in sorted(options.values(), key=lambda x: x["name"]): content.mapper.item = item content.register_license_and_copyrights_of_item(item) content.add_option(item.uid, item["name"], item["index-entries"]) _document_option(item, enabled, content) content.add_licence_and_copyrights() def _get_value(ctx: ItemGetValueContext) -> str: return ctx.value[ctx.key] def _get_value_doxygen_url( ctx: ItemGetValueContext, get_value: Callable[[ItemGetValueContext], str] = _get_value) -> Optional[str]: for ref in ctx.item["references"]: if ref["type"] == "url": return f"{get_value(ctx)}" return None def _get_value_doxygen_unspecified_define(ctx: ItemGetValueContext) -> Any: return _get_value_doxygen_url(ctx) or get_value_hash(ctx) def _get_value_doxygen_unspecified_function(ctx: ItemGetValueContext) -> Any: return _get_value_doxygen_url( ctx, get_value_doxygen_function) or get_value_doxygen_function(ctx) def _get_value_doxygen_unspecified_group(ctx: ItemGetValueContext) -> Any: return _get_value_doxygen_url(ctx) or ctx.value[ctx.key] def _get_value_doxygen_unspecfied_type(ctx: ItemGetValueContext) -> Any: return _get_value_doxygen_url(ctx) or get_value_double_colon(ctx) def _add_doxygen_get_values(mapper: ItemMapper) -> None: for opt in ["feature-enable", "feature", "initializer", "integer"]: name = f"interface/appl-config-option/{opt}:/name" mapper.add_get_value(name, get_value_doxygen_ref) mapper.add_get_value("glossary/term:/plural", get_value_plural) mapper.add_get_value("interface/define:/name", get_value_hash) mapper.add_get_value("interface/function:/name", get_value_doxygen_function) mapper.add_get_value("interface/group:/name", get_value_doxygen_group) mapper.add_get_value("interface/header-file:/path", get_value_header_file) mapper.add_get_value("interface/macro:/name", get_value_doxygen_function) mapper.add_get_value("interface/struct:/name", get_value_double_colon) mapper.add_get_value("interface/typedef:/name", get_value_double_colon) mapper.add_get_value("interface/union:/name", get_value_double_colon) mapper.add_get_value("interface/unspecified-define:/name", _get_value_doxygen_unspecified_define) mapper.add_get_value("interface/unspecified-function:/name", _get_value_doxygen_unspecified_function) mapper.add_get_value("interface/unspecified-group:/name", _get_value_doxygen_unspecified_group) mapper.add_get_value("interface/unspecified-enum:/name", _get_value_doxygen_unspecfied_type) mapper.add_get_value("interface/unspecified-struct:/name", _get_value_doxygen_unspecfied_type) mapper.add_get_value("interface/unspecified-typedef:/name", _get_value_doxygen_unspecfied_type) mapper.add_get_value("interface/unspecified-union:/name", _get_value_doxygen_unspecfied_type) def generate(config: dict, group_uids: List[str], item_cache: ItemCache) -> None: """ Generates application configuration documentation sources according to the configuration. :param config: A dictionary with configuration entries. :param item_cache: The specification item cache containing the application configuration groups and options. """ sphinx_mapper = SphinxInterfaceMapper(EmptyItem(), group_uids) doxygen_mapper = ItemMapper(EmptyItem()) _add_doxygen_get_values(doxygen_mapper) doxygen_content = _DoxygenContentAdaptor(doxygen_mapper) doxygen_content.content.add_automatically_generated_warning() with doxygen_content.content.defgroup_block( "RTEMSApplConfig", "Application Configuration Options"): doxygen_content.content.add("@ingroup RTEMSAPI") for group_config in config["groups"]: group = item_cache[group_config["uid"]] assert group.type == "interface/appl-config-group" options: ItemMap = {} for child in group.children("interface-ingroup"): if child.type.startswith("interface/appl-config-option"): options[child.uid] = child sphinx_content = _SphinxContentAdaptor(sphinx_mapper) _generate(group, options, config["enabled-documentation"], sphinx_content) sphinx_content.write(group_config["target"]) _generate(group, options, config["enabled-source"], doxygen_content) doxygen_content.content.prepend_copyrights_and_licenses() doxygen_content.content.prepend([ "/**", " * @file", " *", " * @ingroup RTEMSImplDoxygen", " *", " * @brief This header file documents " "the application configuration options.", " */", "" ]) doxygen_content.content.prepend_spdx_license_identifier() doxygen_content.write(config["doxygen-target"])