diff --git a/e2e_test.go b/e2e_test.go index fb786e0..94cbca2 100644 --- a/e2e_test.go +++ b/e2e_test.go @@ -171,6 +171,36 @@ func TestE2E_Add(t *testing.T) { }) } +func TestE2E_Replace(t *testing.T) { + t.Parallel() + + address, done := memctest.LaunchTCP(t, nil) + t.Cleanup(done) + + c := New([]string{address}) + defer ignore.Close(c) + + t.Run("success", func(t *testing.T) { + err := Set(c, "key1", "value1") + must.NoError(t, err) + + err = Replace(c, "key1", "value1.replaced") + must.NoError(t, err) + + v, verr := Get[string](c, "key1") + must.NoError(t, verr) + must.Eq(t, "value1.replaced", v) + }) + + t.Run("not found", func(t *testing.T) { + err := Replace(c, "key-does-not-exist", "value") + must.ErrorIs(t, err, ErrNotStored) + + _, verr := Get[string](c, "key-does-not-exist") + must.ErrorIs(t, verr, ErrCacheMiss) + }) +} + func TestE2E_Increment(t *testing.T) { t.Parallel() diff --git a/verbs.go b/verbs.go index ad42af6..65fc7d5 100644 --- a/verbs.go +++ b/verbs.go @@ -128,6 +128,80 @@ func Set[T any](c *Client, key string, item T, opts ...Option) error { }) } +// Replace will store the item using the given key, but only if the key +// already exists. New items are at the top of the LRU. +// +// Uses Client c to connect to a memcached instance, and automatically handles +// connection pooling and reuse. +// +// One or more Option(s) may be applied to configure things such as the +// value expiration TTL or its associated flags. +func Replace[T any](c *Client, key string, item T, opts ...Option) error { + if err := check(key); err != nil { + return err + } + + options := &Options{ + expiration: c.expiration, + flags: 0, + } + + for _, opt := range opts { + opt(options) + } + + return c.do(key, func(conn *iopool.Buffer) error { + encoding, encerr := encode(item) + if encerr != nil { + return encerr + } + + expiration, experr := c.seconds(options.expiration) + if experr != nil { + return experr + } + + // write the header components + if _, err := fmt.Fprintf( + conn, + "replace %s %d %d %d\r\n", + key, options.flags, expiration, len(encoding), + ); err != nil { + return err + } + + // write the payload + if _, err := conn.Write(encoding); err != nil { + return err + } + + // write clrf + if _, err := io.WriteString(conn, "\r\n"); err != nil { + return err + } + + // flush the buffer + if err := conn.Flush(); err != nil { + return err + } + + // read response + line, lerr := conn.ReadSlice('\n') + if lerr != nil { + return lerr + } + + switch string(line) { + case "STORED\r\n": + return nil + case "NOT_STORED\r\n": + return ErrNotStored + default: + return fmt.Errorf("memc: unexpected response to replace: %q", string(line)) + } + }) +} + // Add will store the item using the given key, but only if no item currently // exists. New items are at the top of the LRU. //