HTTP Response Code Table

Change-Id: Ie65366c1f5cb76af50ce116c1bb8747ed610f103
This commit is contained in:
Graham Hayes 2016-05-18 18:54:20 +01:00
parent 1785afb8b3
commit fa30156fd6
6 changed files with 465 additions and 0 deletions

View File

@ -182,6 +182,114 @@ max_version
a *Deprecated in $version* stanza in the html output.
rest_status_code
----------------
The ``rest_status_code`` stanza is how you can show what HTTP status codes your
API uses and what they indicate
.. code-block:: rst
.. rest_status_code:: <sucess|failure> <location of status.yaml file>
This stanza should be the first element after the narrative section of the
method description.
An example from the Designate documentation is:
.. code-block:: rst
:emphasize-lines: 11-25
Create Zone
===========
.. rest_method:: POST /v2/zones
Create a zone
Response codes
--------------
.. rest_status_code:: success status.yaml
- 200
- 100
- 201
.. rest_status_code:: error status.yaml
- 405
- 403
- 401
- 400
- 500
- 409: duplcate_zone
And corresponding entries in ``status.yaml``:
.. code-block:: yaml
100:
default: |
An unusual code for an API
200:
default: |
Request was successful.
201:
default: >
Resource was created and is ready to use. The ``Location`` header
will have the URL to the new item
400:
default: |
Some content in the request was invalid
zone_data_error: |
Some of the data for the
401:
default: |
User must authenticate before making a request
403:
default: |
Policy does not allow current user to do this operation.
405:
default: |
Method is not valid for this endpoint. Not all endpoints allow all HTTP methods.
409:
default: |
This operation conflicted with another operation on this resource
duplcate_zone: |
There is already a zone with this name.
500:
default: |
Something went wrong inside the service.
This will create a 2 tables of response codes, one for success and one for
errors.
status file format
------------------
This is a simple yaml file, with a single object of status codes and the
reasons that each would be used.
Each status code **must** have a default entry. This is used when a code is
used in a ``rest_status_code`` stanza with no value.
There may be situations where the reason for a code may be different across
endpoints, or a different message may be appropriate.
In this case, adding a entry at the same level as the ``default`` and
referencing that in the stanaza like so:
.. code-block:: yaml
- 409: duplcate_zone
This will overide the default message with the newly defined one.
rest_expand_all
---------------

View File

@ -23,6 +23,9 @@ from sphinx.util.compat import Directive
from sphinx.util.osutil import copyfile
import yaml
from os_api_ref.http_codes import http_code
from os_api_ref.http_codes import http_code_html
from os_api_ref.http_codes import HTTPResponseCodeDirective
__version__ = pbr.version.VersionInfo(
'os_api_ref').version_string()
@ -532,11 +535,14 @@ def setup(app):
html=(rest_method_html, None))
app.add_node(rest_expand_all,
html=(rest_expand_all_html, None))
app.add_node(http_code,
html=(http_code_html, None))
# This specifies all our directives that we're adding
app.add_directive('rest_parameters', RestParametersDirective)
app.add_directive('rest_method', RestMethodDirective)
app.add_directive('rest_expand_all', RestExpandAllDirective)
app.add_directive('rest_status_code', HTTPResponseCodeDirective)
# The doctree-read hook is used do the slightly crazy doc
# transformation that we do to get the rest_method document

232
os_api_ref/http_codes.py Normal file
View File

@ -0,0 +1,232 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from docutils import nodes
from docutils.parsers.rst.directives.tables import Table
from docutils.statemachine import ViewList
from httplib import responses
import yaml
# cache for file -> yaml so we only do the load and check of a yaml
# file once during a sphinx processing run.
HTTP_YAML_CACHE = {}
class HTTPResponseCodeDirective(Table):
headers = ["Code", "Reason"]
status_types = ("success", "error")
# This is for HTTP response codes that OpenStack may use that are not part
# the httplib response dict.
CODES = {
429: "Too Many Requests",
}
required_arguments = 2
def __init__(self, *args, **kwargs):
self.CODES.update(responses)
super(HTTPResponseCodeDirective, self).__init__(*args, **kwargs)
def _load_status_file(self, fpath):
global HTTP_YAML_CACHE
if fpath in HTTP_YAML_CACHE:
return HTTP_YAML_CACHE[fpath]
# self.app.info("Fpath: %s" % fpath)
try:
with open(fpath, 'r') as stream:
lookup = yaml.load(stream)
except IOError:
self.env.warn(
self.env.docname,
"Parameters file %s not found" % fpath)
return
except yaml.YAMLError as exc:
self.app.warn(exc)
raise
HTTP_YAML_CACHE[fpath] = lookup
return lookup
def run(self):
self.env = self.state.document.settings.env
self.app = self.env.app
# Make sure we have some content, which should be yaml that
# defines some parameters.
if not self.content:
error = self.state_machine.reporter.error(
'No parameters defined',
nodes.literal_block(self.block_text, self.block_text),
line=self.lineno)
return [error]
if not len(self.arguments) >= 2:
error = self.state_machine.reporter.error(
'%s' % self.arguments,
nodes.literal_block(self.block_text, self.block_text),
line=self.lineno)
return [error]
_, status_defs_file = self.env.relfn2path(self.arguments.pop())
status_type = self.arguments.pop()
self.status_defs = self._load_status_file(status_defs_file)
# self.app.info("%s" % str(self.status_defs))
if status_type not in self.status_types:
error = self.state_machine.reporter.error(
'Type %s is not one of %s' % (status_type, self.status_types),
nodes.literal_block(self.block_text, self.block_text),
line=self.lineno)
return [error]
self.yaml = self._load_codes()
self.max_cols = len(self.headers)
# TODO(sdague): it would be good to dynamically set column
# widths (or basically make the colwidth thing go away
# entirely)
self.options['widths'] = (30, 70)
self.col_widths = self.get_column_widths(self.max_cols)
# Actually convert the yaml
title, messages = self.make_title()
# self.app.info("Title %s, messages %s" % (title, messages))
table_node = self.build_table()
self.add_name(table_node)
title_block = nodes.title(
text=status_type.capitalize())
section = nodes.section(ids=title_block)
section += title_block
section += table_node
return [section] + messages
def _load_codes(self):
content = "\n".join(self.content)
parsed = yaml.load(content)
new_content = list()
for item in parsed:
if isinstance(item, int):
new_content.append((item, self.status_defs[item]['default']))
else:
try:
for code, reason in item.items():
new_content.append(
(code, self.status_defs[code][reason])
)
except KeyError:
self.app.warn(
"Could not find %s for code %s" % (reason, code))
new_content.append(
(code, self.status_defs[code]['default']))
return new_content
def build_table(self):
table = nodes.table()
tgroup = nodes.tgroup(cols=len(self.headers))
table += tgroup
# TODO(sdague): it would be really nice to figure out how not
# to have this stanza, it kind of messes up all of the table
# formatting because it doesn't let tables just be the right
# size.
tgroup.extend(
nodes.colspec(colwidth=col_width, colname='c' + str(idx))
for idx, col_width in enumerate(self.col_widths)
)
thead = nodes.thead()
tgroup += thead
row_node = nodes.row()
thead += row_node
row_node.extend(nodes.entry(h, nodes.paragraph(text=h))
for h in self.headers)
tbody = nodes.tbody()
tgroup += tbody
rows, groups = self.collect_rows()
tbody.extend(rows)
table.extend(groups)
return table
def add_col(self, node):
entry = nodes.entry()
entry.append(node)
return entry
def add_desc_col(self, value):
entry = nodes.entry()
result = ViewList(value.split('\n'))
self.state.nested_parse(result, 0, entry)
return entry
def collect_rows(self):
rows = []
groups = []
try:
# self.app.info("Parsed content is: %s" % self.yaml)
for code, desc in self.yaml:
h_code = http_code()
h_code['code'] = code
h_code['title'] = self.CODES.get(code, 'Unknown')
trow = nodes.row()
trow += self.add_col(h_code)
trow += self.add_desc_col(desc)
rows.append(trow)
except AttributeError as exc:
# if 'key' in locals():
self.app.warn("Failure on key: %s, values: %s. %s" %
(code, desc, exc))
# else:
# rows.append(self.show_no_yaml_error())
return rows, groups
def http_code_html(self, node):
tmpl = "<code>%(code)s - %(title)s</code>"
self.body.append(tmpl % node)
raise nodes.SkipNode
class http_code(nodes.Part, nodes.Element):
"""Node for http_code stanza
Because we need to insert very specific HTML at the final stage of
processing, the http_code stanza needs a custom node type. This
lets us accumulate the relevant data into this node, during
parsing, but not turn it into known sphinx types (lists, tables,
sections).
Then, during the final build phase we transform directly to the
html that we want.
NOTE: this means we error trying to build latex or man pages for
these stanza types right now. This is all fixable if we add an
output formatter for this node type, but it's not yet a
priority. Contributions welcomed.
"""
pass

View File

@ -11,3 +11,22 @@ I am text, hear me roar!
.. rest_parameters:: parameters.yaml
- name: name
Response codes
--------------
.. rest_status_code:: success status.yaml
- 200
- 100
- 201
.. rest_status_code:: error status.yaml
- 405
- 403
- 401
- 400
- 500
- 409: duplcate_zone

View File

@ -0,0 +1,39 @@
#################
# Success Codes #
#################
100:
default: |
An unusual code for an API
200:
default: |
Request was successful.
201:
default: |
Resource was created and is ready to use.
#################
# Error Codes #
#################
400:
default: |
Some content in the request was invalid
zone_data_error: |
Some of the data for the
401:
default: |
User must authenticate before making a request
403:
default: |
Policy does not allow current user to do this operation.
405:
default: |
Method is not valid for this endpoint.
409:
default: |
This operation conflicted with another operation on this resource
duplcate_zone: |
There is already a zone with this name.
500:
default: |
Something went wrong inside the service.

View File

@ -102,3 +102,64 @@ class TestBasicExample(base.TestCase):
</table>"""
self.assertIn(table, self.content)
def test_rest_response(self):
success_table = """table border="1" class="docutils">
<colgroup>
<col width="30%"></col>
<col width="70%"></col>
</colgroup>
<thead valign="bottom">
<tr class="row-odd"><th class="head">Code</th>
<th class="head">Reason</th>
</tr>
</thead>
<tbody valign="top">
<tr class="row-even"><td><code>200 - OK</code></td>
<td>Request was successful.</td>
</tr>
<tr class="row-odd"><td><code>100 - Continue</code></td>
<td>An unusual code for an API</td>
</tr>
<tr class="row-even"><td><code>201 - Created</code></td>
<td>Resource was created and is ready to use.</td>
</tr>
</tbody>
</table>
"""
error_table = """<table border="1" class="docutils">
<colgroup>
<col width="30%"></col>
<col width="70%"></col>
</colgroup>
<thead valign="bottom">
<tr class="row-odd"><th class="head">Code</th>
<th class="head">Reason</th>
</tr>
</thead>
<tbody valign="top">
<tr class="row-even"><td><code>405 - Method Not Allowed</code></td>
<td>Method is not valid for this endpoint.</td>
</tr>
<tr class="row-odd"><td><code>403 - Forbidden</code></td>
<td>Policy does not allow current user to do this operation.</td>
</tr>
<tr class="row-even"><td><code>401 - Unauthorized</code></td>
<td>User must authenticate before making a request</td>
</tr>
<tr class="row-odd"><td><code>400 - Bad Request</code></td>
<td>Some content in the request was invalid</td>
</tr>
<tr class="row-even"><td><code>500 - Internal Server Error</code></td>
<td>Something went wrong inside the service.</td>
</tr>
<tr class="row-odd"><td><code>409 - Conflict</code></td>
<td>There is already a zone with this name.</td>
</tr>
</tbody>
</table>
"""
self.assertIn(success_table, self.content)
self.assertIn(error_table, self.content)