diff --git a/sheriff.go b/sheriff.go index 85ca75b..c2d19a5 100644 --- a/sheriff.go +++ b/sheriff.go @@ -10,8 +10,18 @@ import ( "github.com/hashicorp/go-version" ) +// A FieldFilter is a function that decides whether a field should be marshalled or not. +// If it returns true, the field will be marshalled, otherwise it will be skipped. +type FieldFilter func(field reflect.StructField) (bool, error) + // Options determine which struct fields are being added to the output map. type Options struct { + // The FieldFilter makes the decision whether a field should be marshalled or not. + // It receives the reflect.StructField of the field and should return true if the field should be included. + // If this is not set then the default FieldFilter will be used, which uses the Groups and ApiVersion fields. + // Setting this value will result in the other options being ignored. + FieldFilter FieldFilter + // Groups determine which fields are getting marshalled based on the groups tag. // A field with multiple groups (comma-separated) will result in marshalling of that // field if one of their groups is specified. @@ -67,7 +77,9 @@ func Marshal(options *Options, data interface{}) (interface{}, error) { options.nestedGroupsMap = make(map[string][]string) } - checkGroups := len(options.Groups) > 0 + if options.FieldFilter == nil { + options.FieldFilter = createDefaultFieldFilter(options) + } if t.Kind() == reflect.Ptr { // follow pointer @@ -138,53 +150,16 @@ func Marshal(options *Options, data interface{}) (interface{}, error) { } if !isEmbeddedField { - if checkGroups { - var groups []string - if field.Tag.Get("groups") != "" { - groups = strings.Split(field.Tag.Get("groups"), ",") - } - - if len(groups) == 0 && options.nestedGroupsMap[field.Name] != nil { - groups = append(groups, options.nestedGroupsMap[field.Name]...) - } - - // Marshall the field if - // - it has at least one of the requested groups - // or - // - it has no group and 'IncludeEmptyTag' is set to true - shouldShow := listContains(groups, options.Groups) || (len(groups) == 0 && options.IncludeEmptyTag) - - // Prevent marshalling of the field if - // - it should not be shown (above) - // or - // - it has no groups and 'IncludeEmptyTag' is set to false - shouldHide := !shouldShow || (len(groups) == 0 && !options.IncludeEmptyTag) - - if shouldHide { - // skip this field - continue - } + include, err := options.FieldFilter(field) + if err != nil { + return nil, err } - if since := field.Tag.Get("since"); since != "" { - sinceVersion, err := version.NewVersion(since) - if err != nil { - return nil, err - } - if options.ApiVersion.LessThan(sinceVersion) { - continue - } + if !include { + // skip this field + continue } - if until := field.Tag.Get("until"); until != "" { - untilVersion, err := version.NewVersion(until) - if err != nil { - return nil, err - } - if options.ApiVersion.GreaterThan(untilVersion) { - continue - } - } } v, err := marshalValue(options, val) @@ -210,6 +185,66 @@ func Marshal(options *Options, data interface{}) (interface{}, error) { return dest, nil } +// createDefaultFieldFilter creates a default FieldFilter function which uses the options.Groups and options.ApiVersion +// fields in order to determine whether a field should be marshalled or not. +func createDefaultFieldFilter(options *Options) FieldFilter { + checkGroups := len(options.Groups) > 0 + + return func(field reflect.StructField) (bool, error) { + if checkGroups { + var groups []string + if field.Tag.Get("groups") != "" { + groups = strings.Split(field.Tag.Get("groups"), ",") + } + + if len(groups) == 0 && options.nestedGroupsMap[field.Name] != nil { + groups = append(groups, options.nestedGroupsMap[field.Name]...) + } + + // Marshall the field if + // - it has at least one of the requested groups + // or + // - it has no group and 'IncludeEmptyTag' is set to true + shouldShow := listContains(groups, options.Groups) || (len(groups) == 0 && options.IncludeEmptyTag) + + // Prevent marshalling of the field if + // - it should not be shown (above) + // or + // - it has no groups and 'IncludeEmptyTag' is set to false + shouldHide := !shouldShow || (len(groups) == 0 && !options.IncludeEmptyTag) + + if shouldHide { + // skip this field + return false, nil + } + } + + if since := field.Tag.Get("since"); since != "" { + sinceVersion, err := version.NewVersion(since) + if err != nil { + return true, err + } + if options.ApiVersion.LessThan(sinceVersion) { + // skip this field + return false, nil + } + } + + if until := field.Tag.Get("until"); until != "" { + untilVersion, err := version.NewVersion(until) + if err != nil { + return true, err + } + if options.ApiVersion.GreaterThan(untilVersion) { + // skip this field + return false, nil + } + } + + return true, nil + } +} + // marshalValue is being used for getting the actual value of a field. // // There is support for types implementing the Marshaller interface, arbitrary structs, slices, maps and base types. diff --git a/sheriff_test.go b/sheriff_test.go index fe23197..bddbecf 100644 --- a/sheriff_test.go +++ b/sheriff_test.go @@ -3,6 +3,7 @@ package sheriff import ( "encoding/json" "net" + "reflect" "testing" "time" @@ -853,3 +854,25 @@ func TestMarshal_User(t *testing.T) { assert.NoError(t, err) assert.Equal(t, `{"test":"12","testb":"true","testf":"12","tests":"\"test\""}`, string(d)) } + +func TestMarshal_CustomFieldFilter(t *testing.T) { + type testStruct struct { + TestValue string `json:"test"` + SecretValue string `json:"secret" hidden:"true"` + } + v := testStruct{ + TestValue: "teststring", + SecretValue: "asecretvalue", + } + + o := &Options{ + FieldFilter: func(field reflect.StructField) (bool, error) { + return field.Tag.Get("hidden") == "", nil + }, + } + m, err := Marshal(o, v) + assert.NoError(t, err) + + // ensure the "secret" value is not present in the marshalled map + assert.Equal(t, map[string]interface{}{"test": "teststring"}, m) +}