Parse commands

Diagram #

command parse class diagram

CommandConfig #

This describes a command, the name, key, value, and other options, all are optional but name.

Assumptions for a command:

  • name must be unique globally
  • each data type has its own command, no command shares
  • if no key, then no value
  • each command has zero or more options
  • each command option must have a name, may or may not have a value
  • each command option may have zero or more followers, it’s used to define options order.
  • if a command support multiple values, options must be shown before values

CommandConfig will be defined in CommandFactory.

CommandOptionConfig #

Describes command option, including option name, whether it’s required, and whether it has a value.

Example

// no value, name is EX
NX
// name is EX, value is 500
EX 500

Command #

Each command has a unique name, and each command is used for different data types.

CompositeCommand is used to support pipe in Redis.

Currently only SimpleCommand is supported.

CommandFactory #

CommandFactory will parse given ByteWord list to a Command if it’s valid, or throw InvalidCommandException.

the parse process:

  1. first parse key
        Map<String, ByteWord> optionMap = new HashMap<>();
        if (commandConfig.requireKey()) {
            if (index < words.size()) {
                key = words.get(index++);
            } else {
                throw new InvalidCommandException(commandConfig.name(), "key required");
            }
        }
  1. then parse values and options, the order depends on whether the command support multiple values
        List<CommandOptionConfig> options = commandConfig.options();

        if (commandConfig.supportMultiValues()) {
            // parse options first
            optionParsed = true;
            if (index < words.size() && options != null && !options.isEmpty()) {
                index = parseOptions(commandConfig, options, words, index, optionMap);
            }
        }

        if (commandConfig.requireValue() && index < words.size()) {
            if (commandConfig.supportMultiValues()) {
                values = new ArrayList<>(words.subList(index, words.size()));
                index = words.size();
            } else {
                values = new ArrayList<>(words.subList(index, index + 1));
                index++;
            }
        }

        if (commandConfig.requireValue() && values == null) {
            throw new InvalidCommandException(commandConfig.name(), "value required");
        }

        if (!optionParsed && index < words.size() && options != null && !options.isEmpty()) {
            parseOptions(commandConfig, options, words, index, optionMap);
        }
  1. parse options
    private int parseOptions(CommandConfig commandConfig, List<CommandOptionConfig> optionConfigs, List<ByteWord> words,
                             int index, Map<String, ByteWord> optionMap) {
        while (index < words.size()) {
            ByteWord byteWord = words.get(index);
            String name = byteWord.getString();
            boolean parsed = false;
            for (CommandOptionConfig option : optionConfigs) {
                if (option.name().equals(name)) {
                    index++;
                    index = parseOption(commandConfig, option, words, index, optionMap);
                    parsed = true;
                    break;
                }
            }
            // found an unknown option, what to do?
            if (!parsed) {
                index++;
            }
        }

        return index;
    }
  1. parse a single option and it’s followers
    private int parseOption(CommandConfig commandConfig, CommandOptionConfig optionConfig, List<ByteWord> words,
                            int index, Map<String, ByteWord> optionMap) {
        ByteWord value = ByteWord.NULL;
        if (optionConfig.valueRequired() && index < words.size()) {
            value = words.get(index++);
            if (optionConfig.valueIsNumber() && !value.isNumber()) {
                throw new InvalidCommandException(commandConfig.name(),
                        String.format("%s should be a number", optionConfig.name()));
            }
        }
        if (optionConfig.valueRequired() && value == ByteWord.NULL) {
            throw new InvalidCommandException(commandConfig.name(),
                    String.format("option %s value is required", optionConfig.name()));
        }
        optionMap.put(optionConfig.name(), value);

        if (optionConfig.nextOptions() != null && !optionConfig.nextOptions().isEmpty()) {
            index = parseOptions(commandConfig, optionConfig.nextOptions(), words, index, optionMap);
            for (CommandOptionConfig config : optionConfig.nextOptions()) {
                if (config.required() && !optionMap.containsKey(config.name())) {
                    throw new InvalidCommandException(commandConfig.name(),
                            String.format("option %s is required", config.name()));
                }
            }
        }
        return index;
    }

after parse, a command will be passed to database.