Cloud 1359 errors across http (#2279)

* WIP: json marshal coded errors for http

* WIP: trying to see how best to implement the http-error tests

* finish stubbing out the Schemar methods in the test

* using json to move CodedErrors across boundaries and associated tests

* implemented feedback and fixes

---------

Co-authored-by: Travis Turner <travis@molecula.com>
This commit is contained in:
David Kagan 2023-02-27 16:34:51 -05:00 committed by GitHub
parent 933767ec07
commit d7c6258f16
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 337 additions and 52 deletions

View file

@ -71,8 +71,7 @@ func (c *Client) CreateDatabase(ctx context.Context, qdb *dax.QualifiedDatabase)
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return errors.Errorf("status code: %d: %s", resp.StatusCode, b)
return errors.Wrapf(errors.UnmarshalJSON(resp.Body), "status code: %d", resp.StatusCode)
}
return nil
@ -96,8 +95,7 @@ func (c *Client) DropDatabase(ctx context.Context, qdbid dax.QualifiedDatabaseID
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return errors.Errorf("status code: %d: %s", resp.StatusCode, b)
return errors.Wrapf(errors.UnmarshalJSON(resp.Body), "status code: %d", resp.StatusCode)
}
return nil
@ -122,8 +120,7 @@ func (c *Client) DatabaseByID(ctx context.Context, qdbid dax.QualifiedDatabaseID
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return nil, errors.Errorf("status code: %d: %s", resp.StatusCode, b)
return nil, errors.Wrapf(errors.UnmarshalJSON(resp.Body), "status code: %d", resp.StatusCode)
}
var qdb *dax.QualifiedDatabase
@ -158,8 +155,7 @@ func (c *Client) DatabaseByName(ctx context.Context, orgID dax.OrganizationID, n
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return nil, errors.Errorf("status code: %d: %s", resp.StatusCode, b)
return nil, errors.Wrapf(errors.UnmarshalJSON(resp.Body), "status code: %d", resp.StatusCode)
}
var qdb *dax.QualifiedDatabase
@ -194,8 +190,7 @@ func (c *Client) Databases(ctx context.Context, orgID dax.OrganizationID, ids ..
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return nil, errors.Errorf("status code: %d: %s", resp.StatusCode, b)
return nil, errors.Wrapf(errors.UnmarshalJSON(resp.Body), "status code: %d", resp.StatusCode)
}
var qdbs []*dax.QualifiedDatabase
@ -237,8 +232,7 @@ func (c *Client) SetDatabaseOption(ctx context.Context, qdbid dax.QualifiedDatab
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return errors.Errorf("status code: %d: %s", resp.StatusCode, b)
return errors.Wrapf(errors.UnmarshalJSON(resp.Body), "status code: %d", resp.StatusCode)
}
return nil
@ -277,8 +271,7 @@ func (c *Client) Table(ctx context.Context, qtid dax.QualifiedTableID) (*dax.Qua
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return nil, errors.Errorf("status code: %d: %s", resp.StatusCode, b)
return nil, errors.Wrapf(errors.UnmarshalJSON(resp.Body), "status code: %d", resp.StatusCode)
}
var qtable *dax.QualifiedTable
@ -314,8 +307,7 @@ func (c *Client) TableID(ctx context.Context, qdbid dax.QualifiedDatabaseID, nam
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return dflt, errors.Errorf("status code: %d: %s", resp.StatusCode, b)
return dflt, errors.Wrapf(errors.UnmarshalJSON(resp.Body), "status code: %d", resp.StatusCode)
}
var qtid dax.QualifiedTableID
@ -350,8 +342,7 @@ func (c *Client) Tables(ctx context.Context, qdbid dax.QualifiedDatabaseID, ids
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return nil, errors.Errorf("status code: %d: %s", resp.StatusCode, b)
return nil, errors.Wrapf(errors.UnmarshalJSON(resp.Body), "Status code: %d", resp.StatusCode)
}
var qtables []*dax.QualifiedTable
@ -380,8 +371,7 @@ func (c *Client) CreateTable(ctx context.Context, qtbl *dax.QualifiedTable) erro
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return errors.Errorf("status code: %d: %s", resp.StatusCode, b)
return errors.Wrapf(errors.UnmarshalJSON(resp.Body), "status code: %d", resp.StatusCode)
}
return nil
@ -405,8 +395,7 @@ func (c *Client) DropTable(ctx context.Context, qtid dax.QualifiedTableID) error
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return errors.Errorf("status code: %d: %s", resp.StatusCode, b)
return errors.Wrapf(errors.UnmarshalJSON(resp.Body), "status code: %d", resp.StatusCode)
}
return nil
@ -434,8 +423,7 @@ func (c *Client) CreateField(ctx context.Context, qtid dax.QualifiedTableID, fld
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return errors.Errorf("status code: %d: %s", resp.StatusCode, b)
return errors.Wrapf(errors.UnmarshalJSON(resp.Body), "status code: %d", resp.StatusCode)
}
return nil
@ -464,8 +452,7 @@ func (c *Client) DropField(ctx context.Context, qtid dax.QualifiedTableID, fldNa
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return errors.Errorf("status code: %d: %s", resp.StatusCode, b)
return errors.Wrapf(errors.UnmarshalJSON(resp.Body), "status code: %d", resp.StatusCode)
}
return nil
@ -645,8 +632,7 @@ func (c *Client) RegisterNode(ctx context.Context, node *dax.Node) error {
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return errors.Errorf("registration request to %s status code: %d: %s", url, resp.StatusCode, b)
return errors.Wrapf(errors.UnmarshalJSON(resp.Body), "registration request to %s status code: %d", url, resp.StatusCode)
}
return nil
@ -676,8 +662,7 @@ func (c *Client) CheckInNode(ctx context.Context, node *dax.Node) error {
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return errors.Errorf("status code: %d: %s", resp.StatusCode, b)
return errors.Wrapf(errors.UnmarshalJSON(resp.Body), "status code: %d", resp.StatusCode)
}
return nil

View file

@ -267,6 +267,10 @@ func (c *Controller) RegisterNode(ctx context.Context, n *dax.Node) error {
// from its list (perhaps due to a network fault) and therefore the node needs
// to be re-registered.
func (c *Controller) CheckInNode(ctx context.Context, n *dax.Node) error {
if n == nil || n.Address == "" {
return NewErrNodeKeyInvalid("")
}
tx, err := c.BoltDB.BeginTx(ctx, false)
if err != nil {
return errors.Wrap(err, "beginning tx")
@ -737,6 +741,10 @@ func (c *Controller) SetDatabaseOption(ctx context.Context, qdbid dax.QualifiedD
}
func (c *Controller) Databases(ctx context.Context, orgID dax.OrganizationID, ids ...dax.DatabaseID) ([]*dax.QualifiedDatabase, error) {
if orgID == "" {
return nil, dax.NewErrOrganizationIDDoesNotExist(orgID)
}
tx, err := c.BoltDB.BeginTx(ctx, false)
if err != nil {
return nil, errors.Wrap(err, "beginning tx")

View file

@ -6,6 +6,7 @@ import (
"github.com/featurebasedb/featurebase/v3/dax"
"github.com/featurebasedb/featurebase/v3/dax/controller"
"github.com/featurebasedb/featurebase/v3/errors"
"github.com/gorilla/mux"
)
@ -80,7 +81,7 @@ func (s *server) postCreateDatabase(w http.ResponseWriter, r *http.Request) {
err := s.controller.CreateDatabase(ctx, req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, errors.MarshalJSON(err), http.StatusBadRequest)
return
}
@ -105,7 +106,7 @@ func (s *server) postDropDatabase(w http.ResponseWriter, r *http.Request) {
err := s.controller.DropDatabase(ctx, req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, errors.MarshalJSON(err), http.StatusBadRequest)
return
}
}
@ -124,7 +125,7 @@ func (s *server) postDatabaseByID(w http.ResponseWriter, r *http.Request) {
}
resp, err := s.controller.DatabaseByID(ctx, qdbid)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, errors.MarshalJSON(err), http.StatusBadRequest)
return
}
@ -148,7 +149,7 @@ func (s *server) postDatabaseByName(w http.ResponseWriter, r *http.Request) {
}
resp, err := s.controller.DatabaseByName(ctx, req.OrganizationID, req.Name)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, errors.MarshalJSON(err), http.StatusBadRequest)
return
}
@ -180,7 +181,7 @@ func (s *server) postDatabases(w http.ResponseWriter, r *http.Request) {
resp, err := s.controller.Databases(ctx, req.OrganizationID, ids...)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, errors.MarshalJSON(err), http.StatusBadRequest)
return
}
@ -208,7 +209,7 @@ func (s *server) patchDatabaseOptions(w http.ResponseWriter, r *http.Request) {
}
if err := s.controller.SetDatabaseOption(r.Context(), req.QualifiedDatabaseID, req.Option, req.Value); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, errors.MarshalJSON(err), http.StatusBadRequest)
return
}
}
@ -237,7 +238,7 @@ func (s *server) postCreateTable(w http.ResponseWriter, r *http.Request) {
err := s.controller.CreateTable(ctx, req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, errors.MarshalJSON(err), http.StatusBadRequest)
return
}
@ -261,7 +262,7 @@ func (s *server) postTable(w http.ResponseWriter, r *http.Request) {
}
resp, err := s.controller.TableByID(ctx, qtid)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, errors.MarshalJSON(err), http.StatusBadRequest)
return
}
@ -286,7 +287,7 @@ func (s *server) postTableID(w http.ResponseWriter, r *http.Request) {
qtbl, err := s.controller.TableByName(ctx, req.QualifiedDatabaseID, req.Name)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, errors.MarshalJSON(err), http.StatusBadRequest)
return
}
qtid := qtbl.QualifiedID()
@ -312,7 +313,7 @@ func (s *server) postDropTable(w http.ResponseWriter, r *http.Request) {
err := s.controller.DropTable(ctx, req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, errors.MarshalJSON(err), http.StatusBadRequest)
return
}
}
@ -334,7 +335,7 @@ func (s *server) postCreateField(w http.ResponseWriter, r *http.Request) {
err := s.controller.CreateField(ctx, qtid, req.Field)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, errors.MarshalJSON(err), http.StatusBadRequest)
return
}
}
@ -361,7 +362,7 @@ func (s *server) postDropField(w http.ResponseWriter, r *http.Request) {
err := s.controller.DropField(ctx, qtid, req.Field)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, errors.MarshalJSON(err), http.StatusBadRequest)
return
}
}
@ -389,7 +390,7 @@ func (s *server) postTables(w http.ResponseWriter, r *http.Request) {
resp, err := s.controller.Tables(ctx, qdbid, ids...)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, errors.MarshalJSON(err), http.StatusBadRequest)
return
}
@ -614,7 +615,7 @@ func (s *server) postRegisterNode(w http.ResponseWriter, r *http.Request) {
}
if err := s.controller.RegisterNode(ctx, node); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, errors.MarshalJSON(err), http.StatusBadRequest)
return
}
@ -699,7 +700,7 @@ func (s *server) postCheckInNode(w http.ResponseWriter, r *http.Request) {
}
if err := s.controller.CheckInNode(ctx, node); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, errors.MarshalJSON(err), http.StatusBadRequest)
return
}

View file

@ -7,6 +7,8 @@ import (
)
const (
ErrOrganizationIDDoesNotExist errors.Code = "OrganizationIDDoesNotExist"
ErrDatabaseIDExists errors.Code = "DatabaseIDExists"
ErrDatabaseIDDoesNotExist errors.Code = "DatabaseIDDoesNotExist"
ErrDatabaseNameDoesNotExist errors.Code = "DatabaseNameDoesNotExist"
@ -29,6 +31,13 @@ const (
// The following are helper functions for constructing coded errors containing
// relevant information about the specific error.
func NewErrOrganizationIDDoesNotExist(orgID OrganizationID) error {
return errors.New(
ErrOrganizationIDDoesNotExist,
fmt.Sprintf("Organization ID '%s' does not exist", orgID),
)
}
func NewErrDatabaseIDExists(qdbid QualifiedDatabaseID) error {
return errors.New(
ErrDatabaseIDExists,

View file

@ -13,10 +13,13 @@ import (
featurebase "github.com/featurebasedb/featurebase/v3"
"github.com/featurebasedb/featurebase/v3/dax"
"github.com/featurebasedb/featurebase/v3/dax/controller"
controllerclient "github.com/featurebasedb/featurebase/v3/dax/controller/client"
"github.com/featurebasedb/featurebase/v3/dax/controller/schemar"
queryerclient "github.com/featurebasedb/featurebase/v3/dax/queryer/client"
"github.com/featurebasedb/featurebase/v3/dax/server"
"github.com/featurebasedb/featurebase/v3/dax/server/test"
"github.com/featurebasedb/featurebase/v3/errors"
"github.com/featurebasedb/featurebase/v3/logger"
"github.com/featurebasedb/featurebase/v3/sql3/test/defs"
goerrors "github.com/pkg/errors"
@ -706,6 +709,143 @@ func TestDAXIntegration(t *testing.T) {
assert.Equal(t, partitions2, nodes[2].Partitions)
}
})
// These tests are to test the traversal of CodedErrors across HTTP
// The client is also wrapped to maintain the integrity of the interface to prevent any
// additional methods added to the client without the appropriate tests for CodedErrors
t.Run("HTTPError", func(t *testing.T) {
mc := test.MustRunManagedCommand(t)
defer mc.Close()
svcmgr := mc.Manage()
ctx := context.Background()
t.Run("controller", func(t *testing.T) {
// Set up Controller client.
client := newWrappedControllerClient(controllerclient.New(svcmgr.Controller.Address(), svcmgr.Logger))
t.Run("Registrar", func(t *testing.T) {
t.Run("RegisterNode", func(t *testing.T) {
node := &dax.Node{}
err := client.RegisterNode(ctx, node)
if assert.Error(t, err) {
assert.True(t, errors.Is(err, controller.ErrCodeNodeKeyInvalid))
}
})
t.Run("CheckInNode", func(t *testing.T) {
node := &dax.Node{}
err := client.CheckInNode(ctx, node)
if assert.Error(t, err) {
assert.True(t, errors.Is(err, controller.ErrCodeNodeKeyInvalid))
}
})
})
t.Run("Schemar", func(t *testing.T) {
qtid := dax.QualifiedTableID{
ID: "BAD",
}
qtbl := dax.QualifiedTable{}
qdbid := dax.QualifiedDatabaseID{}
tbfld := &dax.Field{}
t.Run("CreateDatabase", func(t *testing.T) {
err := client.CreateDatabase(ctx, nil)
if assert.Error(t, err) {
assert.True(t, errors.Is(err, schemar.ErrCodeDatabaseNameInvalid))
}
})
t.Run("DropDatabase", func(t *testing.T) {
err := client.DropDatabase(ctx, qdbid)
if assert.Error(t, err) {
assert.True(t, errors.Is(err, dax.ErrDatabaseIDDoesNotExist))
}
})
t.Run("DatabaseByName", func(t *testing.T) {
_, err := client.DatabaseByName(ctx, "", "")
if assert.Error(t, err) {
assert.True(t, errors.Is(err, dax.ErrDatabaseNameDoesNotExist))
}
})
t.Run("DatabaseByID", func(t *testing.T) {
_, err := client.DatabaseByID(ctx, qdbid)
if assert.Error(t, err) {
assert.True(t, errors.Is(err, dax.ErrDatabaseIDDoesNotExist))
}
})
t.Run("SetDatabaseOption", func(t *testing.T) {
err := client.SetDatabaseOption(ctx, qdbid, "", "")
if assert.Error(t, err) {
assert.True(t, errors.Is(err, dax.ErrDatabaseIDDoesNotExist))
}
})
t.Run("Databases", func(t *testing.T) {
_, err := client.Databases(ctx, "", dbID)
if assert.Error(t, err) {
assert.True(t, errors.Is(err, dax.ErrOrganizationIDDoesNotExist))
}
})
t.Run("CreateTable", func(t *testing.T) {
err := client.CreateTable(ctx, &qtbl)
if assert.Error(t, err) {
assert.True(t, errors.Is(err, schemar.ErrCodeTableNameInvalid))
}
})
t.Run("DropTable", func(t *testing.T) {
err := client.DropTable(ctx, qtid)
if assert.Error(t, err) {
assert.True(t, errors.Is(err, dax.ErrTableIDDoesNotExist))
}
})
t.Run("TableByName", func(t *testing.T) {
req := dax.QualifiedTableID{}
_, err := client.TableByName(ctx, qdbid, req.Name)
if assert.Error(t, err) {
assert.True(t, errors.Is(err, dax.ErrTableNameDoesNotExist))
}
})
t.Run("TableByID", func(t *testing.T) {
_, err := client.TableByID(ctx, qtid)
if assert.Error(t, err) {
assert.True(t, errors.Is(err, dax.ErrTableIDDoesNotExist))
}
})
//Todo: make it so "Tables" doesn't return all tables if error is present
t.Run("Tables", func(t *testing.T) {
// _, err := client.Tables(ctx, qdbid)
// //the error is also nil
// if err != nil {
// assert.True(t, errors.Is(err, ""))
// }
})
t.Run("CreateField", func(t *testing.T) {
err := client.CreateField(ctx, qtid, tbfld)
if assert.Error(t, err) {
assert.True(t, errors.Is(err, schemar.ErrCodeFieldNameInvalid))
}
})
t.Run("DropField", func(t *testing.T) {
err := client.DropField(ctx, qtid, tbfld.Name)
if assert.Error(t, err) {
assert.True(t, errors.Is(err, dax.ErrTableIDDoesNotExist))
}
})
})
})
})
}
func dirIsEmpty(t *testing.T, name string) bool {

View file

@ -0,0 +1,93 @@
package dax_test
import (
"context"
"github.com/featurebasedb/featurebase/v3/dax"
"github.com/featurebasedb/featurebase/v3/dax/computer"
controllerclient "github.com/featurebasedb/featurebase/v3/dax/controller/client"
)
var _ computer.Registrar = (*wrappedControllerClient)(nil)
var _ dax.Schemar = (*wrappedControllerClient)(nil)
// wrappedControllerClient is a wrapper around the controller client which we
// use in tests to ensure that all of the methods for the computer.Registrar
// interface are covered by these tests. The idea being that if someone modifes
// the interface to include a new `Foo()` method, this test will no longer
// compile, and the developer will be directected here to add the appropriate
// tests. There's probably a more automated way to do this wil the `reflect`
// package, but that seems overly compilicated right now.
type wrappedControllerClient struct {
cli *controllerclient.Client
}
func newWrappedControllerClient(cli *controllerclient.Client) *wrappedControllerClient {
return &wrappedControllerClient{
cli: cli,
}
}
// Registrar
func (w *wrappedControllerClient) RegisterNode(ctx context.Context, node *dax.Node) error {
return w.cli.RegisterNode(ctx, node)
}
func (w *wrappedControllerClient) CheckInNode(ctx context.Context, node *dax.Node) error {
return w.cli.CheckInNode(ctx, node)
}
// Schemar
func (w *wrappedControllerClient) CreateDatabase(ctx context.Context, qdb *dax.QualifiedDatabase) error {
return w.cli.CreateDatabase(ctx, qdb)
}
func (w *wrappedControllerClient) DropDatabase(ctx context.Context, qdbid dax.QualifiedDatabaseID) error {
return w.cli.DropDatabase(ctx, qdbid)
}
func (w *wrappedControllerClient) DatabaseByName(ctx context.Context, orgID dax.OrganizationID, dbname dax.DatabaseName) (*dax.QualifiedDatabase, error) {
return w.cli.DatabaseByName(ctx, orgID, dbname)
}
func (w *wrappedControllerClient) DatabaseByID(ctx context.Context, qdbid dax.QualifiedDatabaseID) (*dax.QualifiedDatabase, error) {
return w.cli.DatabaseByID(ctx, qdbid)
}
func (w *wrappedControllerClient) SetDatabaseOption(ctx context.Context, qdbid dax.QualifiedDatabaseID, option string, value string) error {
return w.cli.SetDatabaseOption(ctx, qdbid, option, value)
}
func (w *wrappedControllerClient) Databases(ctx context.Context, orgID dax.OrganizationID, dbIDs ...dax.DatabaseID) ([]*dax.QualifiedDatabase, error) {
return w.cli.Databases(ctx, orgID, dbIDs...)
}
func (w *wrappedControllerClient) CreateTable(ctx context.Context, qtbl *dax.QualifiedTable) error {
return w.cli.CreateTable(ctx, qtbl)
}
func (w *wrappedControllerClient) DropTable(ctx context.Context, qtid dax.QualifiedTableID) error {
return w.cli.DropTable(ctx, qtid)
}
func (w *wrappedControllerClient) TableByName(ctx context.Context, qdbid dax.QualifiedDatabaseID, tname dax.TableName) (*dax.QualifiedTable, error) {
return w.cli.TableByName(ctx, qdbid, tname)
}
func (w *wrappedControllerClient) TableByID(ctx context.Context, qtid dax.QualifiedTableID) (*dax.QualifiedTable, error) {
return w.cli.TableByID(ctx, qtid)
}
func (w *wrappedControllerClient) Tables(ctx context.Context, qdbid dax.QualifiedDatabaseID, tids ...dax.TableID) ([]*dax.QualifiedTable, error) {
return w.cli.Tables(ctx, qdbid, tids...)
}
func (w *wrappedControllerClient) CreateField(ctx context.Context, qtid dax.QualifiedTableID, fld *dax.Field) error {
return w.cli.CreateField(ctx, qtid, fld)
}
func (w *wrappedControllerClient) DropField(ctx context.Context, qtid dax.QualifiedTableID, fname dax.FieldName) error {
return w.cli.DropField(ctx, qtid, fname)
}

View file

@ -3,6 +3,9 @@
package errors
import (
"encoding/json"
"io"
"github.com/pkg/errors"
)
@ -12,8 +15,8 @@ type Code string
func New(code Code, message string) error {
return errors.WithStack(codedError{
code: code,
message: message,
Code: code,
Message: message,
})
}
@ -33,7 +36,7 @@ func Errorf(format string, args ...interface{}) error {
// an error Code instead of an error.
func Is(err error, target Code) bool {
match := codedError{
code: target,
Code: target,
}
return errors.Is(err, match)
}
@ -65,12 +68,13 @@ func Wrapf(err error, fmt string, args ...interface{}) error {
// codedError is the fundamental type used by this package to provide coded
// errors.
type codedError struct {
code Code
message string
Code Code `json:"code"`
Message string `json:"message"`
Wrapped string `json:"wrapped,omitempty"`
}
func (ce codedError) Error() string {
return ce.message
return ce.Message
}
// func (ce codedError) As(target interface{}) bool {
@ -78,7 +82,7 @@ func (ce codedError) Error() string {
// }
func (ce codedError) Is(err error) bool {
if e, ok := err.(codedError); ok && ce.code == e.code {
if e, ok := err.(codedError); ok && ce.Code == e.Code {
return true
}
return false
@ -87,3 +91,48 @@ func (ce codedError) Is(err error) bool {
const (
ErrUncoded Code = "Uncoded"
)
// MarshalJSON returns the provided error as a json object (as a string)
// representing a codedError. If err is not already a codedError, the json
// object will still represent a codedError but its `code` value will be empty.
// Note: an empty code here is intentional and is different from code
// `errors.Uncoded` which is a valid code; it just means the developer returned
// a codedError but didn't bother to choose (or create) a useful error code.
func MarshalJSON(err error) string {
cause := Cause(err)
var out *codedError
switch v := cause.(type) {
case codedError:
v.Wrapped = err.Error()
out = &v
default:
out = &codedError{
Message: cause.Error(),
Wrapped: err.Error(),
}
}
// Marshal the codedError to json as output.
j, jerr := json.Marshal(out)
if jerr != nil {
return out.Error()
}
return string(j)
}
// UnmarshalJSON converts the byte slice into a codedError. If the bytes can't
// unmarshal to a codedError, a normal error will be returned containing the
// string value of the byte slice.
func UnmarshalJSON(r io.Reader) error {
b, _ := io.ReadAll(r)
out := &codedError{}
if err := json.Unmarshal(b, out); err != nil {
return errors.New(string(b))
}
return out
}