Skip to content

Commit

Permalink
feat: add option for missing field
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
goyzhang committed Oct 1, 2023
1 parent 0640a15 commit 8de587e
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 0 deletions.
22 changes: 22 additions & 0 deletions decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"reflect"
"sort"
"strconv"
"strings"
"time"

"github.com/goccy/go-yaml/ast"
Expand All @@ -39,6 +40,7 @@ type Decoder struct {
validator StructValidator
disallowUnknownField bool
disallowDuplicateKey bool
disallowMissingField bool
useOrderedMap bool
useJSONUnmarshaler bool
parsedFile *ast.File
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
37 changes: 37 additions & 0 deletions decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
9 changes: 9 additions & 0 deletions option.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 8de587e

Please sign in to comment.