summaryrefslogblamecommitdiffstats
path: root/rtemsspec/specdoc.py
blob: 48e00f9a167bea5a68069e71dd49e80283c84af9 (plain) (tree)

























                                                                             

                                                                           
 
                                                               
                               

                                                                












                                                   


                                                  
                   
                                                                  
                  
                                                                 
                  


                                                                 
                             

 






                                                     






































































































































                                                                             
                                                              



















































                                                                           
                  
                                                  

                                                                  

                                        


                                             
                                              
                                                   
                                         
                                                     
                                                                    
                                                                      


                                               



                                                                           




                                                
                                           
                                              
                                                                          
 
                                             
                                            
                                                                         






                                                                          
                                                                             


                                                             
                                             

                                                        
                                                      
                                                              
                                                          
 

                                                           





                                                                   
                                                                        
                                      
                                                           
 
                                                                           
                                                    




                                                                  








                                                                  
                                                                
                                                  
                                                             

                                                       


                                                          

                                      


                                  

                                                                              
                                                                    





                                                                              
                                                            

                                                             


                                                                          
                 








                                                                            
                                                                 
                                               
                     
                                               
                                                                       


                                                                  

                                                                       
                 
                                                                     







                                                                   
                          
                                                                            



                                                                              
                                                                              
                                                            
                                             



                                                                              
                      
                                                                        
                                                            






                                                                              

                                                               
                                                               




                                                                
                                            



                                                                      
                                           

                                    
 

                                        
                                      




                                                                   
                                                               
                                    
                                          







                                                                      
                                                  


                                                                 
                              
                                                  
                                                          
                                                              

                                                                         






                                                                           

                                                           
 
                                                   






                                                                  
                                                                
 
                                      



                                                         
                                                         
                                            


                                                                              
 











                                        
                                                                              

                                                          






















                                                              
                                       











                                                                 

                                                                              
                               


                                                                     

                                                                    



                                                       
                             
                                                 
                                              
                                    
                                          



                                                                  
                                                      
                                                                   
                                                                       
                                                                    

                                                                             
                                                    

                                        
# 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"])