HTTP Response Code Table
Change-Id: Ie65366c1f5cb76af50ce116c1bb8747ed610f103
This commit is contained in:
parent
1785afb8b3
commit
fa30156fd6
|
@ -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
|
||||
---------------
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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.
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue