From d7c6258f1615b90a0c35294230c77f62a8390761 Mon Sep 17 00:00:00 2001 From: David Kagan <102766847+DKagan07@users.noreply.github.com> Date: Mon, 27 Feb 2023 16:34:51 -0500 Subject: [PATCH] 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 --- dax/controller/client/client.go | 45 ++++------ dax/controller/controller.go | 8 ++ dax/controller/http/handler.go | 31 +++---- dax/errors.go | 9 ++ dax/test/dax/dax_test.go | 140 ++++++++++++++++++++++++++++++++ dax/test/dax/wrappers_test.go | 93 +++++++++++++++++++++ errors/errors.go | 63 ++++++++++++-- 7 files changed, 337 insertions(+), 52 deletions(-) create mode 100644 dax/test/dax/wrappers_test.go diff --git a/dax/controller/client/client.go b/dax/controller/client/client.go index b723b6fbc..b4e2a27af 100644 --- a/dax/controller/client/client.go +++ b/dax/controller/client/client.go @@ -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 diff --git a/dax/controller/controller.go b/dax/controller/controller.go index c5244a8a2..9d1539a84 100644 --- a/dax/controller/controller.go +++ b/dax/controller/controller.go @@ -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") diff --git a/dax/controller/http/handler.go b/dax/controller/http/handler.go index 79c9794b7..3ec073888 100644 --- a/dax/controller/http/handler.go +++ b/dax/controller/http/handler.go @@ -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 } diff --git a/dax/errors.go b/dax/errors.go index fb9416447..a01c6a148 100644 --- a/dax/errors.go +++ b/dax/errors.go @@ -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, diff --git a/dax/test/dax/dax_test.go b/dax/test/dax/dax_test.go index d3536183f..b4e757f71 100644 --- a/dax/test/dax/dax_test.go +++ b/dax/test/dax/dax_test.go @@ -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 { diff --git a/dax/test/dax/wrappers_test.go b/dax/test/dax/wrappers_test.go new file mode 100644 index 000000000..2596ee6d6 --- /dev/null +++ b/dax/test/dax/wrappers_test.go @@ -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) +} diff --git a/errors/errors.go b/errors/errors.go index 230788aaf..dabc52d20 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -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 +}