Skip to content

Commit e2a9a7f

Browse files
authored
fix: Format []ByRegexOption correctly. (#118)
If there's a mix of mappings and singular elements, the mappings will (maybe) be quoted correctly but the singular elements won't be, which can result in a a different YAML flow sequence or even invalid YAML if the singular element has the right special characters. e.g. Before the changes to options.go, the test was generating a different flow sequence with 3 elements instead of 2: `by_regex=[foo, bar, \"\\\\b(\\\\d{2})/(\\\\d{2})/(\\\\d{4})\\\\b\": \"${3}-${1}-${2}\"]"` We now generate strings that include mapping braces (`{` and `}`, e.g. `by_regex=['foo, bar', {'\b(\d{2})/(\d{2})/(\d{4})\b': '${3}-${1}-${2}'}]`). That's less than ideal, but it's still better than generating invalid YAML. I don't see any options in the YAML library to turn those braces off :/ https://pkg.go.dev/gopkg.in/yaml.v3
1 parent b8641a2 commit e2a9a7f

2 files changed

Lines changed: 42 additions & 13 deletions

File tree

keepsorted/options.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ type ByRegexOption struct {
4040
Template *string
4141
}
4242

43+
func (opt ByRegexOption) MarshalYAML() (any, error) {
44+
if opt.Template == nil {
45+
return opt.Pattern.String(), nil
46+
}
47+
48+
return map[string]string{opt.Pattern.String(): *opt.Template}, nil
49+
}
50+
4351
// SortOrder defines whether we sort in ascending or descending order.
4452
type SortOrder string
4553

@@ -248,19 +256,18 @@ func formatValue(val reflect.Value) (string, error) {
248256
return formatIntList(val.Interface().([]int)), nil
249257
case reflect.TypeFor[[]ByRegexOption]():
250258
opts := val.Interface().([]ByRegexOption)
251-
vals := make([]string, 0, len(opts))
259+
vals := make([]string, len(opts))
252260
seenTemplate := false
253-
for _, opt := range opts {
261+
for i, opt := range opts {
254262
if opt.Template != nil {
255263
seenTemplate = true
256-
vals = append(vals, fmt.Sprintf(`%q: %q`, opt.Pattern.String(), *opt.Template))
257-
continue
264+
break
258265
}
259-
vals = append(vals, opt.Pattern.String())
266+
vals[i] = opt.Pattern.String()
260267
}
261268
if seenTemplate {
262269
// always presented as a yaml sequence to preserve any `k:v` items
263-
return fmt.Sprintf("[%s]", strings.Join(vals, ", ")), nil
270+
return formatYAMLList(opts)
264271
}
265272
return formatList(vals)
266273
case reflect.TypeFor[[]*regexp.Regexp]():
@@ -292,6 +299,10 @@ func formatList(vals []string) (string, error) {
292299
return strings.Join(vals, ","), nil
293300
}
294301

302+
return formatYAMLList(vals)
303+
}
304+
305+
func formatYAMLList[T any](vals []T) (string, error) {
295306
node := new(yaml.Node)
296307
if err := node.Encode(vals); err != nil {
297308
return "", fmt.Errorf("while converting list to YAML: %w", err)

keepsorted/options_test.go

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,26 @@ func TestBlockOptions(t *testing.T) {
231231
AllowYAMLLists: true,
232232
ByRegex: []ByRegexOption{
233233
{Pattern: regexp.MustCompile(`.*`)},
234-
{Pattern: regexp.MustCompile(`\b(\d{2})/(\d{2})/(\d{4})\b`),
235-
Template: &[]string{"${3}-${1}-${2}"}[0]},
234+
{
235+
Pattern: regexp.MustCompile(`\b(\d{2})/(\d{2})/(\d{4})\b`),
236+
Template: &[]string{"${3}-${1}-${2}"}[0],
237+
},
238+
},
239+
},
240+
},
241+
{
242+
name: "RegexWithTemplateAndSingletonWithSpecialChars",
243+
in: `by_regex=['foo, bar', '\b(\d{2})/(\d{2})/(\d{4})\b': '${3}-${1}-${2}']`,
244+
defaultOptions: blockOptions{AllowYAMLLists: true},
245+
246+
want: blockOptions{
247+
AllowYAMLLists: true,
248+
ByRegex: []ByRegexOption{
249+
{Pattern: regexp.MustCompile(`foo, bar`)},
250+
{
251+
Pattern: regexp.MustCompile(`\b(\d{2})/(\d{2})/(\d{4})\b`),
252+
Template: &[]string{"${3}-${1}-${2}"}[0],
253+
},
236254
},
237255
},
238256
},
@@ -277,24 +295,24 @@ func TestBlockOptions(t *testing.T) {
277295
got, warns := parseBlockOptions(tc.commentMarker, tc.in, tc.defaultOptions)
278296
if err := errors.Join(warns...); err != nil {
279297
if tc.wantErr == "" {
280-
t.Errorf("parseBlockOptions(%q, %q) = %v", tc.commentMarker, tc.in, err)
298+
t.Errorf("parseBlockOptions(%#v, %#v) = %v", tc.commentMarker, tc.in, err)
281299
} else if !strings.Contains(err.Error(), tc.wantErr) {
282-
t.Errorf("parseBlockOptions(%q, %q) = %v, expected to contain %q", tc.commentMarker, tc.in, err, tc.wantErr)
300+
t.Errorf("parseBlockOptions(%#v, %#v) = %v, expected to contain %q", tc.commentMarker, tc.in, err, tc.wantErr)
283301
}
284302
}
285303
if diff := cmp.Diff(tc.want, got, cmp.AllowUnexported(blockOptions{}), cmpRegexp); diff != "" {
286-
t.Errorf("parseBlockOptions(%q, %q) mismatch (-want +got):\n%s", tc.commentMarker, tc.in, diff)
304+
t.Errorf("parseBlockOptions(%#v, %#v) mismatch (-want +got):\n%s", tc.commentMarker, tc.in, diff)
287305
}
288306

289307
if tc.wantErr == "" {
290308
t.Run("StringRoundtrip", func(t *testing.T) {
291309
s := got.String()
292310
got2, warns := parseBlockOptions(tc.commentMarker, s, tc.defaultOptions)
293311
if err := errors.Join(warns...); err != nil {
294-
t.Errorf("parseBlockOptions(%q, %q) = %v", tc.commentMarker, s, err)
312+
t.Errorf("parseBlockOptions(%#v, %#v) = %v", tc.commentMarker, s, err)
295313
}
296314
if diff := cmp.Diff(got, got2, cmp.AllowUnexported(blockOptions{}), cmpRegexp); diff != "" {
297-
t.Errorf("parseBlockOptions(%q, %q) mismatch (-want +got):\n%s", tc.commentMarker, s, diff)
315+
t.Errorf("parseBlockOptions(%#v, %#v) mismatch (-want +got):\n%s", tc.commentMarker, s, diff)
298316
}
299317
})
300318
}

0 commit comments

Comments
 (0)