diff --git a/internal/utils.go b/internal/utils.go index 2bb7234..03e3bf0 100644 --- a/internal/utils.go +++ b/internal/utils.go @@ -219,3 +219,45 @@ func FilterExpiredKeys(state map[string]KeyData) map[string]KeyData { } return state } + +func EncodeCommand(cmd []string) []byte { + res := fmt.Sprintf("*%d\r\n", len(cmd)) + for _, token := range cmd { + res += fmt.Sprintf("$%d\r\n%s\r\n", len(token), token) + } + return []byte(res) +} + +func ParseStringResponse(b []byte) (string, error) { + r := resp.NewReader(bytes.NewReader(b)) + v, _, err := r.ReadValue() + if err != nil { + return "", err + } + return v.String(), nil +} + +func ParseIntegerResponse(b []byte) (int, error) { + r := resp.NewReader(bytes.NewReader(b)) + v, _, err := r.ReadValue() + if err != nil { + return 0, err + } + return v.Integer(), nil +} + +func ParseArrayResponse(b []byte) ([]string, error) { + r := resp.NewReader(bytes.NewReader(b)) + v, _, err := r.ReadValue() + if err != nil { + return nil, err + } + if v.IsNull() { + return []string{}, nil + } + arr := make([]string, len(v.Array())) + for i, e := range v.Array() { + arr[i] = e.String() + } + return arr, nil +} diff --git a/pkg/echovault/api_generic.go b/pkg/echovault/api_generic.go index afd234a..955c633 100644 --- a/pkg/echovault/api_generic.go +++ b/pkg/echovault/api_generic.go @@ -13,3 +13,269 @@ // limitations under the License. package echovault + +import ( + "github.com/echovault/echovault/internal" + "strconv" +) + +type SETOptions struct { + NX bool + XX bool + LT bool + GT bool + GET bool + EX int + PX int + EXAT int + PXAT int +} + +type EXPIREOptions struct { + NX bool + XX bool + LT bool + GT bool +} + +type PEXPIREOptions EXPIREOptions + +type EXPIREATOptions EXPIREOptions + +type PEXPIREATOptions EXPIREOptions + +func (server *EchoVault) SET(key, value string, options SETOptions) (string, error) { + cmd := []string{"SET", key, value} + + switch { + case options.NX: + cmd = append(cmd, "NX") + case options.XX: + cmd = append(cmd, "XX") + } + + switch { + case options.EX != 0: + cmd = append(cmd, []string{"EX", strconv.Itoa(options.EX)}...) + case options.PX != 0: + cmd = append(cmd, []string{"PX", strconv.Itoa(options.PX)}...) + case options.EXAT != 0: + cmd = append(cmd, []string{"EXAT", strconv.Itoa(options.EXAT)}...) + case options.PXAT != 0: + cmd = append(cmd, []string{"PXAT", strconv.Itoa(options.PXAT)}...) + } + + if options.GET { + cmd = append(cmd, "GET") + } + + encoded := internal.EncodeCommand(cmd) + + b, err := server.handleCommand(server.context, encoded, nil, false) + if err != nil { + return "", err + } + + return internal.ParseStringResponse(b) +} + +func (server *EchoVault) MSET(kvPairs map[string]string) (string, error) { + cmd := []string{"MSET"} + + for k, v := range kvPairs { + cmd = append(cmd, []string{k, v}...) + } + + encoded := internal.EncodeCommand(cmd) + + b, err := server.handleCommand(server.context, encoded, nil, false) + if err != nil { + return "", err + } + + return internal.ParseStringResponse(b) +} + +func (server *EchoVault) GET(key string) (string, error) { + encoded := internal.EncodeCommand([]string{"GET", key}) + + b, err := server.handleCommand(server.context, encoded, nil, false) + if err != nil { + return "", err + } + + return internal.ParseStringResponse(b) +} + +func (server *EchoVault) MGET(keys []string) ([]string, error) { + encoded := internal.EncodeCommand(append([]string{"MGET"}, keys...)) + + b, err := server.handleCommand(server.context, encoded, nil, false) + if err != nil { + return []string{}, err + } + + return internal.ParseArrayResponse(b) +} + +func (server *EchoVault) DEL(keys []string) (int, error) { + encoded := internal.EncodeCommand(append([]string{"DEL"}, keys...)) + + b, err := server.handleCommand(server.context, encoded, nil, false) + if err != nil { + return 0, err + } + + return internal.ParseIntegerResponse(b) +} + +func (server *EchoVault) PERSIST(key string) (int, error) { + encoded := internal.EncodeCommand([]string{"PERSIST", key}) + + b, err := server.handleCommand(server.context, encoded, nil, false) + if err != nil { + return 0, err + } + + return internal.ParseIntegerResponse(b) +} + +func (server *EchoVault) EXPIRETIME(key string) (int, error) { + encoded := internal.EncodeCommand([]string{"EXPIRETIME", key}) + + b, err := server.handleCommand(server.context, encoded, nil, false) + if err != nil { + return 0, err + } + + return internal.ParseIntegerResponse(b) +} + +func (server *EchoVault) PEXPIRETIME(key string) (int, error) { + encoded := internal.EncodeCommand([]string{"PEXPIRETIME", key}) + + b, err := server.handleCommand(server.context, encoded, nil, false) + if err != nil { + return 0, err + } + + return internal.ParseIntegerResponse(b) +} + +func (server *EchoVault) TTL(key string) (int, error) { + encoded := internal.EncodeCommand([]string{"TTL", key}) + + b, err := server.handleCommand(server.context, encoded, nil, false) + if err != nil { + return 0, err + } + + return internal.ParseIntegerResponse(b) +} + +func (server *EchoVault) PTTL(key string) (int, error) { + encoded := internal.EncodeCommand([]string{"PTTL", key}) + + b, err := server.handleCommand(server.context, encoded, nil, false) + if err != nil { + return 0, err + } + + return internal.ParseIntegerResponse(b) +} + +func (server *EchoVault) EXPIRE(key string, seconds int, options EXPIREOptions) (int, error) { + cmd := []string{"EXPIRE", key, strconv.Itoa(seconds)} + + switch { + case options.NX: + cmd = append(cmd, "NX") + case options.XX: + cmd = append(cmd, "XX") + case options.LT: + cmd = append(cmd, "LT") + case options.GT: + cmd = append(cmd, "GT") + } + + encoded := internal.EncodeCommand(cmd) + + b, err := server.handleCommand(server.context, encoded, nil, false) + if err != nil { + return 0, err + } + + return internal.ParseIntegerResponse(b) +} + +func (server *EchoVault) PEXPIRE(key string, milliseconds int, options PEXPIREOptions) (int, error) { + cmd := []string{"PEXPIRE", key, strconv.Itoa(milliseconds)} + + switch { + case options.NX: + cmd = append(cmd, "NX") + case options.XX: + cmd = append(cmd, "XX") + case options.LT: + cmd = append(cmd, "LT") + case options.GT: + cmd = append(cmd, "GT") + } + + encoded := internal.EncodeCommand(cmd) + + b, err := server.handleCommand(server.context, encoded, nil, false) + if err != nil { + return 0, err + } + + return internal.ParseIntegerResponse(b) +} + +func (server *EchoVault) EXPIREAT(key string, unixSeconds int, options EXPIREATOptions) (int, error) { + cmd := []string{"EXPIREAT", key, strconv.Itoa(unixSeconds)} + + switch { + case options.NX: + cmd = append(cmd, "NX") + case options.XX: + cmd = append(cmd, "XX") + case options.LT: + cmd = append(cmd, "LT") + case options.GT: + cmd = append(cmd, "GT") + } + + encoded := internal.EncodeCommand(cmd) + + b, err := server.handleCommand(server.context, encoded, nil, false) + if err != nil { + return 0, err + } + + return internal.ParseIntegerResponse(b) +} + +func (server *EchoVault) PEXPIREAT(key string, unixMilliseconds int, options PEXPIREATOptions) (int, error) { + cmd := []string{"PEXPIREAT", key, strconv.Itoa(unixMilliseconds)} + + switch { + case options.NX: + cmd = append(cmd, "NX") + case options.XX: + cmd = append(cmd, "XX") + case options.LT: + cmd = append(cmd, "LT") + case options.GT: + cmd = append(cmd, "GT") + } + + encoded := internal.EncodeCommand(cmd) + + b, err := server.handleCommand(server.context, encoded, nil, false) + if err != nil { + return 0, err + } + + return internal.ParseIntegerResponse(b) +} diff --git a/pkg/modules/generic/commands.go b/pkg/modules/generic/commands.go index 34ac59d..be6be85 100644 --- a/pkg/modules/generic/commands.go +++ b/pkg/modules/generic/commands.go @@ -524,7 +524,7 @@ PXAT - Expire at the exat time in unix milliseconds (positive integer).`, { Command: "mget", Categories: []string{utils.ReadCategory, utils.FastCategory}, - Description: "(MGET key1 [key2]) Get multiple values from the specified keys.", + Description: "(MGET key [key ...]) Get multiple values from the specified keys.", Sync: false, KeyExtractionFunc: mgetKeyFunc, HandlerFunc: handleMGet,