Skip to content

Commit 05e6525

Browse files
authored
Fix mismatching marker handling, add better error handling (#51)
* Add errorsplus for composite errors * Adjust error message * Change error message to human friendly message * Remove duplicated message * Add indentation keep support, and test case for align * Add proper error handling when markers don't match * Adjust error message * Add clarification comment * Add better error handling test case * Add test case for indent keep mode * Add test case for broken marker * Correct error message
1 parent f8ee32c commit 05e6525

18 files changed

+357
-132
lines changed

internal/cli/generate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func executeGenerate(cmd *cobra.Command, args []string) error {
4444
arg := args[0]
4545
out := generateTargetFile
4646
if err := generate(arg, out); err != nil {
47-
return fmt.Errorf("handling generate, %v", err)
47+
return fmt.Errorf("failed to generate for '%s', %v", arg, err)
4848
}
4949

5050
return nil

internal/cli/purge.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/spf13/cobra"
99

10+
"github.com/upsidr/importer/internal/errorsplus"
1011
"github.com/upsidr/importer/internal/file"
1112
"github.com/upsidr/importer/internal/parse"
1213
)
@@ -38,11 +39,15 @@ func executePurge(cmd *cobra.Command, args []string) error {
3839
// Suppress usage message after this point
3940
cmd.SilenceUsage = true
4041

42+
errs := errorsplus.Errors{}
4143
for _, file := range args {
4244
if err := purge(file); err != nil {
43-
fmt.Printf("Warning: failed to purge for '%s', %v", file, err)
45+
errs = append(errs, fmt.Errorf("failed to update '%s', %v", file, err))
4446
}
4547
}
48+
if len(errs) != 0 {
49+
return errs
50+
}
4651

4752
return nil
4853
}

internal/cli/update.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/spf13/cobra"
99

10+
"github.com/upsidr/importer/internal/errorsplus"
1011
"github.com/upsidr/importer/internal/file"
1112
"github.com/upsidr/importer/internal/parse"
1213
)
@@ -39,11 +40,15 @@ func executeUpdate(cmd *cobra.Command, args []string) error {
3940
// Suppress usage message after this point
4041
cmd.SilenceUsage = true
4142

43+
errs := errorsplus.Errors{}
4244
for _, file := range args {
4345
if err := update(file); err != nil {
44-
fmt.Printf("Warning: failed to generate for '%s', %v", file, err)
46+
errs = append(errs, fmt.Errorf("failed to update '%s', %v", file, err))
4547
}
4648
}
49+
if len(errs) != 0 {
50+
return errs
51+
}
4752

4853
return nil
4954
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package errorsplus
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
)
7+
8+
// Errors represents a slice of errors.
9+
type Errors []error
10+
11+
func (es Errors) Error() string {
12+
switch len(es) {
13+
case 0:
14+
return ""
15+
case 1:
16+
return fmt.Sprintf("%v", es[0])
17+
}
18+
19+
rt := "more than one error occurred:"
20+
for _, e := range es {
21+
if e != nil {
22+
rt = fmt.Sprintf("%s\n\t%v", rt, e)
23+
}
24+
}
25+
return rt
26+
}
27+
28+
func (es Errors) Is(target error) bool {
29+
if len(es) == 0 {
30+
return false
31+
}
32+
33+
for _, e := range es {
34+
if errors.Is(e, target) {
35+
return true
36+
}
37+
}
38+
return false
39+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package errorsplus_test
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"github.com/google/go-cmp/cmp"
8+
9+
"github.com/upsidr/importer/internal/errorsplus"
10+
)
11+
12+
var (
13+
errFirst = errors.New("first error")
14+
errSecond = errors.New("second error")
15+
errThird = errors.New("third error")
16+
errOther = errors.New("some other error")
17+
)
18+
19+
func TestCompositeErrors(t *testing.T) {
20+
cases := map[string]struct {
21+
errs errorsplus.Errors
22+
wantErr error
23+
wantErrString string
24+
}{
25+
"zero error": {
26+
errs: errorsplus.Errors{
27+
// empty
28+
},
29+
wantErr: nil, // cannot be checked against nil, this is skipped explicitly below
30+
wantErrString: "", // empty
31+
},
32+
"single error": {
33+
errs: errorsplus.Errors{
34+
errFirst,
35+
},
36+
wantErr: errFirst,
37+
wantErrString: "first error",
38+
},
39+
"2 errors": {
40+
errs: errorsplus.Errors{
41+
errFirst,
42+
errSecond,
43+
},
44+
wantErr: errFirst,
45+
wantErrString: `more than one error occurred:
46+
first error
47+
second error`,
48+
},
49+
"3 errors": {
50+
errs: errorsplus.Errors{
51+
errFirst,
52+
errSecond,
53+
errThird,
54+
},
55+
wantErr: errFirst,
56+
wantErrString: `more than one error occurred:
57+
first error
58+
second error
59+
third error`,
60+
},
61+
"duplicated errors": {
62+
errs: errorsplus.Errors{
63+
errFirst,
64+
errFirst,
65+
errFirst,
66+
},
67+
wantErr: errFirst,
68+
wantErrString: `more than one error occurred:
69+
first error
70+
first error
71+
first error`,
72+
},
73+
}
74+
75+
for name, tc := range cases {
76+
t.Run(name, func(t *testing.T) {
77+
if tc.wantErr != nil && !errors.Is(tc.errs, tc.wantErr) {
78+
t.Errorf("error mismatch\n\tcomposite error: %v\n\twanted error: %v", tc.errs, tc.wantErr)
79+
}
80+
81+
errString := tc.errs.Error()
82+
if diff := cmp.Diff(tc.wantErrString, errString); diff != "" {
83+
t.Errorf("result didn't match (-want / +got)\n%s", diff)
84+
}
85+
})
86+
}
87+
}
88+
89+
func TestCompositeErrorsMismatch(t *testing.T) {
90+
cases := map[string]struct {
91+
errs errorsplus.Errors
92+
mismatchedErr error
93+
}{
94+
"zero error": {
95+
errs: errorsplus.Errors{
96+
// empty
97+
},
98+
mismatchedErr: errOther,
99+
},
100+
"single error": {
101+
errs: errorsplus.Errors{
102+
errFirst,
103+
},
104+
mismatchedErr: errOther,
105+
},
106+
"2 errors": {
107+
errs: errorsplus.Errors{
108+
errFirst,
109+
errSecond,
110+
},
111+
mismatchedErr: errOther,
112+
},
113+
}
114+
115+
for name, tc := range cases {
116+
t.Run(name, func(t *testing.T) {
117+
if errors.Is(tc.errs, tc.mismatchedErr) {
118+
t.Errorf("unexpected error match, where it should not be matched with errors.Is\n\tcomposite error: %v\n\ttarget error: %v", tc.errs, tc.mismatchedErr)
119+
}
120+
})
121+
}
122+
}

internal/marker/errors.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ var (
77
ErrNoInput = errors.New("no file content found")
88
ErrInvalidPath = errors.New("invalid path provided")
99
ErrInvalidSyntax = errors.New("invalid syntax given")
10-
ErrNoMatchingMarker = errors.New("no matching marker found, marker must be a begin/end pair")
10+
ErrNoMatchingMarker = errors.New("no matching marker found")
1111
ErrMissingOption = errors.New("missing option")
1212
ErrMissingName = errors.New("unnamed marker cannot be used")
1313
ErrNoFileInput = errors.New("no target file input provided")

internal/marker/marker.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func NewMarker(raw *RawMarker) (*Marker, error) {
8080
func processFileOption(marker *Marker, match *RawMarker) error {
8181
matches, err := regexpplus.MapWithNamedSubgroups(match.Options, OptionFilePathIndicator)
8282
if err != nil {
83-
return fmt.Errorf("%w, import target option is missing", ErrInvalidSyntax)
83+
return fmt.Errorf("%w for '%s', import target option is missing", ErrInvalidSyntax, match.Name)
8484
}
8585

8686
if targetPath, found := matches["importer_target_path"]; found {
@@ -116,6 +116,12 @@ func processIndentOption(marker *Marker, match *RawMarker) error {
116116
MarkerIndentation: markerIndentation,
117117
}
118118
return nil // Align option does not care length information
119+
case "keep":
120+
// Keep the provided indentation, and do nothing
121+
marker.Indentation = &Indentation{
122+
Mode: KeepIndentation,
123+
}
124+
return nil
119125
default:
120126
return errors.New("unsupported indentation mode") // This shouldn't happen with the underlying regex
121127
}
@@ -128,7 +134,7 @@ func processIndentOption(marker *Marker, match *RawMarker) error {
128134

129135
length, err := strconv.Atoi(lengthInput)
130136
if err != nil {
131-
return fmt.Errorf("%w, %v", ErrInvalidSyntax, err)
137+
return fmt.Errorf("%w for '%s', %v", ErrInvalidSyntax, marker.Name, err)
132138
}
133139
marker.Indentation.Length = length
134140
}
@@ -217,15 +223,15 @@ func processTargetDetail(marker *Marker, input string) error {
217223
case strings.Contains(input, "~"):
218224
lb, ub, err := getLineRangeWithTilde(input)
219225
if err != nil {
220-
return fmt.Errorf("%w, %v", ErrInvalidSyntax, err)
226+
return fmt.Errorf("%w for '%s', %v", ErrInvalidSyntax, marker.Name, err)
221227
}
222228
marker.TargetLineFrom = lb
223229
marker.TargetLineTo = ub
224230

225231
default:
226232
i, err := strconv.Atoi(input)
227233
if err != nil {
228-
return fmt.Errorf("%w, %v", ErrInvalidSyntax, err)
234+
return fmt.Errorf("%w for '%s', %v", ErrInvalidSyntax, marker.Name, err)
229235
}
230236
marker.TargetLines = append(marker.TargetLines, i)
231237
}
@@ -234,8 +240,9 @@ func processTargetDetail(marker *Marker, input string) error {
234240
}
235241

236242
var (
237-
errLowerBound = errors.New("invalid lower bound in line range")
238-
errUpperBound = errors.New("invalid upper bound in line range")
243+
errLowerBound = errors.New("invalid lower bound in line range")
244+
errUpperBound = errors.New("invalid upper bound in line range")
245+
errMultipleTildes = errors.New("tilde cannot be used more than once")
239246
)
240247

241248
func getLineRangeWithTilde(input string) (int, int, error) {
@@ -244,7 +251,7 @@ func getLineRangeWithTilde(input string) (int, int, error) {
244251

245252
ls := strings.Split(input, "~")
246253
if len(ls) > 2 {
247-
return lowerBound, upperBound, fmt.Errorf("%w, tilde cannot be used more than once", ErrInvalidSyntax)
254+
return lowerBound, upperBound, fmt.Errorf("%w", errMultipleTildes)
248255
}
249256

250257
lb := ls[0]

internal/marker/marker_regex.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ var (
1515
OptionFilePathIndicator = `from: (?P<importer_target_path>\S+)\s*\#(?P<importer_target_detail>[0-9a-zA-Z,-_\~]+)\s?`
1616

1717
// OptionIndentMode is the pattern used for specifying indentation mode.
18-
OptionIndentMode = `indent: (?P<importer_indent_mode>absolute|extra|align)\s?(?P<importer_indent_length>\d*)`
18+
OptionIndentMode = `indent: (?P<importer_indent_mode>absolute|extra|align|keep)\s?(?P<importer_indent_length>\d*)`
1919
)
2020

2121
var (

internal/marker/marker_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,45 @@ func TestNewMarker(t *testing.T) {
120120
},
121121
},
122122
},
123+
"Exporter with indent align": {
124+
input: &marker.RawMarker{
125+
Name: "simple-marker",
126+
IsBeginFound: true,
127+
IsEndFound: true,
128+
LineToInsertAt: 3,
129+
Options: "from: ./abc.yaml#[from-exporter-marker] indent: align",
130+
PrecedingIndentation: " ",
131+
},
132+
want: &marker.Marker{
133+
Name: "simple-marker",
134+
LineToInsertAt: 3,
135+
TargetPath: "./abc.yaml",
136+
TargetExportMarker: "from-exporter-marker",
137+
Indentation: &marker.Indentation{
138+
Mode: marker.AlignIndentation,
139+
MarkerIndentation: 4,
140+
},
141+
},
142+
},
143+
"Exporter with indent keep": {
144+
input: &marker.RawMarker{
145+
Name: "simple-marker",
146+
IsBeginFound: true,
147+
IsEndFound: true,
148+
LineToInsertAt: 3,
149+
Options: "from: ./abc.yaml#[from-exporter-marker] indent: keep",
150+
PrecedingIndentation: " ",
151+
},
152+
want: &marker.Marker{
153+
Name: "simple-marker",
154+
LineToInsertAt: 3,
155+
TargetPath: "./abc.yaml",
156+
TargetExportMarker: "from-exporter-marker",
157+
Indentation: &marker.Indentation{
158+
Mode: marker.KeepIndentation,
159+
},
160+
},
161+
},
123162
}
124163

125164
for name, tc := range cases {

internal/marker/raw_marker.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ type RawMarker struct {
1717
// Validate checks RawMarker's validity.
1818
func (r *RawMarker) Validate() error {
1919
if r.Name == "" {
20-
return fmt.Errorf("%w", ErrMissingName)
20+
return fmt.Errorf("%w", ErrMissingName) // Should not happen with the current regexp setup
2121
}
2222

2323
if !r.IsBeginFound || !r.IsEndFound {
24-
return fmt.Errorf("%w", ErrNoMatchingMarker)
24+
return fmt.Errorf("%w for '%s', marker must be a begin/end pair", ErrNoMatchingMarker, r.Name)
2525
}
2626

2727
return nil

0 commit comments

Comments
 (0)