diff --git a/src/server.c b/src/server.c index e1f7919b7..fa40771ab 100644 --- a/src/server.c +++ b/src/server.c @@ -3836,7 +3836,7 @@ void addReplyFlagsForArg(client *c, uint64_t flags) { void *flaglen = addReplyDeferredLen(c); flagcount += addReplyCommandFlag(c,flags,CMD_ARG_OPTIONAL, "optional"); flagcount += addReplyCommandFlag(c,flags,CMD_ARG_MULTIPLE, "multiple"); - flagcount += addReplyCommandFlag(c,flags,CMD_ARG_MULTIPLE_TOKEN, "multiple-token"); + flagcount += addReplyCommandFlag(c,flags,CMD_ARG_MULTIPLE_TOKEN, "multiple_token"); setDeferredSetLen(c, flaglen, flagcount); } @@ -4133,7 +4133,7 @@ void addReplyCommand(client *c, struct redisCommand *cmd) { maplen++; } if (cmd->key_specs_num) { - addReplyBulkCString(c, "key-specs"); + addReplyBulkCString(c, "key_specs"); addReplyCommandKeySpecs(c, cmd); maplen++; } diff --git a/tests/unit/moduleapi/keyspecs.tcl b/tests/unit/moduleapi/keyspecs.tcl index 265b9e9ee..1358b4f32 100644 --- a/tests/unit/moduleapi/keyspecs.tcl +++ b/tests/unit/moduleapi/keyspecs.tcl @@ -15,7 +15,7 @@ start_server {tags {"modules"}} { dict append mydict $k $v } # Verify key-specs - set keyspecs [dict get $mydict key-specs] + set keyspecs [dict get $mydict key_specs] assert_equal [lindex $keyspecs 0] {flags read begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} assert_equal [lindex $keyspecs 1] {flags write begin_search {type index spec {index 2}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} } @@ -32,7 +32,7 @@ start_server {tags {"modules"}} { dict append mydict $k $v } # Verify key-specs - set keyspecs [dict get $mydict key-specs] + set keyspecs [dict get $mydict key_specs] assert_equal [lindex $keyspecs 0] {flags {} begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} assert_equal [lindex $keyspecs 1] {flags write begin_search {type keyword spec {keyword STORE startfrom 2}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} assert_equal [lindex $keyspecs 2] {flags read begin_search {type keyword spec {keyword KEYS startfrom 2}} find_keys {type keynum spec {keynumidx 0 firstkey 1 keystep 1}}} @@ -50,7 +50,7 @@ start_server {tags {"modules"}} { dict append mydict $k $v } # Verify key-specs - set keyspecs [dict get $mydict key-specs] + set keyspecs [dict get $mydict key_specs] assert_equal [lindex $keyspecs 0] {flags write begin_search {type keyword spec {keyword STORE startfrom 5}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} assert_equal [lindex $keyspecs 1] {flags read begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} assert_equal [lindex $keyspecs 2] {flags read begin_search {type index spec {index 2}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} diff --git a/tests/unit/moduleapi/subcommands.tcl b/tests/unit/moduleapi/subcommands.tcl index 9be6f5cea..8de4ccbdb 100644 --- a/tests/unit/moduleapi/subcommands.tcl +++ b/tests/unit/moduleapi/subcommands.tcl @@ -12,8 +12,8 @@ start_server {tags {"modules"}} { dict append mydict $k $v } set subcmds [lsort [dict get $mydict subcommands]] - assert_equal [lindex $subcmds 0] {get -2 module 1 1 1 {} {summary {} since {} group module key-specs {{flags read begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}}}} - assert_equal [lindex $subcmds 1] {set -2 module 1 1 1 {} {summary {} since {} group module key-specs {{flags write begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}}}} + assert_equal [lindex $subcmds 0] {get -2 module 1 1 1 {} {summary {} since {} group module key_specs {{flags read begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}}}} + assert_equal [lindex $subcmds 1] {set -2 module 1 1 1 {} {summary {} since {} group module key_specs {{flags write begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}}}} } test "Module pure-container command fails on arity error" { diff --git a/utils/generate-commands-json.py b/utils/generate-commands-json.py new file mode 100755 index 000000000..8e6d915df --- /dev/null +++ b/utils/generate-commands-json.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python +import argparse +import json +from collections import OrderedDict +from sys import argv, stdin + +def convert_flags_to_boolean_dict(flags): + """Return a dict with a key set to `True` per element in the flags list.""" + return {f: True for f in flags} + + +def set_if_not_none_or_empty(dst, key, value): + """Set 'key' in 'dst' if 'value' is not `None` or an empty list.""" + if value is not None and (type(value) is not list or len(value)): + dst[key] = value + + +def convert_argument(arg): + """Transform an argument.""" + arg.update(convert_flags_to_boolean_dict(arg.pop('flags', []))) + set_if_not_none_or_empty(arg, 'arguments', + [convert_argument(x) for x in arg.pop('arguments',[])]) + return arg + + +def convert_keyspec(spec): + """Transform a key spec.""" + spec.update(convert_flags_to_boolean_dict(spec.pop('flags', []))) + return spec + + +def convert_entry_to_objects_array(container, cmd): + """Transform the JSON output of `COMMAND` to a friendlier format. + + `COMMAND`'s output per command is a fixed-size (8) list as follows: + 1. Name (lower case, e.g. "lolwut") + 2. Arity + 3. Flags + 4-6. First/last/step key specification (deprecated as of Redis v7.0) + 7. ACL categories + 8. A dict of meta information (as of Redis 7.0) + + This returns a list with a dict for the command and per each of its + subcommands. Each dict contains one key, the command's full name, with a + value of a dict that's set with the command's properties and meta + information.""" + assert len(cmd) >= 8 + obj = {} + rep = [obj] + name = cmd[0].upper() + arity = cmd[1] + command_flags = cmd[2] + acl_categories = cmd[6] + meta = cmd[7] + key = f'{container} {name}' if container else name + + rep.extend([convert_entry_to_objects_array(name, x)[0] for x in meta.pop('subcommands', [])]) + + # The command's value is ordered so the interesting stuff that we care about + # is at the start. Optional `None` and empty list values are filtered out. + value = OrderedDict() + value['summary'] = meta.pop('summary') + value['since'] = meta.pop('since') + value['group'] = meta.pop('group') + set_if_not_none_or_empty(value, 'complexity', meta.pop('complexity', None)) + set_if_not_none_or_empty(value, 'deprecated_since', meta.pop('deprecated_since', None)) + set_if_not_none_or_empty(value, 'replaced_by', meta.pop('replaced_by', None)) + set_if_not_none_or_empty(value, 'history', meta.pop('history', [])) + set_if_not_none_or_empty(value, 'acl_categories', acl_categories) + value['arity'] = arity + set_if_not_none_or_empty(value, 'key_specs', + [convert_keyspec(x) for x in meta.pop('key_specs',[])]) + set_if_not_none_or_empty(value, 'arguments', + [convert_argument(x) for x in meta.pop('arguments', [])]) + set_if_not_none_or_empty(value, 'command_flags', command_flags) + set_if_not_none_or_empty(value, 'doc_flags', meta.pop('doc_flags', [])) + set_if_not_none_or_empty(value, 'hints', meta.pop('hints', [])) + + # All remaining meta key-value tuples, if any, are appended to the command + # to be future-proof. + while len(meta) > 0: + (k, v) = meta.popitem() + value[k] = v + + obj[key] = value + return rep + + +# MAIN +if __name__ == '__main__': + opts = { + 'description': 'Transform the output from `redis-cli --json COMMAND` to commands.json format.', + 'epilog': f'Usage example: src/redis-cli --json COMMAND | {argv[0]}' + } + parser = argparse.ArgumentParser(**opts) + parser.add_argument('input', help='JSON-formatted input file (default: stdin)', + nargs='?', type=argparse.FileType(), default=stdin) + args = parser.parse_args() + + payload = OrderedDict() + commands = [] + data = json.load(args.input) + + for entry in data: + cmds = convert_entry_to_objects_array(None, entry) + commands.extend(cmds) + + # The final output is a dict of all commands, ordered by name. + commands.sort(key=lambda x: list(x.keys())[0]) + for cmd in commands: + name = list(cmd.keys())[0] + payload[name] = cmd[name] + + print(json.dumps(payload, indent=4))