diff --git a/analytics/record.go b/analytics/record.go index fc3eb35..85557c4 100644 --- a/analytics/record.go +++ b/analytics/record.go @@ -24,9 +24,9 @@ import ( "unicode/utf8" "github.com/apigee/apigee-remote-service-golib/v2/auth" + "github.com/apigee/apigee-remote-service-golib/v2/errorset" "github.com/apigee/apigee-remote-service-golib/v2/log" "github.com/google/uuid" - "github.com/hashicorp/go-multierror" ) const ( @@ -102,36 +102,36 @@ func (r Record) validate(now time.Time) error { // Validate that certain fields are set. if r.Organization == "" { - err = multierror.Append(err, errors.New("missing Organization")) + err = errorset.Append(err, errors.New("missing Organization")) } if r.Environment == "" { - err = multierror.Append(err, errors.New("missing Environment")) + err = errorset.Append(err, errors.New("missing Environment")) } if r.GatewayFlowID == "" { - err = multierror.Append(err, errors.New("missing GatewayFlowID")) + err = errorset.Append(err, errors.New("missing GatewayFlowID")) } if r.ClientReceivedStartTimestamp == 0 { - err = multierror.Append(err, errors.New("missing ClientReceivedStartTimestamp")) + err = errorset.Append(err, errors.New("missing ClientReceivedStartTimestamp")) } if r.ClientReceivedEndTimestamp == 0 { - err = multierror.Append(err, errors.New("missing ClientReceivedEndTimestamp")) + err = errorset.Append(err, errors.New("missing ClientReceivedEndTimestamp")) } if r.ClientReceivedStartTimestamp > r.ClientReceivedEndTimestamp { - err = multierror.Append(err, errors.New("ClientReceivedStartTimestamp > ClientReceivedEndTimestamp")) + err = errorset.Append(err, errors.New("ClientReceivedStartTimestamp > ClientReceivedEndTimestamp")) } // Validate that timestamps make sense. ts := time.Unix(r.ClientReceivedStartTimestamp/1000, 0) if ts.After(now.Add(time.Minute)) { // allow a minute of tolerance - err = multierror.Append(err, errors.New("ClientReceivedStartTimestamp cannot be in the future")) + err = errorset.Append(err, errors.New("ClientReceivedStartTimestamp cannot be in the future")) } if ts.Before(now.Add(-90 * 24 * time.Hour)) { - err = multierror.Append(err, errors.New("ClientReceivedStartTimestamp cannot be more than 90 days old")) + err = errorset.Append(err, errors.New("ClientReceivedStartTimestamp cannot be more than 90 days old")) } for _, attr := range r.Attributes { if validateErr := validateAttribute(attr); validateErr != nil { - err = multierror.Append(err, validateErr) + err = errorset.Append(err, validateErr) } } diff --git a/analytics/recovery.go b/analytics/recovery.go index a878580..3c64f8c 100644 --- a/analytics/recovery.go +++ b/analytics/recovery.go @@ -22,8 +22,8 @@ import ( "os" "path/filepath" + "github.com/apigee/apigee-remote-service-golib/v2/errorset" "github.com/apigee/apigee-remote-service-golib/v2/log" - "github.com/hashicorp/go-multierror" ) // crashRecovery cleans up the temp and staging dirs post-crash. This function @@ -39,13 +39,13 @@ func (m *manager) crashRecovery() error { tempDir := m.getTempDir(tenant) tempFiles, err := os.ReadDir(tempDir) if err != nil { - errs = multierror.Append(errs, err) + errs = errorset.Append(errs, err) continue } err = m.prepTenant(tenant) if err != nil { - errs = multierror.Append(errs, err) + errs = errorset.Append(errs, err) continue } stageDir := m.getStagingDir(tenant) @@ -66,13 +66,13 @@ func (m *manager) crashRecovery() error { dest, err := os.Create(stageFile) if err != nil { - errs = multierror.Append(errs, fmt.Errorf("create recovery file %s: %s", tempDir, err)) + errs = errorset.Append(errs, fmt.Errorf("create recovery file %s: %s", tempDir, err)) continue } if err := m.recoverFile(tempFile, dest); err != nil { - errs = multierror.Append(errs, fmt.Errorf("recoverFile %s: %s", tempDir, err)) + errs = errorset.Append(errs, fmt.Errorf("recoverFile %s: %s", tempDir, err)) if err := os.Remove(stageFile); err != nil { - errs = multierror.Append(errs, fmt.Errorf("remove stage file %s: %s", tempDir, err)) + errs = errorset.Append(errs, fmt.Errorf("remove stage file %s: %s", tempDir, err)) } continue } diff --git a/analytics/staging.go b/analytics/staging.go index 483b8c6..a7b7e27 100644 --- a/analytics/staging.go +++ b/analytics/staging.go @@ -20,8 +20,8 @@ import ( "path/filepath" "sync" + "github.com/apigee/apigee-remote-service-golib/v2/errorset" "github.com/apigee/apigee-remote-service-golib/v2/log" - "github.com/hashicorp/go-multierror" "github.com/prometheus/client_golang/prometheus" ) @@ -54,7 +54,7 @@ func (m *manager) getFilesInStaging() ([]string, error) { stagedFiles, err := os.ReadDir(tenantDirPath) if err != nil { - errs = multierror.Append(errs, fmt.Errorf("ls %s: %s", tenantDirPath, err)) + errs = errorset.Append(errs, fmt.Errorf("ls %s: %s", tenantDirPath, err)) continue } diff --git a/errorset/errorset.go b/errorset/errorset.go new file mode 100644 index 0000000..db42204 --- /dev/null +++ b/errorset/errorset.go @@ -0,0 +1,70 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package errorset + +import ( + "fmt" + "strings" +) + +func Append(err error, errs ...error) error { + es, ok := err.(*Error) + if !ok && err != nil { + es = es.append(err) + } + es = es.append(errs...) + if es.Len() == 0 { + return nil + } + return es +} + +func Errors(err error) []error { + if errset, ok := err.(*Error); ok { + return errset.Errors + } + return []error{err} +} + +type Error struct { + Errors []error +} + +func (es *Error) append(errs ...error) *Error { + if es == nil { + es = &Error{} + } + for _, e := range errs { + if errset, ok := e.(*Error); ok { // unwrap + es.Errors = es.append(errset.Errors...).Errors + } else if e != nil { + es.Errors = append(es.Errors, e) + } + } + return es +} + +func (es *Error) Error() string { + b := &strings.Builder{} + fmt.Fprintf(b, "Error(s):") + for _, e := range es.Errors { + fmt.Fprintf(b, "\n\t* %s", e.Error()) + } + return b.String() +} + +func (es *Error) Len() int { + return len(es.Errors) +} diff --git a/errorset/errorset_test.go b/errorset/errorset_test.go new file mode 100644 index 0000000..b5bd370 --- /dev/null +++ b/errorset/errorset_test.go @@ -0,0 +1,125 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package errorset + +import ( + "errors" + "testing" +) + +func TestNils(t *testing.T) { + err := Append(nil) + if err != nil { + t.Errorf("should be nil") + } + + err = Append(nil, nil) + if err != nil { + t.Errorf("should be nil") + } +} + +func TestNilAppend(t *testing.T) { + want := "Error(s):\n\t* my error" + err := Append(nil, nil, errors.New("my error")) + if err == nil { + t.Fatalf("should not be nil") + } + got := err.Error() + if want != got { + t.Errorf("want: %s, got: %s", want, got) + } + if 1 != err.(*Error).Len() { + t.Errorf("want: %d, got: %d", 1, err.(*Error).Len()) + } +} + +func TestError(t *testing.T) { + want := "Error(s):\n\t* my error" + err := Append(errors.New("my error")) + if err == nil { + t.Errorf("should not be nil") + } + got := err.Error() + if want != got { + t.Errorf("want: %s, got: %s", want, err.Error()) + } + if 1 != err.(*Error).Len() { + t.Errorf("want: %d, got: %d", 1, err.(*Error).Len()) + } +} + +func TestErrors(t *testing.T) { + want := "Error(s):\n\t* my error\n\t* my error2" + err1 := errors.New("my error") + err2 := errors.New("my error2") + err := Append(err1, err2) + if err == nil { + t.Errorf("should not be nil") + } + got := err.Error() + if want != got { + t.Errorf("want: %s, got: %s", want, err.Error()) + } + if 2 != err.(*Error).Len() { + t.Errorf("want: %d, got: %d", 2, err.(*Error).Len()) + } + + errs := Errors(err1) + if err1 != errs[0] { + t.Errorf("want: %v, got: %v", err1, errs[0]) + } + + errs = Errors(err) + if err1 != errs[0] { + t.Errorf("want: %v, got: %v", err1, errs[0]) + } + if err2 != errs[1] { + t.Errorf("want: %v, got: %v", err2, errs[1]) + } +} + +func TestMultiError(t *testing.T) { + want := "Error(s):\n\t* my error\n\t* my error2" + err := Append(errors.New("my error")) + err = Append(err, errors.New("my error2")) + if err == nil { + t.Errorf("should not be nil") + } + got := err.Error() + if want != got { + t.Errorf("want: %s, got: %s", want, err.Error()) + } + if 2 != err.(*Error).Len() { + t.Errorf("want: %d, got: %d", 2, err.(*Error).Len()) + } +} + +func TestUnwrap(t *testing.T) { + want := "Error(s):\n\t* my error3\n\t* my error\n\t* my error2" + wrapped := Append(errors.New("my error")) + wrapped = Append(wrapped, errors.New("my error2")) + err := Append(errors.New("my error3"), wrapped) + if err == nil { + t.Errorf("should not be nil") + } + got := err.Error() + if want != got { + t.Errorf("want: %s, got: %s", want, err.Error()) + } + if 3 != err.(*Error).Len() { + t.Errorf("want: %d, got: %d", 3, err.(*Error).Len()) + } +} diff --git a/go.mod b/go.mod index 6b45b3c..fc090c8 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.16 require ( github.com/google/uuid v1.2.0 - github.com/hashicorp/go-multierror v1.1.1 github.com/lestrrat-go/backoff/v2 v2.0.8 github.com/lestrrat-go/jwx v1.1.5 github.com/pkg/errors v0.9.1