From 5020b6610c469739fc02f3cebd2ff97f6ae5a319 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Wed, 15 Feb 2023 18:03:13 +1100 Subject: [PATCH] feat: add ErrNotFound * Intended to replace github.com/ipfs/go-ipld-format#ErrNotFound * A new IsNotFound() that uses feature detection rather than type checking so it's compatible with old and new forms. Ref: https://github.com/ipld/go-car/pull/363 Ref: https://github.com/ipld/go-ipld-prime/pull/493 --- storage/memstore/memstore.go | 9 +++--- storage/notfound.go | 60 ++++++++++++++++++++++++++++++++++++ storage/notfound_test.go | 58 ++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 storage/notfound.go create mode 100644 storage/notfound_test.go diff --git a/storage/memstore/memstore.go b/storage/memstore/memstore.go index 1b1e66d4..b484e834 100644 --- a/storage/memstore/memstore.go +++ b/storage/memstore/memstore.go @@ -3,8 +3,9 @@ package memstore import ( "bytes" "context" - "fmt" "io" + + "github.com/ipld/go-ipld-prime/storage" ) // Store is a simple in-memory storage. @@ -56,7 +57,7 @@ func (store *Store) Get(ctx context.Context, key string) ([]byte, error) { store.beInitialized() content, exists := store.Bag[key] if !exists { - return nil, fmt.Errorf("404") // FIXME this needs a standard error type + return nil, storage.ErrNotFound{Key: key} } cpy := make([]byte, len(content)) copy(cpy, content) @@ -82,7 +83,7 @@ func (store *Store) Put(ctx context.Context, key string, content []byte) error { func (store *Store) GetStream(ctx context.Context, key string) (io.ReadCloser, error) { content, exists := store.Bag[key] if !exists { - return nil, fmt.Errorf("404") // FIXME this needs a standard error type + return nil, storage.ErrNotFound{Key: key} } return noopCloser{bytes.NewReader(content)}, nil } @@ -91,7 +92,7 @@ func (store *Store) GetStream(ctx context.Context, key string) (io.ReadCloser, e func (store *Store) Peek(ctx context.Context, key string) ([]byte, io.Closer, error) { content, exists := store.Bag[key] if !exists { - return nil, nil, fmt.Errorf("404") // FIXME this needs a standard error type + return nil, nil, storage.ErrNotFound{Key: key} } return content, noopCloser{nil}, nil } diff --git a/storage/notfound.go b/storage/notfound.go new file mode 100644 index 00000000..f6779a9c --- /dev/null +++ b/storage/notfound.go @@ -0,0 +1,60 @@ +package storage + +import "github.com/ipfs/go-cid" + +// compatible with the go-ipld-format ErrNotFound, match against +// interface{NotFound() bool} +// this could go into go-ipld-prime, but for now we'll just exercise the +// feature-test pattern + +// ErrNotFound is a 404, but for block storage systems. It is returned when +// a block is not found. The Key is typically the binary form of a CID +// (CID#KeyString()). +// +// ErrNotFound implements `interface{NotFound() bool}`, which makes it roughly +// compatible with the legacy github.com/ipfs/go-ipld-format#ErrNotFound. +// The IsNotFound() function here will test for this and therefore be compatible +// with this ErrNotFound, and the legacy ErrNotFound. The same is not true for +// the legacy github.com/ipfs/go-ipld-format#IsNotFound. +type ErrNotFound struct { + Key string +} + +// NewErrNotFound is a convenience factory that creates a new ErrNotFound error +// from a CID. +func NewErrNotFound(c cid.Cid) ErrNotFound { + return ErrNotFound{Key: c.KeyString()} +} + +func (e ErrNotFound) Error() string { + if c, err := cid.Cast([]byte(e.Key)); err == nil && c != cid.Undef { + return "ipld: could not find " + c.String() + } + return "ipld: could not find " + e.Key +} + +// NotFound always returns true, and is used to feature-test for ErrNotFound +// errors. +func (e ErrNotFound) NotFound() bool { + return true +} + +// Is allows errors.Is to work with this error type. +func (e ErrNotFound) Is(err error) bool { + switch err.(type) { + case ErrNotFound: + return true + default: + return false + } +} + +// IsNotFound returns true if the error is a ErrNotFound. As it uses a +// feature-test, it is also compatible with the legacy +// github.com/ipfs/go-ipld-format#ErrNotFound. +func IsNotFound(err error) bool { + if nf, ok := err.(interface{ NotFound() bool }); ok { + return nf.NotFound() + } + return false +} diff --git a/storage/notfound_test.go b/storage/notfound_test.go new file mode 100644 index 00000000..32ba046b --- /dev/null +++ b/storage/notfound_test.go @@ -0,0 +1,58 @@ +package storage_test + +import ( + "testing" + + "github.com/ipfs/go-cid" + "github.com/ipld/go-ipld-prime/storage" +) + +func TestNotFound(t *testing.T) { + nf := storage.ErrNotFound{Key: "foo"} + if !storage.IsNotFound(nf) { + t.Fatal("expected ErrNotFound to be a NotFound error") + } + if nf.Error() != "ipld: could not find foo" { + t.Fatal("unexpected error message") + } + + nf = storage.NewErrNotFound(cid.MustParse("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi")) + if !storage.IsNotFound(nf) { + t.Fatal("expected ErrNotFound to be a NotFound error") + } + if nf.Error() != "ipld: could not find bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi" { + t.Fatal("unexpected error message") + } + + wnf := &weirdNotFoundError{} + if !storage.IsNotFound(wnf) { + t.Fatal("expected weirdNotFoundError to be a NotFound error") + } + + // a weirder case, this one implements `NotFound()` but it returns false, so + // this shouldn't be a NotFound error + wnnf := &weirdNotNotFoundError{} + if storage.IsNotFound(wnnf) { + t.Fatal("expected weirdNotNotFoundError to NOT be a NotFound error") + } +} + +type weirdNotFoundError struct{} + +func (weirdNotFoundError) NotFound() bool { + return true +} + +func (weirdNotFoundError) Error() string { + return "weird not found error" +} + +type weirdNotNotFoundError struct{} + +func (weirdNotNotFoundError) NotFound() bool { + return false +} + +func (weirdNotNotFoundError) Error() string { + return "weird not NOT found error" +}