Skip to content

Commit ae89482

Browse files
committed
add errors/ package
1 parent 2d1791c commit ae89482

File tree

9 files changed

+481
-2
lines changed

9 files changed

+481
-2
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
Originally this library was written for [BOSH CLI v2](http://bosh.io/docs/cli-v2.html). Given its generic nature, it's been extracted as a separate, easily importable library.
44

5+
- `errors` package: helps a bit with formatting of errors
6+
- `ui` package: helps with printing CLI content
7+
- `table` package: helps format CLI content as a table
8+
59
Examples:
610

7-
- [Table](examples/table.go)
11+
- [Table](examples/table/main.go)

errors/error_piece.go

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
package errors
2+
3+
import (
4+
"regexp"
5+
"strings"
6+
)
7+
8+
const (
9+
braketOpen = "["
10+
parenOpen = "("
11+
curlyOpen = "{"
12+
rootOpen = "*"
13+
customOpen = "~"
14+
rawOpen = "...|"
15+
16+
braketClose = "]"
17+
parenClose = ")"
18+
curlyClose = "}"
19+
rawClose = "|..."
20+
)
21+
22+
var (
23+
sepOpeners = map[string]string{
24+
braketOpen: string(braketOpen),
25+
parenOpen: string(parenOpen),
26+
curlyOpen: string(curlyOpen),
27+
rootOpen: "",
28+
customOpen: "",
29+
rawOpen: "",
30+
}
31+
sepClosers = map[string]string{
32+
braketOpen: string(braketClose),
33+
parenOpen: string(parenClose),
34+
curlyOpen: string(curlyClose),
35+
rootOpen: "",
36+
customOpen: "",
37+
rawOpen: "",
38+
}
39+
)
40+
41+
type ErrorPiece struct {
42+
Value string
43+
44+
ContainerType string // eg *,{,[,(
45+
Pieces []*ErrorPiece
46+
47+
ReorganizePiecesAroundCommas bool
48+
FormatPiecesAsList bool
49+
}
50+
51+
func NewErrorPiecesFromString(str string) (*ErrorPiece, bool) {
52+
rootPiece := &ErrorPiece{
53+
ContainerType: rootOpen,
54+
}
55+
stack := []*ErrorPiece{rootPiece}
56+
57+
for i, f := range str {
58+
if len(stack) == 0 {
59+
return rootPiece, false
60+
}
61+
62+
charStr := string(f)
63+
64+
if !stack[len(stack)-1].IsLeafContainer() {
65+
switch charStr {
66+
case braketOpen, curlyOpen, parenOpen:
67+
piece := &ErrorPiece{ContainerType: charStr}
68+
stack[len(stack)-1].AddPiece(piece)
69+
stack = append(stack, piece)
70+
continue
71+
72+
case braketClose, curlyClose, parenClose:
73+
stack = stack[:len(stack)-1]
74+
continue
75+
}
76+
}
77+
78+
switch {
79+
case checkForward(str, i, rawOpen):
80+
piece := &ErrorPiece{ContainerType: rawOpen}
81+
piece.AddStr(charStr)
82+
stack[len(stack)-1].AddPiece(piece)
83+
stack = append(stack, piece)
84+
85+
case checkBackward(str, i, rawClose):
86+
stack[len(stack)-1].AddStr(charStr)
87+
stack = stack[:len(stack)-1]
88+
89+
default:
90+
stack[len(stack)-1].AddStr(charStr)
91+
}
92+
}
93+
94+
return rootPiece, len(stack) == 1
95+
}
96+
97+
func checkForward(str string, i int, sep string) bool {
98+
endIdx := i + len(sep)
99+
if endIdx < len(str) {
100+
return str[i:endIdx] == sep
101+
}
102+
return false
103+
}
104+
105+
func checkBackward(str string, i int, sep string) bool {
106+
startIdx := i - len(sep) + 1
107+
if startIdx >= 0 && i < len(str) {
108+
return str[startIdx:i+1] == sep
109+
}
110+
return false
111+
}
112+
113+
func (p *ErrorPiece) IsContainer() bool {
114+
return len(p.ContainerType) > 0
115+
}
116+
117+
func (p *ErrorPiece) IsLeafContainer() bool {
118+
return p.ContainerType == rawOpen
119+
}
120+
121+
func (p *ErrorPiece) AddPiece(piece *ErrorPiece) {
122+
p.Pieces = append(p.Pieces, piece)
123+
}
124+
125+
func (p *ErrorPiece) AddStr(str string) {
126+
var lastPiece *ErrorPiece
127+
if len(p.Pieces) > 0 {
128+
lastPiece = p.Pieces[len(p.Pieces)-1]
129+
}
130+
if lastPiece == nil || lastPiece.IsContainer() {
131+
lastPiece = &ErrorPiece{}
132+
p.Pieces = append(p.Pieces, lastPiece)
133+
}
134+
lastPiece.Value += str
135+
}
136+
137+
var (
138+
listItemSep = ", "
139+
likelyListItemStart = regexp.MustCompile(`^[a-z\.\[\]:\s]{3,10}`)
140+
)
141+
142+
func (p *ErrorPiece) ReorganizePieces() {
143+
for _, piece := range p.Pieces {
144+
piece.ReorganizePieces()
145+
}
146+
147+
if p.ReorganizePiecesAroundCommas {
148+
p.ReorganizePiecesAroundCommas = false
149+
150+
var newPieces []*ErrorPiece
151+
152+
for _, piece := range p.Pieces {
153+
switch {
154+
case len(piece.Value) > 0:
155+
for i, val := range strings.Split(piece.Value, listItemSep) {
156+
switch {
157+
case len(newPieces) > 0 && i == 0:
158+
newPieces[len(newPieces)-1].AddStr(val)
159+
160+
case len(newPieces) > 0 && !likelyListItemStart.MatchString(val):
161+
newPieces[len(newPieces)-1].AddStr(listItemSep + val)
162+
163+
default:
164+
newPieces = append(newPieces, &ErrorPiece{
165+
ContainerType: customOpen,
166+
Pieces: []*ErrorPiece{{Value: val}},
167+
})
168+
}
169+
}
170+
171+
case p.IsContainer():
172+
if len(newPieces) > 0 {
173+
newPieces[len(newPieces)-1].AddPiece(piece)
174+
} else {
175+
newPieces = append(newPieces, &ErrorPiece{
176+
ContainerType: customOpen,
177+
Pieces: []*ErrorPiece{piece},
178+
})
179+
}
180+
}
181+
}
182+
183+
if len(newPieces) > 1 {
184+
p.Pieces = newPieces
185+
p.FormatPiecesAsList = true
186+
}
187+
}
188+
}
189+
190+
func (p *ErrorPiece) AsString() string {
191+
if len(p.Value) > 0 {
192+
return p.Value
193+
}
194+
195+
var result string
196+
197+
if p.FormatPiecesAsList {
198+
if len(p.Pieces) > 0 {
199+
result += "\n\n"
200+
}
201+
for _, piece := range p.Pieces {
202+
result += " - " + piece.AsString() + "\n\n"
203+
}
204+
} else {
205+
result += sepOpeners[p.ContainerType]
206+
for _, piece := range p.Pieces {
207+
result += piece.AsString()
208+
}
209+
result += sepClosers[p.ContainerType]
210+
}
211+
212+
return result
213+
}
214+
215+
func (p *ErrorPiece) AsIndentedString(indent string) string {
216+
if len(p.Value) > 0 {
217+
return indent + "+ " + p.Value + "\n"
218+
}
219+
var result string
220+
if p.IsContainer() {
221+
result += indent + "+ ...\n"
222+
}
223+
for _, piece := range p.Pieces {
224+
result += piece.AsIndentedString(indent + " ")
225+
}
226+
return result
227+
}

errors/multi_line_error.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package errors
2+
3+
import (
4+
"regexp"
5+
"strings"
6+
)
7+
8+
var (
9+
errColonSep = regexp.MustCompile("(: )([A-Z])")
10+
)
11+
12+
type MultiLineError struct {
13+
err error
14+
}
15+
16+
func NewMultiLineError(err error) MultiLineError {
17+
return MultiLineError{err}
18+
}
19+
20+
func (e MultiLineError) Error() (result string) {
21+
// Be conservative in not swallowing underlying error if any
22+
// error processing fails (shouldn't happen, but let's be certain)
23+
defer func() {
24+
if rec := recover(); rec != nil {
25+
result = e.err.Error()
26+
}
27+
}()
28+
29+
lines := strings.Split(e.err.Error(), "\n")
30+
31+
firstLineRootPiece, complete := NewErrorPiecesFromString(lines[0])
32+
if !complete {
33+
return e.err.Error() // cannot deconstruct error message, return original
34+
}
35+
36+
for _, piece := range firstLineRootPiece.Pieces {
37+
// Do not split withing container pieces
38+
if len(piece.Value) > 0 {
39+
piece.Value = errColonSep.ReplaceAllString(piece.Value, ":\n$2")
40+
}
41+
}
42+
43+
var firstLines []string
44+
for i, chunk := range strings.Split(firstLineRootPiece.AsString(), "\n") {
45+
firstLines = append(firstLines, strings.Repeat(" ", i)+chunk)
46+
}
47+
48+
lines[0] = strings.Join(firstLines, "\n")
49+
50+
return strings.Join(lines, "\n")
51+
}

errors/mutli_line_error_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package errors_test
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"testing"
7+
8+
"github.com/cppforlife/go-cli-ui/errors"
9+
)
10+
11+
func TestMultiLineError(t *testing.T) {
12+
tests := []multiLineErrorTest{
13+
{
14+
Description: "single line error",
15+
Actual: `Applying create deployment/frontend (apps/v1) namespace: default: Creating resource deployment/frontend (apps/v1) namespace: default: Deployment.apps "frontend" is invalid: spec.template.metadata.labels: Invalid value: map[string]string{"app":"guestbook", "kapp.k14s.io/app":"1588343775866234000", "kapp.k14s.io/association":"v1.95c1511bde234f3b1296c5e2be3c6864", "tier":"frontend"}: selector does not match template labels (reason: Invalid)`,
16+
Expected: `
17+
Applying create deployment/frontend (apps/v1) namespace: default:
18+
Creating resource deployment/frontend (apps/v1) namespace: default:
19+
Deployment.apps "frontend" is invalid: spec.template.metadata.labels:
20+
Invalid value: map[string]string{"app":"guestbook", "kapp.k14s.io/app":"1588343775866234000", "kapp.k14s.io/association":"v1.95c1511bde234f3b1296c5e2be3c6864", "tier":"frontend"}: selector does not match template labels (reason: Invalid)
21+
`,
22+
},
23+
{
24+
Description: "multi line error",
25+
Actual: `
26+
Applying create deployment/frontend (apps/v1) namespace: default: Creating resource deployment/frontend (apps/v1) namespace: default: Job.batch "successful-job" is invalid:
27+
28+
- spec.selector: Invalid value: v1.LabelSelector{MatchLabels:map[string]string{"blah":"balh", "controller-uid":"374ab0c4-8a21-4a9b-b814-fe85cf99a69a"}, MatchExpressions:[]v1.LabelSelectorRequirement(nil)}: selector not auto-generated
29+
30+
- spec.template.spec.restartPolicy: Unsupported value: "Always": supported values: "OnFailure", "Never"
31+
32+
(reason: Invalid)
33+
`,
34+
Expected: `
35+
Applying create deployment/frontend (apps/v1) namespace: default:
36+
Creating resource deployment/frontend (apps/v1) namespace: default:
37+
Job.batch "successful-job" is invalid:
38+
39+
- spec.selector: Invalid value: v1.LabelSelector{MatchLabels:map[string]string{"blah":"balh", "controller-uid":"374ab0c4-8a21-4a9b-b814-fe85cf99a69a"}, MatchExpressions:[]v1.LabelSelectorRequirement(nil)}: selector not auto-generated
40+
41+
- spec.template.spec.restartPolicy: Unsupported value: "Always": supported values: "OnFailure", "Never"
42+
43+
(reason: Invalid)
44+
`,
45+
},
46+
}
47+
48+
for _, test := range tests {
49+
test.Check(t)
50+
}
51+
}
52+
53+
type multiLineErrorTest struct {
54+
Description string
55+
Actual string
56+
Expected string
57+
}
58+
59+
func (e multiLineErrorTest) Check(t *testing.T) {
60+
apiErr := errors.NewMultiLineError(fmt.Errorf("%s", strings.TrimSpace(e.Actual)))
61+
e.Expected = strings.TrimSpace(e.Expected)
62+
63+
if apiErr.Error() != e.Expected {
64+
t.Fatalf("(%s) expected error to match:\n%d chars >>>%s<<< vs \n%d chars >>>%s<<<",
65+
e.Description, len(apiErr.Error()), apiErr, len(e.Expected), e.Expected)
66+
}
67+
}

errors/semi_structured_error.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package errors
2+
3+
type SemiStructuredError struct {
4+
err error
5+
}
6+
7+
func NewSemiStructuredError(err error) SemiStructuredError {
8+
return SemiStructuredError{err}
9+
}
10+
11+
func (e SemiStructuredError) Error() (result string) {
12+
// Be conservative in not swallowing underlying error if any
13+
// error processing fails (shouldn't happen, but let's be certain)
14+
defer func() {
15+
if rec := recover(); rec != nil {
16+
result = e.err.Error()
17+
}
18+
}()
19+
20+
rootPiece, complete := NewErrorPiecesFromString(e.err.Error())
21+
if !complete {
22+
// Cannot deconstruct error message, return original
23+
return e.err.Error()
24+
}
25+
26+
for _, piece := range rootPiece.Pieces {
27+
// k8s list of errors is wrapped with [] and separated by comma
28+
// (https://github.com/kubernetes/kubernetes/blob/a5e6ac0a959b059513c1e7908fbb0713467839c4/staging/src/k8s.io/apimachinery/pkg/util/errors/errors.go#L64)
29+
if piece.ContainerType == braketOpen {
30+
piece.ReorganizePiecesAroundCommas = true
31+
}
32+
}
33+
34+
rootPiece.ReorganizePieces()
35+
36+
return rootPiece.AsString()
37+
}

0 commit comments

Comments
 (0)