2 sphinxcontrib.httpdomain
3 ~~~~~~~~~~~~~~~~~~~~~~~~
5 The HTTP domain for documenting RESTful HTTP APIs.
7 :copyright: Copyright 2011 by Hong Minhee
8 :license: BSD, see LICENSE
for details.
14from docutils import nodes
16from pygments.lexer import RegexLexer, bygroups
17from pygments.lexers import get_lexer_by_name
18from pygments.token import Literal, Text, Operator, Keyword, Name, Number
19from pygments.util import ClassNotFound
21from sphinx import addnodes
22from sphinx.roles import XRefRole
23from sphinx.domains import Domain, ObjType, Index
24from sphinx.directives import ObjectDescription, directives
25from sphinx.util.nodes import make_refnode
26from sphinx.util.docfields import GroupedField, TypedField
28# The env.get_doctree() lookup results in a pickle.load() call which is
29# expensive enough to dominate the runtime entirely when the number of endpoints
30# and references is large enough. The doctrees are generated during the read-
31# phase and we can cache their lookup during the write-phase significantly
32# improving performance.
33# Currently sphinxcontrib-httpdomain does not declare to support parallel read
34# support (parallel_read_safe is the default False) so we can simply use a
35# module global to hold the cache.
40 """Represents a reference to an abstract specification."""
48 """Returns the URL onto related specification section for the related
54 """Represents a reference to RFC2616.
55 In 2014, RFC2616 was replaced by multiple RFCs (7230-7237)."""
58 url =
'http://www.w3.org/Protocols/rfc2616/rfc2616-sec{0:d}.html'
59 url = url.format(
int(section))
60 super(RFC2616Ref, self).
__init__(url,
'sec', section)
64 """Represents a reference to the specific IETF RFC."""
67 url =
'https://tools.ietf.org/html/rfc{0:d}'.format(rfc)
68 super(IETFRef, self).
__init__(url,
'section-', section)
74 url =
'http://www.w3.org/TR/eventsource/'
75 super(EventSourceRef, self).
__init__(url, section,
'')
79 """Represents a reference to W3 Cross-Origin Resource Sharing recommendation."""
82 url =
'http://www.w3.org/TR/cors/'
83 super(CORSRef, self).
__init__(url, name,
'-' + type)
91 'options':
IETFRef(7231,
'4.3.7'),
96 'delete':
IETFRef(7231,
'4.3.5'),
97 'trace':
IETFRef(7231,
'4.3.8'),
98 'connect':
IETFRef(7231,
'4.3.6'),
107 'Accept':
IETFRef(7231,
'5.3.2'),
108 'Accept-Charset':
IETFRef(7231,
'5.3.3'),
109 'Accept-Encoding':
IETFRef(7231,
'5.3.4'),
110 'Accept-Language':
IETFRef(7231,
'5.3.5'),
111 'Accept-Ranges':
IETFRef(7233, 2.3),
113 'Allow':
IETFRef(7231,
'7.4.1'),
114 'Authorization':
IETFRef(7235, 4.2),
115 'Cache-Control':
IETFRef(7234, 5.2),
116 'Connection':
IETFRef(7230, 6.1),
117 'Content-Encoding':
IETFRef(7231,
'3.1.2.2'),
118 'Content-Language':
IETFRef(7231,
'3.1.3.2'),
119 'Content-Length':
IETFRef(7230,
'3.3.2'),
120 'Content-Location':
IETFRef(7231,
'3.1.4.2'),
122 'Content-Range':
IETFRef(7233, 4.2),
123 'Content-Type':
IETFRef(7231,
'3.1.1.5'),
124 'Cookie':
IETFRef(2109,
'4.3.4'),
125 'Date':
IETFRef(7231,
'7.1.1.2'),
126 'Destination':
IETFRef(2518, 9.3),
128 'Expect':
IETFRef(7231,
'5.1.1'),
130 'From':
IETFRef(7231,
'5.5.2'),
132 'If-Match':
IETFRef(7232, 3.1),
133 'If-Modified-Since':
IETFRef(7232, 3.3),
134 'If-None-Match':
IETFRef(7232, 3.2),
135 'If-Range':
IETFRef(7233, 3.2),
136 'If-Unmodified-Since':
IETFRef(7232, 3.4),
138 'Last-Modified':
IETFRef(7232, 2.2),
140 'Location':
IETFRef(7231,
'7.1.2'),
141 'Max-Forwards':
IETFRef(7231,
'5.1.2'),
143 'Proxy-Authenticate':
IETFRef(7235, 4.3),
144 'Proxy-Authorization':
IETFRef(7235, 4.4),
146 'Referer':
IETFRef(7231,
'5.5.2'),
147 'Retry-After':
IETFRef(7231,
'7.1.3'),
148 'Server':
IETFRef(7231,
'7.4.2'),
149 'Set-Cookie':
IETFRef(2109,
'4.2.2'),
152 'Transfer-Encoding':
IETFRef(7230,
'3.3.1'),
154 'User-Agent':
IETFRef(7231,
'5.5.3'),
155 'Vary':
IETFRef(7231,
'7.1.4'),
158 'WWW-Authenticate':
IETFRef(7235, 4.1),
159 'Access-Control-Allow-Origin':
CORSRef(
'access-control-allow-origin',
161 'Access-Control-Allow-Credentials':
CORSRef(
'access-control-allow-credentials',
163 'Access-Control-Expose-Headers':
CORSRef(
'access-control-expose-headers',
165 'Access-Control-Max-Age':
CORSRef(
'access-control-max-age',
167 'Access-Control-Allow-Methods':
CORSRef(
'access-control-allow-methods',
169 'Access-Control-Allow-Headers':
CORSRef(
'access-control-allow-headers',
171 'Origin':
CORSRef(
'origin',
'request-header'),
172 'Access-Control-Request-Method':
CORSRef(
'access-control-request-method',
174 'Access-Control-Request-Headers':
CORSRef(
'access-control-request-headers',
181 101:
'Switching Protocols',
186 203:
'Non Authoritative Information',
188 205:
'Reset Content',
189 206:
'Partial Content',
192 300:
'Multiple Choices',
193 301:
'Moved Permanently',
198 307:
'Temporary Redirect',
199 308:
'Permanent Redirect',
202 402:
'Payment Required',
205 405:
'Method Not Allowed',
206 406:
'Not Acceptable',
207 407:
'Proxy Authentication Required',
208 408:
'Request Timeout',
211 411:
'Length Required',
212 412:
'Precondition Failed',
213 413:
'Request Entity Too Large',
214 414:
'Request URI Too Long',
215 415:
'Unsupported Media Type',
216 416:
'Requested Range Not Satisfiable',
217 417:
'Expectation Failed',
219 422:
'Unprocessable Entity',
221 424:
'Failed Dependency',
223 426:
'Upgrade Required',
224 429:
'Too Many Requests',
226 451:
'Unavailable For Legal Reasons',
227 500:
'Internal Server Error',
228 501:
'Not Implemented',
230 503:
'Service Unavailable',
231 504:
'Gateway Timeout',
232 505:
'HTTP Version Not Supported',
233 507:
'Insufficient Storage',
237WEBDAV_STATUS_CODES = [207, 422, 423, 424, 507]
239http_sig_param_re = re.compile(
r'\((?:(?P<type>[^:)]+):)?(?P<name>[\w_]+)\)',
245 order = [
'HEAD',
'GET',
'POST',
'PUT',
'DELETE',
'PATCH',
246 'OPTIONS',
'TRACE',
'CONNECT',
'COPY',
'ANY']
247 method = item[0].split(
' ', 1)[0]
249 return order.index(method)
251 return sorted(entries, key=cmp)
255 path = re.sub(
r'[{}]',
'', re.sub(
r'[<>:/]',
'-', path))
256 return method.lower() +
'-' + path
262 TypedField(
'parameter', label=
'Parameters',
263 names=(
'param',
'parameter',
'arg',
'argument'),
264 typerolename=
'obj', typenames=(
'paramtype',
'type')),
265 TypedField(
'jsonparameter', label=
'JSON Parameters',
266 names=(
'jsonparameter',
'jsonparam',
'json'),
267 typerolename=
'obj', typenames=(
'jsonparamtype',
'jsontype')),
268 TypedField(
'requestjsonobject', label=
'Request JSON Object',
269 names=(
'reqjsonobj',
'reqjson',
'<jsonobj',
'<json'),
270 typerolename=
'obj', typenames=(
'reqjsonobj',
'<jsonobj')),
271 TypedField(
'requestjsonarray', label=
'Request JSON Array of Objects',
272 names=(
'reqjsonarr',
'<jsonarr'),
274 typenames=(
'reqjsonarrtype',
'<jsonarrtype')),
275 TypedField(
'responsejsonobject', label=
'Response JSON Object',
276 names=(
'resjsonobj',
'resjson',
'>jsonobj',
'>json'),
277 typerolename=
'obj', typenames=(
'resjsonobj',
'>jsonobj')),
278 TypedField(
'responsejsonarray', label=
'Response JSON Array of Objects',
279 names=(
'resjsonarr',
'>jsonarr'),
281 typenames=(
'resjsonarrtype',
'>jsonarrtype')),
282 TypedField(
'queryparameter', label=
'Query Parameters',
283 names=(
'queryparameter',
'queryparam',
'qparam',
'query'),
285 typenames=(
'queryparamtype',
'querytype',
'qtype')),
286 GroupedField(
'formparameter', label=
'Form Parameters',
287 names=(
'formparameter',
'formparam',
'fparam',
'form')),
288 GroupedField(
'requestheader', label=
'Request Headers',
290 names=(
'<header',
'reqheader',
'requestheader')),
291 GroupedField(
'responseheader', label=
'Response Headers',
293 names=(
'>header',
'resheader',
'responseheader')),
294 GroupedField(
'statuscode', label=
'Status Codes',
295 rolename=
'statuscode',
296 names=(
'statuscode',
'status',
'code'))
300 'deprecated': directives.flag,
301 'noindex': directives.flag,
302 'synopsis':
lambda x: x,
305 method = NotImplemented
308 method = self.
method.upper() +
' '
309 signode += addnodes.desc_name(method, method)
312 for match
in http_sig_param_re.finditer(sig):
313 path = sig[offset:match.start()]
314 signode += addnodes.desc_name(path, path)
315 params = addnodes.desc_parameterlist()
316 typ = match.group(
'type')
319 params += addnodes.desc_annotation(typ, typ)
320 name = match.group(
'name')
321 params += addnodes.desc_parameter(name, name)
324 if offset < len(sig):
325 path = sig[offset:len(sig)]
326 signode += addnodes.desc_name(path, path)
327 assert path
is not None,
'no matches for sig: %s' % sig
328 fullname = self.
method.upper() +
' ' + path
329 signode[
'method'] = self.
method
330 signode[
'path'] = sig
331 signode[
'fullname'] = fullname
332 return (fullname, self.
method, sig)
339 if 'noindex' not in self.options:
340 self.env.domaindata[
'http'][self.
method][sig] = (
342 self.options.
get(
'synopsis',
''),
343 'deprecated' in self.options)
407 XRefRole.__init__(self, **kwargs)
410 def process_link(self, env, refnode, has_explicit_title, title, target):
411 if not has_explicit_title:
412 title = self.
method.upper() +
' ' + title
419 method = node[0][0].lower()
420 rawsource = node[0].rawsource
421 config = env.domains[
'http'].env.config
422 if method
not in METHOD_REFS:
423 if not config[
'http_strict_mode']:
424 return [nodes.emphasis(method, method)], []
425 reporter = document.reporter
426 msg = reporter.error(
'%s is not valid HTTP method' % method,
428 prb = nodes.problematic(method, method)
430 url = str(METHOD_REFS[method])
432 return [nodes.emphasis(method, method)], []
433 node = nodes.reference(rawsource, method.upper(), refuri=url)
440 def get_code_status(text):
443 return code, HTTP_STATUS_CODES.get(code)
446 code, status = re.split(
r'\s', text.strip(), 1)
450 known_status = HTTP_STATUS_CODES.get(code)
451 if known_status
is None:
453 elif known_status.lower() != status.lower():
458 def report_unknown_code():
459 if not config[
'http_strict_mode']:
460 return [nodes.emphasis(text, text)], []
461 reporter = document.reporter
462 msg = reporter.error(
'%d is unknown HTTP status code' % code,
464 prb = nodes.problematic(text, text)
467 def report_invalid_code():
468 if not config[
'http_strict_mode']:
469 return [nodes.emphasis(text, text)], []
470 reporter = document.reporter
471 msg = reporter.error(
472 'HTTP status code must be an integer (e.g. `200`) or '
473 'start with an integer (e.g. `200 OK`); %r is invalid' %
477 prb = nodes.problematic(text, text)
481 rawsource = node[0].rawsource
482 config = env.domains[
'http'].env.config
484 code, status = get_code_status(text)
486 return report_invalid_code()
488 return report_unknown_code()
490 url =
'http://www.ietf.org/rfc/rfc3229.txt'
492 url =
'http://www.ietf.org/rfc/rfc2324.txt'
494 url =
'http://tools.ietf.org/html/rfc6585#section-4'
496 url =
'http://msdn.microsoft.com/en-us/library/dd891478(v=prot.10).aspx'
498 url =
'http://www.ietf.org/rfc/rfc7725.txt'
499 elif code
in WEBDAV_STATUS_CODES:
500 url =
'http://tools.ietf.org/html/rfc4918#section-11.%d' % (WEBDAV_STATUS_CODES.index(code) + 1)
501 elif code
in HTTP_STATUS_CODES:
502 url =
'http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html' \
503 '#sec10.' + (
'%d.%d' % (code // 100, 1 + code % 100))
506 node = nodes.reference(rawsource,
'%d %s' % (code, status), refuri=url)
514 rawsource = node[0].rawsource
515 if header
not in HEADER_REFS:
516 _header =
'-'.join(
map(
lambda i: i.title(), header.split(
'-')))
517 if _header
not in HEADER_REFS:
518 return [nodes.emphasis(header, header)], []
519 url = str(HEADER_REFS[header])
520 node = nodes.reference(rawsource, header, refuri=url)
526 name =
'routingtable'
527 localname =
'HTTP Routing Table'
528 shortname =
'routing table'
531 super(HTTPIndex, self).
__init__(*args, **kwargs)
534 [l
for l
in x.split(
'/')
if l]
535 for x
in self.domain.env.config[
'http_index_ignore_prefixes']]
536 self.
ignore.sort(reverse=
True)
545 letters = [x
for x
in path.split(
'/')
if x]
546 for prefix
in self.
ignore:
547 if letters[:len(prefix)] == prefix:
548 return '/' +
'/'.join(letters[:len(prefix) + 1])
549 return '/%s' % (letters[0]
if letters
else '',)
553 items = ((method, path, info)
554 for method, routes
in self.domain.routes.items()
555 for path, info
in routes.items())
556 items = sorted(items, key=
lambda item: item[1])
557 for method, path, info
in items:
560 method.upper() +
' ' + path, 0, info[0],
562 '',
'Deprecated' if info[2]
else '', info[1]
566 for path, entries
in content.items()
578 'options': ObjType(
'options',
'options',
'obj'),
579 'head': ObjType(
'head',
'head',
'obj'),
580 'post': ObjType(
'post',
'post',
'obj'),
581 'get': ObjType(
'get',
'get',
'obj'),
582 'put': ObjType(
'put',
'put',
'obj'),
583 'patch': ObjType(
'patch',
'patch',
'obj'),
584 'delete': ObjType(
'delete',
'delete',
'obj'),
585 'trace': ObjType(
'trace',
'trace',
'obj'),
586 'connect': ObjType(
'connect',
'connect',
'obj'),
587 'copy': ObjType(
'copy',
'copy',
'obj'),
588 'any': ObjType(
'any',
'any',
'obj')
592 'options': HTTPOptions,
598 'delete': HTTPDelete,
600 'connect': HTTPConnect,
640 return dict((key, self.data[key])
for key
in self.
object_types)
644 for path, info
in list(routes.items()):
645 if info[0] == docname:
651 info = self.data[str(typ)][target]
653 text = contnode.rawsource
658 if fromdocname
not in _doctree_cache:
659 _doctree_cache[fromdocname] = env.get_doctree(fromdocname)
660 doctree = _doctree_cache[fromdocname]
662 resnode = role.result_nodes(doctree, env, node,
None)[0][0]
663 if isinstance(resnode, addnodes.pending_xref):
665 reporter = doctree.reporter
666 reporter.warning(
'Cannot resolve reference to %r' % text,
672 title = typ.upper() +
' ' + target
673 return make_refnode(builder, fromdocname, info[0], anchor,
677 """Resolve the pending_xref *node* with the given *target*.
679 The reference comes from an
"any" or similar role, which means that Sphinx
682 For now sphinxcontrib-httpdomain doesn't resolve any xref nodes.
685 list of tuples ``(
'domain:role', newnode)``, where ``
'domain:role'``
686 is the name of a role that could have created the same reference,
692 for path, info
in routes.items():
694 yield (path, path, method, info[0], anchor, 1)
698 """Lexer for HTTP sessions."""
706 if match.group(1).lower() ==
'content-type':
707 content_type = match.group(5).strip()
708 if ';' in content_type:
709 content_type = content_type[:content_type.find(
';')].strip()
711 yield match.start(1), Name.Attribute, match.group(1)
712 yield match.start(2), Text, match.group(2)
713 yield match.start(3), Operator, match.group(3)
714 yield match.start(4), Text, match.group(4)
715 yield match.start(5), Literal, match.group(5)
716 yield match.start(6), Text, match.group(6)
719 yield match.start(1), Text, match.group(1)
720 yield match.start(2), Literal, match.group(2)
721 yield match.start(3), Text, match.group(3)
724 content_type = getattr(self,
'content_type',
None)
725 content = match.group()
726 offset = match.start()
728 from pygments.lexers
import get_lexer_for_mimetype
730 lexer = get_lexer_for_mimetype(content_type)
731 except ClassNotFound:
734 for idx, token, value
in lexer.get_tokens_unprocessed(content):
735 yield offset + idx, token, value
737 yield offset, Text, content
741 (
r'(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE|COPY)( +)([^ ]+)( +)'
742 r'(HTTPS?)(/)(1\.[01])(\r?\n|$)',
743 bygroups(Name.Function, Text, Name.Namespace, Text,
744 Keyword.Reserved, Operator, Number, Text),
746 (
r'(HTTPS?)(/)(1\.[01])( +)(\d{3})( +)([^\r\n]+)(\r?\n|$)',
747 bygroups(Keyword.Reserved, Operator, Number, Text, Number,
748 Text, Name.Exception, Text),
752 (
r'([^\s:]+)( *)(:)( *)([^\r\n]+)(\r?\n|$)', header_callback),
753 (
r'([\t ]+)([^\r\n]+)(\r?\n|$)', continuous_header_callback),
754 (
r'\r?\n', Text,
'content')
757 (
r'.+', content_callback)
763 app.add_domain(HTTPDomain)
766 get_lexer_by_name(
'http')
767 except ClassNotFound:
769 app.add_config_value(
'http_index_ignore_prefixes', [],
None)
770 app.add_config_value(
'http_index_shortname',
'routing table',
True)
771 app.add_config_value(
'http_index_localname',
'HTTP Routing Table',
True)
772 app.add_config_value(
'http_strict_mode',
True,
None)
773 app.add_config_value(
'http_headers_ignore_prefixes', [
'X-'],
None)
def __init__(self, name, type)
def __init__(self, base_url, anchor, section)
def __init__(self, section)
def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode)
def clear_doc(self, docname)
def resolve_any_xref(self, env, fromdocname, builder, target, node, contnode)
def __init__(self, *args, **kwargs)
def generate(self, docnames=None)
def grouping_prefix(self, path)
def header_callback(self, match)
def continuous_header_callback(self, match)
def content_callback(self, match)
def get_index_text(self, modname, name)
def handle_signature(self, sig, signode)
def add_target_and_index(self, name_cls, sig, signode)
def result_nodes(self, document, env, node, is_ref)
def result_nodes(self, document, env, node, is_ref)
def process_link(self, env, refnode, has_explicit_title, title, target)
def __init__(self, method, **kwargs)
def result_nodes(self, document, env, node, is_ref)
def __init__(self, rfc, section)
def __init__(self, section)
static int list
Set if we should print a list of currently running services.
static int get
Get DID Documement for DID Flag.
def sort_by_method(entries)
def http_resource_anchor(method, path)
static struct GNUNET_CONTAINER_MultiPeerMap * map
Peermap of PeerIdentities to "struct PeerEntry" (for fast lookup).