xref: /aosp_15_r20/external/pigweed/pw_docgen/py/pw_docgen/sphinx/pw_status_codes.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1# Copyright 2024 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Implementation of the custom pw-status-codes reStructuredText directive.
15
16The main goal of this directive is to improve the consistency of the
17presentation of our API references. All C++ functions and methods that return
18a set of pw::Status codes use this directive.
19
20This directive is mainly used within the Doxygen comments of C++ header files.
21Usage example:
22
23/// @returns @rst
24///
25/// .. pw-status-codes::
26///
27///    OK: The bulk read was successful.
28///
29///    DEADLINE_EXCEEDED: Can't acquire
30///    bus access. See :ref:`x`.
31///
32/// @endrst
33
34The word before the colon must be a valid pw_status code. The text after the
35colon is a description of what the status code means in this particular
36scenario. Descriptions may span multiple lines and can contain nested
37reStructuredText.
38"""
39import sys
40from typing import List, Tuple
41
42from docutils import nodes
43from docutils.parsers.rst import Directive
44from docutils.statemachine import ViewList
45from pw_status import Status
46from sphinx.application import Sphinx
47
48
49help_url = 'https://pigweed.dev/docs/style/doxygen.html#pw-status-codes'
50
51
52class PwStatusCodesDirective(Directive):
53    """Renders `pw-status-codes` directives."""
54
55    has_content = True
56
57    # The main event handler for the directive.
58    # https://docutils.sourceforge.io/docs/howto/rst-directives.html
59    def run(self) -> list[nodes.Node]:
60        merged_data = self._merge()
61        transformed_data = self._transform(merged_data)
62        return self._render(transformed_data)
63
64    def _merge(self) -> List[str]:
65        """Merges the content into a format that's easier to work with.
66
67        Docutils provides content line-by-line, with empty strings
68        representing newlines:
69
70        [
71            'OK: The bulk read was successful.',
72            '',
73            'DEADLINE_EXCEEDED: Can't acquire',
74            'bus access. See :ref:`x`.',
75        ]
76
77        _merge() consolidates each code and its accompanying description into
78        a single string representing the complete key-value pair (KVP) and
79        removes the empty strings:
80
81        [
82            'OK: The bulk read was successful.',
83            'DEADLINE_EXCEEDED: Can't acquire bus access. See :ref:`x`.'
84        ]
85
86        We also need to handle the scenario of getting a single line of
87        content:
88
89        [
90            'OK: The operation was successful.'
91        ]
92        """
93        kvps = []  # key-value pairs
94        index = 0
95        kvps.append('')
96        content: ViewList = self.content
97        for line in content:
98            # An empty line in the source content means that we're about to
99            # encounter a new kvp. See the empty line after 'OK: The bulk read
100            # was successful.' for an example.
101            if line == '':
102                # Make sure that the current kvp has content before starting
103                # a new kvp.
104                if kvps[index] != '':
105                    index += 1
106                    kvps.append('')
107            else:  # The line has content we need.
108                # If the current line isn't empty and our current kvp also
109                # isn't empty then we're dealing with the multi-line scenario.
110                # See the DEADLINE_EXCEEDED example. We just need to add back
111                # the whitespace that was stripped out.
112                if kvps[index] != '':
113                    kvps[index] += ' '
114            # All edge cases have been handled and it's now safe to always add
115            # the current line to the current kvp.
116            kvps[index] += line
117        return kvps
118
119    def _transform(self, items: List[str]) -> List[Tuple[str, str]]:
120        """Transforms the data into normalized and annotated tuples.
121
122        _normalize() gives us data like this:
123
124        [
125            'OK: The bulk read was successful.',
126        ]
127
128        _transform() changes it to a list of tuples where the first element
129        is the status code converted into a cross-reference and the second
130        element is the description.
131
132        [
133            (':c:enumerator:`OK`', 'The bulk read was successful.'),
134        ]
135
136        This function exits if any provided status code is invalid.
137        """
138        data = []
139        valid_codes = [member.name for member in Status]
140        for item in items:
141            code = item.split(':', 1)[0]
142            desc = item.split(':', 1)[1].strip()
143            if code not in valid_codes:
144                docname = self.state.document.settings.env.docname
145                error = (
146                    f'[error] pw-status-codes found invalid code: {code}\n'
147                    f'        offending content: {item}\n'
148                    f'        included in following file: {docname}.rst\n'
149                    f'        help: {help_url}\n'
150                )
151                sys.exit(error)
152            link = f':c:enumerator:`{code}`'
153            data.append((link, desc))
154        return data
155
156    def _render(self, data: List[Tuple[str, str]]) -> List[nodes.Node]:
157        """Renders the content as a table.
158
159        _transform() gives us data like this:
160
161        [
162            (':c:enumerator:`OK`', 'The bulk read was successful.'),
163        ]
164
165        _render() structures it as a csv-table:
166
167        .. csv-table:
168           :header: "Code", "Description"
169           :align: left
170
171           ":c:enumerator:`OK`", "The bulk read was successful."
172
173        And then lets Sphinx/Docutils do the rendering. This has the added
174        bonus of making it possible to use inline reStructuredText elements
175        like code formatting and cross-references in the descriptions.
176        """
177        table = [
178            '.. csv-table::',
179            '   :header: "Code", "Description"',
180            '   :align: left',
181            '',
182        ]
183        for item in data:
184            table.append(f'   "{item[0]}", "{item[1]}"')
185        container = nodes.container()
186        container['classes'] = ['pw-status-codes']
187        # Docutils doesn't accept normal lists; it must be a ViewList.
188        # inclusive-language: disable
189        # https://www.sphinx-doc.org/en/master/extdev/markupapi.html#viewlists
190        # inclusive-language: enable
191        self.state.nested_parse(ViewList(table), self.content_offset, container)
192        return [container]
193
194
195def setup(app: Sphinx) -> dict[str, bool]:
196    """Registers the pw-status-codes directive in the Sphinx build system."""
197    app.add_directive('pw-status-codes', PwStatusCodesDirective)
198    return {
199        'parallel_read_safe': True,
200        'parallel_write_safe': True,
201    }
202