mirror of
https://github.com/dashpay/dash.git
synced 2024-12-25 12:02:48 +01:00
merge bitcoin#19762: Allow named and positional arguments to be used together
This commit is contained in:
parent
4e82a960b6
commit
d40f28edb4
@ -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
|
||||||
|
19
doc/release-notes-19762.md
Normal file
19
doc/release-notes-19762.md
Normal 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
|
||||||
|
```
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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")
|
||||||
|
@ -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()
|
||||||
|
@ -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):
|
||||||
|
@ -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"]
|
||||||
|
Loading…
Reference in New Issue
Block a user