From 8de587e00bf54ea64c67ed41deb2b6a7d7ce7104 Mon Sep 17 00:00:00 2001 From: goyzhang <24909320+goyzhang@users.noreply.github.com> Date: Sat, 30 Sep 2023 18:35:16 +0800 Subject: [PATCH] feat: add option for missing field Add DisallowMissingField option to create error when a field of a struct is missing. Remark: This solves the issue that the validator can't handle: when a int type is absent in the yaml, the validator has no way to find out as it checks against go structs. --- decode.go | 22 ++++++++++++++++++++++ decode_test.go | 37 +++++++++++++++++++++++++++++++++++++ option.go | 9 +++++++++ 3 files changed, 68 insertions(+) diff --git a/decode.go b/decode.go index d3dbabcb..6c8dbc1b 100644 --- a/decode.go +++ b/decode.go @@ -14,6 +14,7 @@ import ( "reflect" "sort" "strconv" + "strings" "time" "github.com/goccy/go-yaml/ast" @@ -39,6 +40,7 @@ type Decoder struct { validator StructValidator disallowUnknownField bool disallowDuplicateKey bool + disallowMissingField bool useOrderedMap bool useJSONUnmarshaler bool parsedFile *ast.File @@ -538,6 +540,18 @@ func errUnknownField(msg string, tk *token.Token) *unknownFieldError { return &unknownFieldError{err: errors.ErrSyntax(msg, tk)} } +type missingRequiredfieldError struct { + err error +} + +func (e *missingRequiredfieldError) Error() string { + return e.err.Error() +} + +func errMissingRequiredField(msg string, tk *token.Token) *missingRequiredfieldError { + return &missingRequiredfieldError{err: errors.ErrSyntax(msg, tk)} +} + func errUnexpectedNodeType(actual, expected ast.NodeType, tk *token.Token) error { return errors.ErrSyntax(fmt.Sprintf("%s was used where %s is expected", actual.YAMLName(), expected.YAMLName()), tk) } @@ -1177,6 +1191,7 @@ func (d *Decoder) decodeStruct(ctx context.Context, dst reflect.Value, src ast.N } } + var missingFields []string aliasName := d.getMergeAliasName(src) var foundErr error @@ -1243,6 +1258,9 @@ func (d *Decoder) decodeStruct(ctx context.Context, dst reflect.Value, src ast.N } v, exists := keyToNodeMap[structField.RenderName] if !exists { + if d.disallowMissingField { + missingFields = append(missingFields, structField.RenderName) + } continue } delete(unknownFields, structField.RenderName) @@ -1273,6 +1291,10 @@ func (d *Decoder) decodeStruct(ctx context.Context, dst reflect.Value, src ast.N return errors.Wrapf(foundErr, "failed to decode value") } + if len(missingFields) != 0 && d.disallowMissingField && src.GetToken() != nil { + return errMissingRequiredField(fmt.Sprintf(`missing required field: "%s" `, strings.Join(missingFields, ",")), src.GetToken()) + } + // Ignore unknown fields when parsing an inline struct (recognized by a nil token). // Unknown fields are expected (they could be fields from the parent struct). if len(unknownFields) != 0 && d.disallowUnknownField && src.GetToken() != nil { diff --git a/decode_test.go b/decode_test.go index 2373804d..842312bf 100644 --- a/decode_test.go +++ b/decode_test.go @@ -2890,3 +2890,40 @@ func TestSameNameInineStruct(t *testing.T) { t.Fatalf("failed to decode") } } + +func Test_DecoderMissingFieldOption(t *testing.T) { + yml := ` +b: 2 +` + expected := ` +[2:2] missing required field: "a,v" +> 2 | b: 2 + ^ +` + t.Run("map", func(t *testing.T) { + var v map[string]string + err := yaml.NewDecoder(strings.NewReader(yml), yaml.DisallowMissingField()).Decode(&v) + if err != nil { + t.Fatal("decoding should success") + } + if len(v) != 1 && v["b"] != "2" { + t.Fatal("failed to decode, DisallowMissingField should have no impact on map") + } + }) + t.Run("struct", func(t *testing.T) { + var v struct { + A int + B int + V string + } + err := yaml.NewDecoder(strings.NewReader(yml), yaml.DisallowMissingField()).Decode(&v) + if err == nil { + t.Fatal("decoding should fail") + } + actual := "\n" + err.Error() + if expected != actual { + fmt.Println(err) + t.Fatalf("expected:[%s] actual:[%s]", expected, actual) + } + }) +} diff --git a/option.go b/option.go index eab5d43a..ab4a661b 100644 --- a/option.go +++ b/option.go @@ -77,6 +77,15 @@ func DisallowDuplicateKey() DecodeOption { } } +// DisallowMissingField causes an error when mapping keys that are missing if the destination is a struct +// This does no effect on dynamic types like map. +func DisallowMissingField() DecodeOption { + return func(d *Decoder) error { + d.disallowMissingField = true + return nil + } +} + // UseOrderedMap can be interpreted as a map, // and uses MapSlice ( ordered map ) aggressively if there is no type specification func UseOrderedMap() DecodeOption {