diff --git a/rhodecode/api/__init__.py b/rhodecode/api/__init__.py
--- a/rhodecode/api/__init__.py
+++ b/rhodecode/api/__init__.py
@@ -22,6 +22,7 @@ import inspect
import itertools
import logging
import types
+import fnmatch
import decorator
import venusian
@@ -47,6 +48,18 @@ DEFAULT_RENDERER = 'jsonrpc_renderer'
DEFAULT_URL = '/_admin/apiv2'
+def find_methods(jsonrpc_methods, pattern):
+ matches = OrderedDict()
+ if not isinstance(pattern, (list, tuple)):
+ pattern = [pattern]
+
+ for single_pattern in pattern:
+ for method_name, method in jsonrpc_methods.items():
+ if fnmatch.fnmatch(method_name, single_pattern):
+ matches[method_name] = method
+ return matches
+
+
class ExtJsonRenderer(object):
"""
Custom renderer that mkaes use of our ext_json lib
@@ -143,7 +156,19 @@ def exception_view(exc, request):
log.debug('json-rpc method `%s` not found in list of '
'api calls: %s, rpc_id:%s',
method, request.registry.jsonrpc_methods.keys(), rpc_id)
- fault_message = "No such method: {}".format(method)
+
+ similar = 'none'
+ try:
+ similar_paterns = ['*{}*'.format(x) for x in method.split('_')]
+ similar_found = find_methods(
+ request.registry.jsonrpc_methods, similar_paterns)
+ similar = ', '.join(similar_found.keys()) or similar
+ except Exception:
+ # make the whole above block safe
+ pass
+
+ fault_message = "No such method: {}. Similar methods: {}".format(
+ method, similar)
return jsonrpc_error(request, fault_message, rpc_id)
@@ -348,9 +373,10 @@ class RoutePredicate(object):
class NotFoundPredicate(object):
def __init__(self, val, config):
self.val = val
+ self.methods = config.registry.jsonrpc_methods
def text(self):
- return 'jsonrpc method not found = %s' % self.val
+ return 'jsonrpc method not found = {}.'.format(self.val)
phash = text
diff --git a/rhodecode/api/tests/test_api.py b/rhodecode/api/tests/test_api.py
--- a/rhodecode/api/tests/test_api.py
+++ b/rhodecode/api/tests/test_api.py
@@ -80,7 +80,13 @@ class TestApi(object):
def test_api_non_existing_method(self, request):
id_, params = build_data(self.apikey, 'not_existing', args='xx')
response = api_call(self.app, params)
- expected = 'No such method: not_existing'
+ expected = 'No such method: not_existing. Similar methods: none'
+ assert_error(id_, expected, given=response.body)
+
+ def test_api_non_existing_method_have_similar(self, request):
+ id_, params = build_data(self.apikey, 'comment', args='xx')
+ response = api_call(self.app, params)
+ expected = 'No such method: comment. Similar methods: changeset_comment, comment_pull_request, comment_commit'
assert_error(id_, expected, given=response.body)
def test_api_disabled_user(self, request):
diff --git a/rhodecode/api/tests/test_get_method.py b/rhodecode/api/tests/test_get_method.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/api/tests/test_get_method.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2010-2017 RhodeCode GmbH
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License, version 3
+# (only), as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# This program is dual-licensed. If you wish to learn more about the
+# RhodeCode Enterprise Edition, including its added features, Support services,
+# and proprietary license terms, please see https://rhodecode.com/licenses/
+
+
+import pytest
+
+from rhodecode.api.tests.utils import build_data, api_call, assert_ok
+
+
+@pytest.mark.usefixtures("testuser_api", "app")
+class TestGetMethod(object):
+ def test_get_methods_no_matches(self):
+ id_, params = build_data(self.apikey, 'get_method', pattern='hello')
+ response = api_call(self.app, params)
+
+ expected = []
+ assert_ok(id_, expected, given=response.body)
+
+ def test_get_methods(self):
+ id_, params = build_data(self.apikey, 'get_method', pattern='*comment*')
+ response = api_call(self.app, params)
+
+ expected = ['changeset_comment', 'comment_pull_request',
+ 'comment_commit']
+ assert_ok(id_, expected, given=response.body)
+
+ def test_get_methods_on_single_match(self):
+ id_, params = build_data(self.apikey, 'get_method', pattern='*comment_commit*')
+ response = api_call(self.app, params)
+
+ expected = ['comment_commit',
+ {'apiuser': '',
+ 'comment_type': "",
+ 'commit_id': '',
+ 'message': '',
+ 'repoid': '',
+ 'request': '',
+ 'resolves_comment_id': '',
+ 'status': '',
+ 'userid': '>'}]
+ assert_ok(id_, expected, given=response.body)
diff --git a/rhodecode/api/views/server_api.py b/rhodecode/api/views/server_api.py
--- a/rhodecode/api/views/server_api.py
+++ b/rhodecode/api/views/server_api.py
@@ -18,10 +18,12 @@
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
-
+import inspect
import logging
+import itertools
-from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden
+from rhodecode.api import (
+ jsonrpc_method, JSONRPCError, JSONRPCForbidden, find_methods)
from rhodecode.api.utils import (
Optional, OAttr, has_superadmin_permission, get_user_or_error)
@@ -243,3 +245,77 @@ def cleanup_sessions(request, apiuser, o
raise JSONRPCError(
'Error occurred during session cleanup'
)
+
+
+@jsonrpc_method()
+def get_method(request, apiuser, pattern=Optional('*')):
+ """
+ Returns list of all available API methods. By default match pattern
+ os "*" but any other pattern can be specified. eg *comment* will return
+ all methods with comment inside them. If just single method is matched
+ returned data will also include method specification
+
+ This command can only be run using an |authtoken| with admin rights to
+ the specified repository.
+
+ This command takes the following options:
+
+ :param apiuser: This is filled automatically from the |authtoken|.
+ :type apiuser: AuthUser
+ :param pattern: pattern to match method names against
+ :type older_then: Optional("*")
+
+ Example output:
+
+ .. code-block:: bash
+
+ id :
+ "result": [
+ "changeset_comment",
+ "comment_pull_request",
+ "comment_commit"
+ ]
+ error : null
+
+ .. code-block:: bash
+
+ id :
+ "result": [
+ "comment_commit",
+ {
+ "apiuser": "",
+ "comment_type": "",
+ "commit_id": "",
+ "message": "",
+ "repoid": "",
+ "request": "",
+ "resolves_comment_id": "",
+ "status": "",
+ "userid": ">"
+ }
+ ]
+ error : null
+ """
+ if not has_superadmin_permission(apiuser):
+ raise JSONRPCForbidden()
+
+ pattern = Optional.extract(pattern)
+
+ matches = find_methods(request.registry.jsonrpc_methods, pattern)
+
+ args_desc = []
+ if len(matches) == 1:
+ func = matches[matches.keys()[0]]
+
+ argspec = inspect.getargspec(func)
+ arglist = argspec[0]
+ defaults = map(repr, argspec[3] or [])
+
+ default_empty = ''
+
+ # kw arguments required by this method
+ func_kwargs = dict(itertools.izip_longest(
+ reversed(arglist), reversed(defaults), fillvalue=default_empty))
+ args_desc.append(func_kwargs)
+
+ return matches.keys() + args_desc