Automatically generate API documentation.

Needs CSS love and some rewriting of the docstrings that are now
exposed, but otherwise complete.
This commit is contained in:
Dave Pifke
2012-03-15 20:58:33 +00:00
committed by Max Goodman
parent f1dc6548f8
commit fb0b650490
8 changed files with 148 additions and 3 deletions

View File

@@ -204,6 +204,7 @@ def make_map(global_conf={}, app_conf={}):
mc('/post/:action', controller='post',
requirements=dict(action="options|over18|unlogged_options|optout|optin|login|reg"))
mc('/api', controller='apihelp', action='help')
mc('/api/distinguish/:how', controller='api', action="distinguish")
# wherever this is, google has to agree.
mc('/api/gcheckout', controller='ipn', action='gcheckout')

View File

@@ -59,7 +59,7 @@ try:
except ImportError:
from api import ApiController
from api import ApiminimalController
from api import ApiminimalController, ApihelpController
from apiv1 import APIv1Controller as Apiv1Controller
from oauth2 import OAuth2FrontendController as Oauth2frontendController
from oauth2 import OAuth2AccessController as Oauth2accessController

View File

@@ -34,7 +34,7 @@ from r2.lib.utils import query_string, timefromnow, randstr
from r2.lib.utils import timeago, tup, filter_links
from r2.lib.pages import EnemyList, FriendList, ContributorList, ModList, \
BannedList, BoringPage, FormPage, CssError, UploadedImage, ClickGadget, \
UrlParser, WrappedUser
UrlParser, WrappedUser, ApiHelp, BoringPage
from r2.lib.pages import FlairList, FlairCsv, FlairTemplateEditor, \
FlairSelector
from r2.lib.utils.trial_utils import indict, end_trial, trial_info
@@ -2494,3 +2494,52 @@ class ApiController(RedditController):
self.enable_admin_mode(c.user)
form.redirect(dest)
class ApihelpController(RedditController):
@staticmethod
def docs_from_controller(controller, url_prefix='/api'):
api_methods = defaultdict(dict)
for name, func in controller.__dict__.iteritems():
i = name.find('_')
if i > 0:
method = name[:i]
action = name[i+1:]
else:
continue
if func.__doc__ and method in ('GET', 'POST'):
docs = func.__doc__.strip()
if hasattr(func, 'oauth2_perms'):
scopes = func.oauth2_perms.get('allowed_scopes')
if scopes:
docs = '*OAuth2 scope(s): %s*\n\n%s' % (
', '.join([
('[%s](#oauth2_scope_%s)' % (scope, scope))
for scope in scopes
]),
docs,
)
api_methods['/'.join((url_prefix, action))][method] = docs
return api_methods
def GET_help(self):
from r2.controllers.apiv1 import APIv1Controller
from r2.controllers.oauth2 import OAuth2FrontendController, OAuth2AccessController, scope_info
api_methods = defaultdict(dict)
for controller, url_prefix in ((ApiController, '/api'),
(ApiminimalController, '/api'),
(OAuth2FrontendController, '/api/v1'),
(OAuth2AccessController, '/api/v1'),
(APIv1Controller, '/api/v1')):
for url, methods in self.docs_from_controller(controller, url_prefix).iteritems():
api_methods[url].update(methods)
return BoringPage(
_('api documentation'),
content=ApiHelp(
api_methods=api_methods,
oauth2_scopes=scope_info,
)
).render()

View File

@@ -29,5 +29,6 @@ class APIv1Controller(OAuth2ResourceController):
@require_oauth2_scope("identity")
def GET_me(self):
"""Returns the identity of the user currently authenticated via OAuth."""
resp = IdentityJsonTemplate().data(c.oauth_user)
return self.api_wrapper(resp)

View File

@@ -89,6 +89,8 @@ class OAuth2FrontendController(RedditController):
scope = VOneOf("scope", scope_info.keys()),
state = VRequired("state", errors.NO_TEXT))
def GET_authorize(self, response_type, client, redirect_uri, scope, state):
"""Endpoint for OAuth2 authorization."""
self._check_redirect_uri(client, redirect_uri)
resp = {}
@@ -107,6 +109,8 @@ class OAuth2FrontendController(RedditController):
state = VRequired("state", errors.NO_TEXT),
authorize = VRequired("authorize", errors.OAUTH2_ACCESS_DENIED))
def POST_authorize(self, authorize, client, redirect_uri, scope, state):
"""Endpoint for OAuth2 authorization."""
self._check_redirect_uri(client, redirect_uri)
resp = {}

View File

@@ -139,6 +139,9 @@ def validate(*simple_vals, **param_vals):
return self.intermediate_redirect('/login')
except VerifiedUserRequiredException:
return self.intermediate_redirect('/verify')
newfn.__name__ = fn.__name__
newfn.__doc__ = fn.__doc__
return newfn
return val
@@ -183,11 +186,14 @@ def api_validate(response_type=None):
except VerifiedUserRequiredException:
responder.send_failure(errors.VERIFIED_USER_REQUIRED)
return self.api_wrapper(responder.make_response())
newfn.__name__ = fn.__name__
newfn.__doc__ = fn.__doc__
return newfn
return val
return _api_validate
return wrap
@api_validate("html")
def noresponse(self, self_method, responder, simple_vals, param_vals, *a, **kw):

View File

@@ -3782,3 +3782,9 @@ class UserIPHistory(Templated):
def __init__(self):
self.ips = ips_by_account_id(c.user._id)
super(UserIPHistory, self).__init__()
class ApiHelp(Templated):
def __init__(self, api_methods, oauth2_scopes, *a, **kw):
self.api_methods = api_methods
self.oauth2_scopes = oauth2_scopes
super(ApiHelp, self).__init__(*a, **kw)

View File

@@ -0,0 +1,78 @@
<%!
from r2.lib.filters import safemarkdown
%>
<div class="content apihelp">
<div class="introduction">
<p>This is the automatically-generated documentation for the Reddit API.</p>
<p>It's gathered from the docstrings in the code.</p>
</div>
<div class="contents">
<h1>Contents</h1>
<ul>
<li>
<strong>API methods</strong>
<ul>
%for uri in sorted(thing.api_methods.keys()):
<li>
<a href="#api_method_${uri.split('/')[-1]}">${uri}</a>
&nbsp; <span class="gray">(${', '.join(sorted(thing.api_methods[uri].keys()))})</span>
</li>
%endfor
</ul>
<li>
<strong>OAuth scopes</strong>
<ul>
%for scope in sorted(thing.oauth2_scopes.keys()):
<li><a href="#oauth2_scope_${scope}">${scope}</a></li>
%endfor
</ul>
</li>
</ul>
</div>
<div class="methods">
<h1>API methods</h1>
%for uri in sorted(thing.api_methods.keys()):
<div class="endpoint">
<a name="api_method_${uri.split('/')[-1]}"></a>
<h2>${uri}</h2>
%for method in sorted(thing.api_methods[uri].keys()):
<div class="method">
<h3>${method}</h3>
${unsafe(safemarkdown(thing.api_methods[uri][method]))}
</div>
%endfor
</div>
%endfor
</div>
<div class="apihelp scopes">
<h1>OAuth scopes</h1>
<table class="oauth2-scopes">
<thead>
<tr>
<th>id</th>
<th>name</th>
<th>description</th>
</tr>
</thead>
<tbody>
%for scope in sorted(thing.oauth2_scopes.keys()):
<tr>
<td class="oauth2-scope-id">
<a name="oauth2_scope_${scope}"></a>
${scope}
</td>
<td class="oauth2-scope-name">${thing.oauth2_scopes[scope]['name']}</td>
<td class="oauth2-scope-description">${thing.oauth2_scopes[scope]['description']}</td>
</tr>
%endfor
</tbody>
</table>
</div>
</div>