Skip to content

Commit e7828dc

Browse files
committed
merge prerelease branch
1 parent 270e6af commit e7828dc

20 files changed

+328
-116
lines changed

ldattr/errors.go

Lines changed: 0 additions & 12 deletions
This file was deleted.

ldattr/ref.go

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"strconv"
66
"strings"
77

8+
"github.com/launchdarkly/go-sdk-common/v3/lderrors"
89
"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
910

1011
"github.com/launchdarkly/go-jsonstream/v2/jreader"
@@ -81,7 +82,7 @@ type attrRefComponent struct {
8182
// parameter will consider it invalid.
8283
func NewRef(referenceString string) Ref {
8384
if referenceString == "" || referenceString == "/" {
84-
return Ref{err: errAttributeEmpty, rawPath: referenceString}
85+
return Ref{err: lderrors.ErrAttributeEmpty{}, rawPath: referenceString}
8586
}
8687
if referenceString[0] != '/' {
8788
// When there is no leading slash, this is a simple attribute reference with no character escaping.
@@ -94,18 +95,18 @@ func NewRef(referenceString string) Ref {
9495
if unescaped, ok := unescapePath(path); ok {
9596
return Ref{singlePathComponent: unescaped, rawPath: referenceString}
9697
}
97-
return Ref{err: errAttributeInvalidEscape, rawPath: referenceString}
98+
return Ref{err: lderrors.ErrAttributeInvalidEscape{}, rawPath: referenceString}
9899
}
99100
parts := strings.Split(path, "/")
100101
ret := Ref{rawPath: referenceString, components: make([]attrRefComponent, 0, len(parts))}
101102
for _, p := range parts {
102103
if p == "" {
103-
ret.err = errAttributeExtraSlash
104+
ret.err = lderrors.ErrAttributeExtraSlash{}
104105
return ret
105106
}
106107
unescaped, ok := unescapePath(p)
107108
if !ok {
108-
return Ref{err: errAttributeInvalidEscape, rawPath: referenceString}
109+
return Ref{err: lderrors.ErrAttributeInvalidEscape{}, rawPath: referenceString}
109110
}
110111
component := attrRefComponent{name: unescaped}
111112
if p[0] >= '0' && p[0] <= '9' {
@@ -129,7 +130,7 @@ func NewRef(referenceString string) Ref {
129130
// or to ldattr.NewRef("/a~1b").
130131
func NewLiteralRef(attrName string) Ref {
131132
if attrName == "" {
132-
return Ref{err: errAttributeEmpty, rawPath: attrName}
133+
return Ref{err: lderrors.ErrAttributeEmpty{}, rawPath: attrName}
133134
}
134135
if attrName[0] != '/' {
135136
// When there is no leading slash, this is a simple attribute reference with no character escaping.
@@ -162,13 +163,9 @@ func (a Ref) Equal(other Ref) bool {
162163

163164
// Err returns nil for a valid Ref, or a non-nil error value for an invalid Ref.
164165
//
165-
// A Ref can only be invalid for the following reasons:
166-
//
167-
// 1. The input string was empty, or consisted only of "/".
168-
//
169-
// 2. A slash-delimited string had a double slash causing one component to be empty, such as "/a//b".
170-
//
171-
// 3. A slash-delimited string contained a "~" character that was not followed by "0" or "1".
166+
// A Ref is invalid if the input string is empty, or starts with a slash but is not a valid
167+
// slash-delimited path, or starts with a slash and contains an invalid escape sequence. For a list of
168+
// the possible validation errors, see the ErrAttribute___ types in the lderrors package.
172169
//
173170
// Otherwise, the Ref is valid, but that does not guarantee that such an attribute exists in any
174171
// given Context. For instance, NewRef("name") is a valid Ref, but a specific Context might or might
@@ -177,7 +174,7 @@ func (a Ref) Equal(other Ref) bool {
177174
// See comments on the Ref type for more details of the attribute reference syntax.
178175
func (a Ref) Err() error {
179176
if a.err == nil && a.rawPath == "" {
180-
return errAttributeEmpty
177+
return lderrors.ErrAttributeEmpty{}
181178
}
182179
return a.err
183180
}

ldattr/ref_test.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"testing"
77

8+
"github.com/launchdarkly/go-sdk-common/v3/lderrors"
89
"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
910

1011
"github.com/stretchr/testify/assert"
@@ -15,15 +16,15 @@ func TestRefInvalid(t *testing.T) {
1516
input string
1617
expectedError error
1718
}{
18-
{"", errAttributeEmpty},
19-
{"/", errAttributeEmpty},
20-
{"//", errAttributeExtraSlash},
21-
{"/a//b", errAttributeExtraSlash},
22-
{"/a/b/", errAttributeExtraSlash},
23-
{"/a~x", errAttributeInvalidEscape},
24-
{"/a~", errAttributeInvalidEscape},
25-
{"/a/b~x", errAttributeInvalidEscape},
26-
{"/a/b~", errAttributeInvalidEscape},
19+
{"", lderrors.ErrAttributeEmpty{}},
20+
{"/", lderrors.ErrAttributeEmpty{}},
21+
{"//", lderrors.ErrAttributeExtraSlash{}},
22+
{"/a//b", lderrors.ErrAttributeExtraSlash{}},
23+
{"/a/b/", lderrors.ErrAttributeExtraSlash{}},
24+
{"/a~x", lderrors.ErrAttributeInvalidEscape{}},
25+
{"/a~", lderrors.ErrAttributeInvalidEscape{}},
26+
{"/a/b~x", lderrors.ErrAttributeInvalidEscape{}},
27+
{"/a/b~", lderrors.ErrAttributeInvalidEscape{}},
2728
} {
2829
t.Run(fmt.Sprintf("input string %q", p.input), func(t *testing.T) {
2930
a := NewRef(p.input)
@@ -37,7 +38,7 @@ func TestRefInvalid(t *testing.T) {
3738
t.Run("uninitialized", func(t *testing.T) {
3839
var a Ref
3940
assert.False(t, a.IsDefined())
40-
assert.Equal(t, errAttributeEmpty, a.Err())
41+
assert.Equal(t, lderrors.ErrAttributeEmpty{}, a.Err())
4142
assert.Equal(t, "", a.String())
4243
assert.Equal(t, 0, a.Depth())
4344
})
@@ -103,7 +104,7 @@ func TestNewLiteralRef(t *testing.T) {
103104
assert.Equal(t, 1, a3.Depth())
104105

105106
a4 := NewLiteralRef("")
106-
assert.Equal(t, errAttributeEmpty, a4.Err())
107+
assert.Equal(t, lderrors.ErrAttributeEmpty{}, a4.Err())
107108
}
108109

109110
func TestRefComponents(t *testing.T) {

ldcontext/builder_multi.go

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
package ldcontext
22

33
import (
4-
"errors"
5-
"fmt"
64
"sort"
7-
"strings"
5+
6+
"github.com/launchdarkly/go-sdk-common/v3/lderrors"
87
)
98

109
const defaultMultiBuilderCapacity = 3 // arbitrary value based on presumed likely use cases
@@ -53,14 +52,14 @@ func NewMultiBuilder() *MultiBuilder {
5352
// than a multi-kind context.
5453
func (m *MultiBuilder) Build() Context {
5554
if len(m.contexts) == 0 {
56-
return Context{defined: true, err: errContextKindMultiWithNoKinds}
55+
return Context{defined: true, err: lderrors.ErrContextKindMultiWithNoKinds{}}
5756
}
5857

5958
if len(m.contexts) == 1 {
6059
// Never return a multi-kind context with just one kind; instead return the individual one
6160
c := m.contexts[0]
6261
if c.Multiple() {
63-
return Context{defined: true, err: errContextKindMultiWithinMulti}
62+
return Context{defined: true, err: lderrors.ErrContextKindMultiWithinMulti{}}
6463
}
6564
return c
6665
}
@@ -72,14 +71,17 @@ func (m *MultiBuilder) Build() Context {
7271
sort.Slice(m.contexts, func(i, j int) bool { return m.contexts[i].Kind() < m.contexts[j].Kind() })
7372

7473
// Check for conditions that could make a multi-kind context invalid
75-
var errs []string
74+
var individualErrors map[string]error
7675
nestedMulti := false
7776
duplicates := false
7877
for i, c := range m.contexts {
7978
err := c.Err()
8079
switch {
8180
case err != nil: // one of the individual contexts already had an error
82-
errs = append(errs, fmt.Sprintf("(%s) %s", c.Kind(), err.Error()))
81+
if individualErrors == nil {
82+
individualErrors = make(map[string]error)
83+
}
84+
individualErrors[string(c.Kind())] = err
8385
case c.Multiple(): // multi-kind inside multi-kind is not allowed
8486
nestedMulti = true
8587
default:
@@ -91,16 +93,19 @@ func (m *MultiBuilder) Build() Context {
9193
}
9294
}
9395
}
94-
if nestedMulti {
95-
errs = append(errs, errContextKindMultiWithinMulti.Error())
96-
}
97-
if duplicates {
98-
errs = append(errs, errContextKindMultiDuplicates.Error())
96+
var err error
97+
switch {
98+
case nestedMulti:
99+
err = lderrors.ErrContextKindMultiWithinMulti{}
100+
case duplicates:
101+
err = lderrors.ErrContextKindMultiDuplicates{}
102+
case len(individualErrors) != 0:
103+
err = lderrors.ErrContextPerKindErrors{Errors: individualErrors}
99104
}
100-
if len(errs) != 0 {
105+
if err != nil {
101106
return Context{
102107
defined: true,
103-
err: errors.New(strings.Join(errs, ", ")),
108+
err: err,
104109
}
105110
}
106111

ldcontext/builder_multi_test.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ldcontext
33
import (
44
"testing"
55

6+
"github.com/launchdarkly/go-sdk-common/v3/lderrors"
67
"github.com/stretchr/testify/assert"
78
)
89

@@ -57,25 +58,25 @@ func TestMultiBuilderErrors(t *testing.T) {
5758
}
5859

5960
t.Run("empty", func(t *testing.T) {
60-
verifyError(t, NewMultiBuilder(), errContextKindMultiWithNoKinds)
61+
verifyError(t, NewMultiBuilder(), lderrors.ErrContextKindMultiWithNoKinds{})
6162
})
6263

6364
t.Run("nested multi", func(t *testing.T) {
6465
sub1 := NewWithKind("org", "my-key")
6566
sub2 := NewMulti(New("user-key"), NewWithKind("org", "other"))
6667
b0 := NewMultiBuilder().Add(sub1).Add(sub2)
67-
verifyError(t, b0, errContextKindMultiWithinMulti)
68+
verifyError(t, b0, lderrors.ErrContextKindMultiWithinMulti{})
6869

6970
b1 := NewMultiBuilder().Add(sub2)
70-
verifyError(t, b1, errContextKindMultiWithinMulti)
71+
verifyError(t, b1, lderrors.ErrContextKindMultiWithinMulti{})
7172
})
7273

7374
t.Run("duplicate kind", func(t *testing.T) {
7475
sub1 := NewWithKind("org", "my-org-key")
7576
sub2 := NewWithKind("user", "my-user-key")
7677
sub3 := NewWithKind("org", "other-org-key")
7778
b := NewMultiBuilder().Add(sub1).Add(sub2).Add(sub3)
78-
verifyError(t, b, errContextKindMultiDuplicates)
79+
verifyError(t, b, lderrors.ErrContextKindMultiDuplicates{})
7980
})
8081

8182
t.Run("error in individual contexts", func(t *testing.T) {
@@ -85,7 +86,12 @@ func TestMultiBuilderErrors(t *testing.T) {
8586
b := NewMultiBuilder().Add(sub1).Add(sub2).Add(sub3)
8687
c0 := b.Build()
8788
assert.Error(t, c0.Err())
88-
assert.Regexp(t, "\\(kind1\\).*must not be empty, \\(kind3!\\).*disallowed characters", c0.Err().Error())
89+
if assert.IsType(t, lderrors.ErrContextPerKindErrors{}, c0.Err()) {
90+
e := c0.Err().(lderrors.ErrContextPerKindErrors)
91+
assert.Len(t, e.Errors, 2)
92+
assert.Equal(t, lderrors.ErrContextKeyEmpty{}, e.Errors["kind1"])
93+
assert.Equal(t, lderrors.ErrContextKindInvalidChars{}, e.Errors["kind3!"])
94+
}
8995
c1, err := b.TryBuild()
9096
assert.Equal(t, c0.Err(), c1.Err())
9197
assert.Equal(t, c0.Err(), err)

ldcontext/builder_simple.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"net/url"
66

77
"github.com/launchdarkly/go-sdk-common/v3/ldattr"
8+
"github.com/launchdarkly/go-sdk-common/v3/lderrors"
89
"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
910
)
1011

@@ -99,7 +100,7 @@ func (b *Builder) Build() Context {
99100
return Context{defined: true, err: err, kind: b.kind}
100101
}
101102
if b.key == "" && !b.allowEmptyKey {
102-
return Context{defined: true, err: errContextKeyEmpty, kind: b.kind}
103+
return Context{defined: true, err: lderrors.ErrContextKeyEmpty{}, kind: b.kind}
103104
}
104105
// We set the kind in the error cases above because that improves error reporting if this
105106
// context is used within a multi-kind context.

ldcontext/builder_simple_test.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"testing"
66

77
"github.com/launchdarkly/go-sdk-common/v3/ldattr"
8+
"github.com/launchdarkly/go-sdk-common/v3/lderrors"
89
"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
910

1011
m "github.com/launchdarkly/go-test-helpers/v2/matchers"
@@ -19,12 +20,12 @@ type invalidKindTestParams struct {
1920

2021
func makeInvalidKindTestParams() []invalidKindTestParams {
2122
return []invalidKindTestParams{
22-
{"kind", errContextKindCannotBeKind},
23-
{"multi", errContextKindMultiWithSimpleBuilder},
24-
{"örg", errContextKindInvalidChars},
25-
{"o~rg", errContextKindInvalidChars},
26-
{"😀rg", errContextKindInvalidChars},
27-
{"o\trg", errContextKindInvalidChars},
23+
{"kind", lderrors.ErrContextKindCannotBeKind{}},
24+
{"multi", lderrors.ErrContextKindMultiForSingleKind{}},
25+
{"örg", lderrors.ErrContextKindInvalidChars{}},
26+
{"o~rg", lderrors.ErrContextKindInvalidChars{}},
27+
{"😀rg", lderrors.ErrContextKindInvalidChars{}},
28+
{"o\trg", lderrors.ErrContextKindInvalidChars{}},
2829
}
2930
}
3031

@@ -68,12 +69,12 @@ func TestBuilderKeyValidation(t *testing.T) {
6869

6970
c0 := b.Build()
7071
assert.True(t, c0.IsDefined())
71-
assert.Equal(t, errContextKeyEmpty, c0.Err())
72+
assert.Equal(t, lderrors.ErrContextKeyEmpty{}, c0.Err())
7273

7374
c1, err := b.TryBuild()
7475
assert.True(t, c1.IsDefined())
75-
assert.Equal(t, errContextKeyEmpty, c1.Err())
76-
assert.Equal(t, errContextKeyEmpty, err)
76+
assert.Equal(t, lderrors.ErrContextKeyEmpty{}, c1.Err())
77+
assert.Equal(t, lderrors.ErrContextKeyEmpty{}, err)
7778
}
7879

7980
func TestBuilderFullyQualifiedKey(t *testing.T) {

ldcontext/context.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"sort"
66

77
"github.com/launchdarkly/go-sdk-common/v3/ldattr"
8+
"github.com/launchdarkly/go-sdk-common/v3/lderrors"
89
"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
910
)
1011

@@ -44,14 +45,8 @@ func (c Context) IsDefined() bool {
4445
//
4546
// A valid Context is one that can be used in SDK operations. An invalid Context is one that is
4647
// missing necessary attributes or has invalid attributes, indicating an incorrect usage of the
47-
// SDK API. The only ways for a Context to be invalid are:
48-
//
49-
// - It has a disallowed value for the Kind property. See Builder.Kind().
50-
// - It is a single-kind Context whose Key is empty.
51-
// - It is a multi-kind Context that does not have any kinds. See MultiBuilder.
52-
// - It is a multi-kind Context where the same kind appears more than once.
53-
// - It is a multi-kind Context where at least one of the nested Contexts had an error.
54-
// - It is an uninitialized empty Context{} value.
48+
// SDK API. For a complete list of the ways a Context can be invalid, see the ErrContext___
49+
// types in the lderrors package.
5550
//
5651
// Since in normal usage it is easy for applications to be sure they are using context kinds
5752
// correctly (so that having to constantly check error return values would be needlessly
@@ -63,7 +58,7 @@ func (c Context) IsDefined() bool {
6358
// are not sure if you have a valid Context, you can call Err() to check.
6459
func (c Context) Err() error {
6560
if !c.defined && c.err == nil {
66-
return errContextUninitialized
61+
return lderrors.ErrContextUninitialized{}
6762
}
6863
return c.err
6964
}

0 commit comments

Comments
 (0)