merge bitcoin#19762: Allow named and positional arguments to be used together

This commit is contained in:
Kittywhiskers Van Gogh 2023-07-18 22:47:58 +00:00 committed by PastaPastaPasta
parent 4e82a960b6
commit d40f28edb4
9 changed files with 148 additions and 5 deletions

View File

@ -5,6 +5,28 @@ The headless daemon `dashd` has the JSON-RPC API enabled by default, the GUI
option. In the GUI it is possible to execute RPC methods in the Debug Console option. In the GUI it is possible to execute RPC methods in the Debug Console
Dialog. Dialog.
## Parameter passing
The JSON-RPC server supports both _by-position_ and _by-name_ [parameter
structures](https://www.jsonrpc.org/specification#parameter_structures)
described in the JSON-RPC specification. For extra convenience, to avoid the
need to name every parameter value, all RPC methods accept a named parameter
called `args`, which can be set to an array of initial positional values that
are combined with named values.
Examples:
```sh
# "params": ["mywallet", false, false, "", false, false, true]
dash-cli createwallet mywallet false false "" false false true
# "params": {"wallet_name": "mywallet", "load_on_startup": true}
dash-cli -named createwallet wallet_name=mywallet load_on_startup=true
# "params": {"args": ["mywallet"], "load_on_startup": true}
dash-cli -named createwallet mywallet load_on_startup=true
```
## Versioning ## Versioning
The RPC interface might change from one major version of Dash Core to the The RPC interface might change from one major version of Dash Core to the

View File

@ -0,0 +1,19 @@
JSON-RPC
---
All JSON-RPC methods accept a new [named
parameter](JSON-RPC-interface.md#parameter-passing) called `args` that can
contain positional parameter values. This is a convenience to allow some
parameter values to be passed by name without having to name every value. The
python test framework and `dash-cli` tool both take advantage of this, so
for example:
```sh
dash-cli -named createwallet wallet_name=mywallet load_on_startup=1
```
Can now be shortened to:
```sh
dash-cli -named createwallet mywallet load_on_startup=1
```

View File

@ -277,11 +277,13 @@ UniValue RPCConvertValues(const std::string &strMethod, const std::vector<std::s
UniValue RPCConvertNamedValues(const std::string &strMethod, const std::vector<std::string> &strParams) UniValue RPCConvertNamedValues(const std::string &strMethod, const std::vector<std::string> &strParams)
{ {
UniValue params(UniValue::VOBJ); UniValue params(UniValue::VOBJ);
UniValue positional_args{UniValue::VARR};
for (const std::string &s: strParams) { for (const std::string &s: strParams) {
size_t pos = s.find('='); size_t pos = s.find('=');
if (pos == std::string::npos) { if (pos == std::string::npos) {
throw(std::runtime_error("No '=' in named argument '"+s+"', this needs to be present for every argument (even if it is empty)")); positional_args.push_back(rpcCvtTable.convert(strMethod, positional_args.size()) ? ParseNonRFCJSONValue(s) : s);
continue;
} }
std::string name = s.substr(0, pos); std::string name = s.substr(0, pos);
@ -296,5 +298,9 @@ UniValue RPCConvertNamedValues(const std::string &strMethod, const std::vector<s
} }
} }
if (!positional_args.empty()) {
params.pushKV("args", positional_args);
}
return params; return params;
} }

View File

@ -420,8 +420,16 @@ static inline JSONRPCRequest transformNamedArguments(const JSONRPCRequest& in, c
for (size_t i=0; i<keys.size(); ++i) { for (size_t i=0; i<keys.size(); ++i) {
argsIn[keys[i]] = &values[i]; argsIn[keys[i]] = &values[i];
} }
// Process expected parameters. // Process expected parameters. If any parameters were left unspecified in
// the request before a parameter that was specified, null values need to be
// inserted at the unspecifed parameter positions, and the "hole" variable
// below tracks the number of null values that need to be inserted.
// The "initial_hole_size" variable stores the size of the initial hole,
// i.e. how many initial positional arguments were left unspecified. This is
// used after the for-loop to add initial positional arguments from the
// "args" parameter, if present.
int hole = 0; int hole = 0;
int initial_hole_size = 0;
for (const std::string &argNamePattern: argNames) { for (const std::string &argNamePattern: argNames) {
std::vector<std::string> vargNames = SplitString(argNamePattern, '|'); std::vector<std::string> vargNames = SplitString(argNamePattern, '|');
auto fr = argsIn.end(); auto fr = argsIn.end();
@ -443,6 +451,24 @@ static inline JSONRPCRequest transformNamedArguments(const JSONRPCRequest& in, c
argsIn.erase(fr); argsIn.erase(fr);
} else { } else {
hole += 1; hole += 1;
if (out.params.empty()) initial_hole_size = hole;
}
}
// If leftover "args" param was found, use it as a source of positional
// arguments and add named arguments after. This is a convenience for
// clients that want to pass a combination of named and positional
// arguments as described in doc/JSON-RPC-interface.md#parameter-passing
auto positional_args{argsIn.extract("args")};
if (positional_args && positional_args.mapped()->isArray()) {
const bool has_named_arguments{initial_hole_size < (int)argNames.size()};
if (initial_hole_size < (int)positional_args.mapped()->size() && has_named_arguments) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Parameter " + argNames[initial_hole_size] + " specified twice both as positional and named argument");
}
// Assign positional_args to out.params and append named_args after.
UniValue named_args{std::move(out.params)};
out.params = *positional_args.mapped();
for (size_t i{out.params.size()}; i < named_args.size(); ++i) {
out.params.push_back(named_args[i]);
} }
} }
// If there are still arguments in the argsIn map, this is an error. // If there are still arguments in the argsIn map, this is an error.

View File

@ -16,12 +16,50 @@
#include <boost/test/unit_test.hpp> #include <boost/test/unit_test.hpp>
static UniValue JSON(std::string_view json)
{
UniValue value;
BOOST_CHECK(value.read(json.data(), json.size()));
return value;
}
class HasJSON
{
public:
explicit HasJSON(std::string json) : m_json(std::move(json)) {}
bool operator()(const UniValue& value) const
{
std::string json{value.write()};
BOOST_CHECK_EQUAL(json, m_json);
return json == m_json;
};
private:
const std::string m_json;
};
class RPCTestingSetup : public TestingSetup class RPCTestingSetup : public TestingSetup
{ {
public: public:
UniValue TransformParams(const UniValue& params, std::vector<std::string> arg_names);
UniValue CallRPC(std::string args); UniValue CallRPC(std::string args);
}; };
UniValue RPCTestingSetup::TransformParams(const UniValue& params, std::vector<std::string> arg_names)
{
UniValue transformed_params;
CRPCTable table;
CRPCCommand command{"category", "method", [&](const JSONRPCRequest& request, UniValue&, bool) -> bool { transformed_params = request.params; return true; }, arg_names, /*unique_id=*/0};
table.appendCommand("method", &command);
CoreContext context{m_node};
JSONRPCRequest request(context);
request.strMethod = "method";
request.params = params;
if (RPCIsInWarmup(nullptr)) SetRPCWarmupFinished();
table.execute(request);
return transformed_params;
}
UniValue RPCTestingSetup::CallRPC(std::string args) UniValue RPCTestingSetup::CallRPC(std::string args)
{ {
std::vector<std::string> vArgs{SplitString(args, ' ')}; std::vector<std::string> vArgs{SplitString(args, ' ')};
@ -45,6 +83,29 @@ UniValue RPCTestingSetup::CallRPC(std::string args)
BOOST_FIXTURE_TEST_SUITE(rpc_tests, RPCTestingSetup) BOOST_FIXTURE_TEST_SUITE(rpc_tests, RPCTestingSetup)
BOOST_AUTO_TEST_CASE(rpc_namedparams)
{
const std::vector<std::string> arg_names{{"arg1", "arg2", "arg3", "arg4", "arg5"}};
// Make sure named arguments are transformed into positional arguments in correct places separated by nulls
BOOST_CHECK_EQUAL(TransformParams(JSON(R"({"arg2": 2, "arg4": 4})"), arg_names).write(), "[null,2,null,4]");
// Make sure named and positional arguments can be combined.
BOOST_CHECK_EQUAL(TransformParams(JSON(R"({"arg5": 5, "args": [1, 2], "arg4": 4})"), arg_names).write(), "[1,2,null,4,5]");
// Make sure a unknown named argument raises an exception
BOOST_CHECK_EXCEPTION(TransformParams(JSON(R"({"arg2": 2, "unknown": 6})"), arg_names), UniValue,
HasJSON(R"({"code":-8,"message":"Unknown named parameter unknown"})"));
// Make sure an overlap between a named argument and positional argument raises an exception
BOOST_CHECK_EXCEPTION(TransformParams(JSON(R"({"args": [1,2,3], "arg4": 4, "arg2": 2})"), arg_names), UniValue,
HasJSON(R"({"code":-8,"message":"Parameter arg2 specified twice both as positional and named argument"})"));
// Make sure extra positional arguments can be passed through to the method implemenation, as long as they don't overlap with named arguments.
BOOST_CHECK_EQUAL(TransformParams(JSON(R"({"args": [1,2,3,4,5,6,7,8,9,10]})"), arg_names).write(), "[1,2,3,4,5,6,7,8,9,10]");
BOOST_CHECK_EQUAL(TransformParams(JSON(R"([1,2,3,4,5,6,7,8,9,10])"), arg_names).write(), "[1,2,3,4,5,6,7,8,9,10]");
}
BOOST_AUTO_TEST_CASE(rpc_rawparams) BOOST_AUTO_TEST_CASE(rpc_rawparams)
{ {
// Test raw transaction API argument handling // Test raw transaction API argument handling

View File

@ -44,6 +44,11 @@ class TestBitcoinCli(BitcoinTestFramework):
rpc_response = self.nodes[0].getblockchaininfo() rpc_response = self.nodes[0].getblockchaininfo()
assert_equal(cli_response, rpc_response) assert_equal(cli_response, rpc_response)
self.log.info("Test named arguments")
assert_equal(self.nodes[0].cli.echo(0, 1, arg3=3, arg5=5), ['0', '1', None, '3', None, '5'])
assert_raises_rpc_error(-8, "Parameter arg1 specified twice both as positional and named argument", self.nodes[0].cli.echo, 0, 1, arg1=1)
assert_raises_rpc_error(-8, "Parameter arg1 specified twice both as positional and named argument", self.nodes[0].cli.echo, 0, None, 2, arg1=1)
user, password = get_auth_cookie(self.nodes[0].datadir, self.chain) user, password = get_auth_cookie(self.nodes[0].datadir, self.chain)
self.log.info("Test -stdinrpcpass option") self.log.info("Test -stdinrpcpass option")

View File

@ -30,6 +30,9 @@ class NamedArgumentTest(BitcoinTestFramework):
assert_equal(node.echo(arg1=1), [None, 1]) assert_equal(node.echo(arg1=1), [None, 1])
assert_equal(node.echo(arg9=None), [None]*10) assert_equal(node.echo(arg9=None), [None]*10)
assert_equal(node.echo(arg0=0,arg3=3,arg9=9), [0] + [None]*2 + [3] + [None]*5 + [9]) assert_equal(node.echo(arg0=0,arg3=3,arg9=9), [0] + [None]*2 + [3] + [None]*5 + [9])
assert_equal(node.echo(0, 1, arg3=3, arg5=5), [0, 1, None, 3, None, 5])
assert_raises_rpc_error(-8, "Parameter arg1 specified twice both as positional and named argument", node.echo, 0, 1, arg1=1)
assert_raises_rpc_error(-8, "Parameter arg1 specified twice both as positional and named argument", node.echo, 0, None, 2, arg1=1)
if __name__ == '__main__': if __name__ == '__main__':
NamedArgumentTest().main() NamedArgumentTest().main()

View File

@ -131,10 +131,12 @@ class AuthServiceProxy():
json.dumps(args or argsn, default=EncodeDecimal, ensure_ascii=self.ensure_ascii), json.dumps(args or argsn, default=EncodeDecimal, ensure_ascii=self.ensure_ascii),
)) ))
if args and argsn: if args and argsn:
raise ValueError('Cannot handle both named and positional arguments') params = dict(args=args, **argsn)
else:
params = args or argsn
return {'version': '1.1', return {'version': '1.1',
'method': self._service_name, 'method': self._service_name,
'params': args or argsn, 'params': params,
'id': AuthServiceProxy.__id_count} 'id': AuthServiceProxy.__id_count}
def __call__(self, *args, **argsn): def __call__(self, *args, **argsn):

View File

@ -629,7 +629,6 @@ class TestNodeCLI():
"""Run dash-cli command. Deserializes returned string as python object.""" """Run dash-cli command. Deserializes returned string as python object."""
pos_args = [arg_to_cli(arg) for arg in args] pos_args = [arg_to_cli(arg) for arg in args]
named_args = [str(key) + "=" + arg_to_cli(value) for (key, value) in kwargs.items()] named_args = [str(key) + "=" + arg_to_cli(value) for (key, value) in kwargs.items()]
assert not (pos_args and named_args), "Cannot use positional arguments and named arguments in the same dash-cli call"
p_args = [self.binary, "-datadir=" + self.datadir] + self.options p_args = [self.binary, "-datadir=" + self.datadir] + self.options
if named_args: if named_args:
p_args += ["-named"] p_args += ["-named"]