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