rpc: Support named arguments

The [JSON-RPC specification](http://www.jsonrpc.org/specification) allows passing parameters as an Array, for by-position
arguments, or an Object, for by-name arguments.

This implements by-name arguments, but preserves full backwards compatibility. API using by-name arguments are
easier to extend, and easier to use (no need to guess which argument goes where).

Named are mapped to positions by a per-call structure, provided through the RPC command table.

Missing arguments will be replaced by null, except if at the end, then the argument is left out completely.

Currently calls fail (though not crash) on intermediate nulls, but this should be improved on a per-call basis later.
This commit is contained in:
Wladimir J. van der Laan 2016-09-25 20:42:49 +02:00
parent 5865d41f88
commit 6f1c76ae14
2 changed files with 57 additions and 9 deletions

View File

@ -26,6 +26,7 @@
#include <boost/algorithm/string/case_conv.hpp> // for to_upper()
#include <memory> // for unique_ptr
#include <unordered_map>
using namespace RPCServer;
using namespace std;
@ -268,11 +269,11 @@ UniValue stop(const JSONRPCRequest& jsonRequest)
* Call Table
*/
static const CRPCCommand vRPCCommands[] =
{ // category name actor (function) okSafeMode
// --------------------- ------------------------ ----------------------- ----------
{ // category name actor (function) okSafe argNames
// --------------------- ------------------------ ----------------------- ------ ----------
/* Overall control/query calls */
{ "control", "help", &help, true },
{ "control", "stop", &stop, true },
{ "control", "help", &help, true, {"command"} },
{ "control", "stop", &stop, true, {} },
};
CRPCTable::CRPCTable()
@ -379,12 +380,12 @@ void JSONRPCRequest::parse(const UniValue& valRequest)
// Parse params
UniValue valParams = find_value(request, "params");
if (valParams.isArray())
params = valParams.get_array();
if (valParams.isArray() || valParams.isObject())
params = valParams;
else if (valParams.isNull())
params = UniValue(UniValue::VARR);
else
throw JSONRPCError(RPC_INVALID_REQUEST, "Params must be an array");
throw JSONRPCError(RPC_INVALID_REQUEST, "Params must be an array or object");
}
static UniValue JSONRPCExecOne(const UniValue& req)
@ -420,6 +421,48 @@ std::string JSONRPCExecBatch(const UniValue& vReq)
return ret.write() + "\n";
}
/**
* Process named arguments into a vector of positional arguments, based on the
* passed-in specification for the RPC call's arguments.
*/
static inline JSONRPCRequest transformNamedArguments(const JSONRPCRequest& in, const std::vector<std::string>& argNames)
{
JSONRPCRequest out = in;
out.params = UniValue(UniValue::VARR);
// Build a map of parameters, and remove ones that have been processed, so that we can throw a focused error if
// there is an unknown one.
const std::vector<std::string>& keys = in.params.getKeys();
const std::vector<UniValue>& values = in.params.getValues();
std::unordered_map<std::string, const UniValue*> argsIn;
for (size_t i=0; i<keys.size(); ++i) {
argsIn[keys[i]] = &values[i];
}
// Process expected parameters.
int hole = 0;
for (const std::string &argName: argNames) {
auto fr = argsIn.find(argName);
if (fr != argsIn.end()) {
for (int i = 0; i < hole; ++i) {
// Fill hole between specified parameters with JSON nulls,
// but not at the end (for backwards compatibility with calls
// that act based on number of specified parameters).
out.params.push_back(UniValue());
}
hole = 0;
out.params.push_back(*fr->second);
argsIn.erase(fr);
} else {
hole += 1;
}
}
// If there are still arguments in the argsIn map, this is an error.
if (!argsIn.empty()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Unknown named parameter " + argsIn.begin()->first);
}
// Return request with named arguments transformed to positional arguments
return out;
}
UniValue CRPCTable::execute(const JSONRPCRequest &request) const
{
// Return immediately if in warmup
@ -438,9 +481,13 @@ UniValue CRPCTable::execute(const JSONRPCRequest &request) const
try
{
// Execute
// Execute, convert arguments to array if necessary
if (request.params.isObject()) {
return pcmd->actor(transformNamedArguments(request, pcmd->argNames));
} else {
return pcmd->actor(request);
}
}
catch (const std::exception& e)
{
throw JSONRPCError(RPC_MISC_ERROR, e.what());

View File

@ -136,6 +136,7 @@ public:
std::string name;
rpcfn_type actor;
bool okSafeMode;
std::vector<std::string> argNames;
};
/**