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:
- 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");
}
}
- 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);
}
- 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;
}
- 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.