diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dd2d512..9cb3d9b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] -- No changes yet. +## Added +- Redacted annotation provides a mechanism to redact certain struct fields from +errors messages and log objects. ## [1.31.0] - 2023-06-09 ### Changed diff --git a/gen/field.go b/gen/field.go index 93bfb40e..2ef1c669 100644 --- a/gen/field.go +++ b/gen/field.go @@ -699,25 +699,38 @@ func (f fieldGroupGenerator) String(g Generator) error { <- $fname := goName . -> <- $f := printf "%s.%s" $v $fname -> + <- $shouldRedact := shouldRedact . -> + <- $redactedContent := redactedContent -> <- if not .Required -> if <$f> != nil { - - <$fields>[<$i>] = <$fmt>.Sprintf("<$fname>: %v", *(<$f>)) - <- else -> - <$fields>[<$i>] = <$fmt>.Sprintf("<$fname>: %v", <$f>) + <- if $shouldRedact -> + <$fields>[<$i>] = "<$fname>: <$redactedContent>" + <- else > + + <$fields>[<$i>] = <$fmt>.Sprintf("<$fname>: %v", *(<$f>)) + <- else -> + <$fields>[<$i>] = <$fmt>.Sprintf("<$fname>: %v", <$f>) + <- end> <- end> <$i>++ } <- else -> - <$fields>[<$i>] = <$fmt>.Sprintf("<$fname>: %v", <$f>) + <- if $shouldRedact -> + <$fields>[<$i>] = "<$fname>: <$redactedContent>" + <- else -> + <$fields>[<$i>] = <$fmt>.Sprintf("<$fname>: %v", <$f>) + <- end> <$i>++ <- end> return <$fmt>.Sprintf("<.Name>{%v}", <$strings>.Join(<$fields>[:<$i>], ", ")) } - `, f) + `, f, + TemplateFunc("shouldRedact", shouldRedact), + TemplateFunc("redactedContent", redactedContent), + ) } func (f fieldGroupGenerator) ErrorName(g Generator) error { @@ -779,18 +792,28 @@ func (f fieldGroupGenerator) Zap(g Generator) error { if <$v> == nil { return nil } + < $redactedContent := redactedContent -> + <- $shouldRedact := shouldRedact . -> <- if not (zapOptOut .) -> <- $fval := printf "%s.%s" $v (goName .) -> <- if .Required -> - - <$enc>.Add("", ) - <- zapEncodeEnd .Type> + <- if $shouldRedact -> + <$enc>.AddString("", "<$redactedContent>") + <- else -> + <- zapEncodeBegin .Type -> + <$enc>.Add("", ) + <- zapEncodeEnd .Type -> + <- end -> <- else -> if <$fval> != nil { - - <$enc>.Add("", ) - <- zapEncodeEnd .Type> + <- if $shouldRedact -> + <$enc>.AddString("", "<$redactedContent>") + <- else -> + <- zapEncodeBegin .Type -> + <$enc>.Add("", ) + <- zapEncodeEnd .Type -> + <- end > } <- end> <- end> @@ -800,6 +823,8 @@ func (f fieldGroupGenerator) Zap(g Generator) error { `, f, TemplateFunc("zapOptOut", zapOptOut), TemplateFunc("fieldLabel", entityLabel), + TemplateFunc("shouldRedact", shouldRedact), + TemplateFunc("redactedContent", redactedContent), ) } @@ -875,3 +900,23 @@ func verifyUniqueFieldLabels(fs compile.FieldGroup) error { } return nil } + +// RedactLabel provides a mechanism to redact certain struct fields from +// the outputs of String(), Error() and MarshalLogObject() methods. +// +// struct Contact { +// 1: required string name +// 2: required string email (go.redact) +// } +const RedactLabel = "go.redact" + +func shouldRedact(spec *compile.FieldSpec) bool { + _, ok := spec.Annotations[RedactLabel] + return ok +} + +const _redactContent = "" + +func redactedContent() string { + return _redactContent +} diff --git a/gen/field_test.go b/gen/field_test.go index 561011e1..7299b413 100644 --- a/gen/field_test.go +++ b/gen/field_test.go @@ -25,10 +25,12 @@ import ( "strings" "testing" - "go.uber.org/thriftrw/compile" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/thriftrw/compile" + "go.uber.org/thriftrw/gen/internal/tests/exceptions" + ts "go.uber.org/thriftrw/gen/internal/tests/structs" + "go.uber.org/zap/zapcore" ) func TestFieldLabelConflict(t *testing.T) { @@ -137,3 +139,70 @@ func TestCompileJSONTag(t *testing.T) { }) } } + +func TestHasRedactedAnnotation(t *testing.T) { + foo := &compile.FieldSpec{ + Name: "foo", + } + redact := &compile.FieldSpec{ + Name: "redact", + Annotations: compile.Annotations{RedactLabel: ""}, + } + tests := []struct { + name string + spec *compile.FieldSpec + want bool + }{ + {name: "redact annotation", spec: redact, want: true}, + {name: "no redact annotation", spec: foo, want: false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, shouldRedact(tt.spec), "shouldRedact(%v)", tt.spec) + }) + } +} + +func TestRedactedAnnotation(t *testing.T) { + age := int32(21) + pi := ts.PersonalInfo{ + Age: toPtr(age), + Race: toPtr("martian"), + } + redactException := exceptions.DoesNotExistException{ + Key: "s", + UserName: toPtr("john doe"), + } + enc := zapcore.NewMapObjectEncoder() + require.NoError(t, pi.MarshalLogObject(enc)) + require.Len(t, enc.Fields, 2) + _, ok := enc.Fields["race"] + require.True(t, ok) + + eEncoder := zapcore.NewMapObjectEncoder() + require.NoError(t, redactException.MarshalLogObject(eEncoder)) + require.Len(t, eEncoder.Fields, 2) + _, ok = eEncoder.Fields["userName"] + require.True(t, ok) + + tests := []struct { + name string + got any + want any + }{ + {name: "struct/string", got: pi.String(), want: "PersonalInfo{Age: 21, Race: }"}, + {name: "struct/MarshalLogObject", got: enc.Fields["race"], want: _redactContent}, + {name: "exception/string", got: redactException.String(), want: "DoesNotExistException{Key: s, UserName: }"}, + {name: "exception/error", got: redactException.Error(), want: "DoesNotExistException{Key: s, UserName: }"}, + {name: "exception/MarshalLogObject", got: eEncoder.Fields["userName"], want: _redactContent}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.EqualValues(t, tt.want, tt.got) + }) + } +} + +func toPtr[T any](input T) *T { + return &input +} diff --git a/gen/internal/tests/exceptions/exceptions.go b/gen/internal/tests/exceptions/exceptions.go index 89bd45bf..67b6b208 100644 --- a/gen/internal/tests/exceptions/exceptions.go +++ b/gen/internal/tests/exceptions/exceptions.go @@ -16,8 +16,9 @@ import ( // Raised when something doesn't exist. type DoesNotExistException struct { // Key that was missing. - Key string `json:"key,required"` - Error2 *string `json:"Error,omitempty"` + Key string `json:"key,required"` + Error2 *string `json:"Error,omitempty"` + UserName *string `json:"userName,omitempty"` } // ToWire translates a DoesNotExistException struct into a Thrift-level intermediate @@ -37,7 +38,7 @@ type DoesNotExistException struct { // } func (v *DoesNotExistException) ToWire() (wire.Value, error) { var ( - fields [2]wire.Field + fields [3]wire.Field i int = 0 w wire.Value err error @@ -57,6 +58,14 @@ func (v *DoesNotExistException) ToWire() (wire.Value, error) { fields[i] = wire.Field{ID: 2, Value: w} i++ } + if v.UserName != nil { + w, err = wire.NewValueString(*(v.UserName)), error(nil) + if err != nil { + return w, err + } + fields[i] = wire.Field{ID: 3, Value: w} + i++ + } return wire.NewValueStruct(wire.Struct{Fields: fields[:i]}), nil } @@ -102,6 +111,16 @@ func (v *DoesNotExistException) FromWire(w wire.Value) error { return err } + } + case 3: + if field.Value.Type() == wire.TBinary { + var x string + x, err = field.Value.GetString(), error(nil) + v.UserName = &x + if err != nil { + return err + } + } } } @@ -144,6 +163,18 @@ func (v *DoesNotExistException) Encode(sw stream.Writer) error { } } + if v.UserName != nil { + if err := sw.WriteFieldBegin(stream.FieldHeader{ID: 3, Type: wire.TBinary}); err != nil { + return err + } + if err := sw.WriteString(*(v.UserName)); err != nil { + return err + } + if err := sw.WriteFieldEnd(); err != nil { + return err + } + } + return sw.WriteStructEnd() } @@ -181,6 +212,14 @@ func (v *DoesNotExistException) Decode(sr stream.Reader) error { return err } + case fh.ID == 3 && fh.Type == wire.TBinary: + var x string + x, err = sr.ReadString() + v.UserName = &x + if err != nil { + return err + } + default: if err := sr.Skip(fh.Type); err != nil { return err @@ -214,7 +253,7 @@ func (v *DoesNotExistException) String() string { return "" } - var fields [2]string + var fields [3]string i := 0 fields[i] = fmt.Sprintf("Key: %v", v.Key) i++ @@ -222,6 +261,10 @@ func (v *DoesNotExistException) String() string { fields[i] = fmt.Sprintf("Error2: %v", *(v.Error2)) i++ } + if v.UserName != nil { + fields[i] = "UserName: " + i++ + } return fmt.Sprintf("DoesNotExistException{%v}", strings.Join(fields[:i], ", ")) } @@ -258,6 +301,9 @@ func (v *DoesNotExistException) Equals(rhs *DoesNotExistException) bool { if !_String_EqualsPtr(v.Error2, rhs.Error2) { return false } + if !_String_EqualsPtr(v.UserName, rhs.UserName) { + return false + } return true } @@ -272,6 +318,9 @@ func (v *DoesNotExistException) MarshalLogObject(enc zapcore.ObjectEncoder) (err if v.Error2 != nil { enc.AddString("Error", *v.Error2) } + if v.UserName != nil { + enc.AddString("userName", "") + } return err } @@ -299,6 +348,21 @@ func (v *DoesNotExistException) IsSetError2() bool { return v != nil && v.Error2 != nil } +// GetUserName returns the value of UserName if it is set or its +// zero value if it is unset. +func (v *DoesNotExistException) GetUserName() (o string) { + if v != nil && v.UserName != nil { + return *v.UserName + } + + return +} + +// IsSetUserName returns true if UserName is not nil. +func (v *DoesNotExistException) IsSetUserName() bool { + return v != nil && v.UserName != nil +} + func (v *DoesNotExistException) Error() string { return v.String() } @@ -739,8 +803,8 @@ var ThriftModule = &thriftreflect.ThriftModule{ Name: "exceptions", Package: "go.uber.org/thriftrw/gen/internal/tests/exceptions", FilePath: "exceptions.thrift", - SHA1: "a31a29f9f7bca3221100e43e32b0d833ca9c774e", + SHA1: "671449b355e9a5f64483f157e93dc762fe3d1944", Raw: rawIDL, } -const rawIDL = "exception EmptyException {}\n\n/**\n * Raised when something doesn't exist.\n */\nexception DoesNotExistException {\n /** Key that was missing. */\n 1: required string key\n 2: optional string Error (go.name=\"Error2\")\n}\n\nexception Does_Not_Exist_Exception_Collision {\n /** Key that was missing. */\n 1: required string key\n 2: optional string Error (go.name=\"Error2\")\n} (go.name=\"DoesNotExistException2\")\n" +const rawIDL = "exception EmptyException {}\n\n/**\n * Raised when something doesn't exist.\n */\nexception DoesNotExistException {\n /** Key that was missing. */\n 1: required string key\n 2: optional string Error (go.name=\"Error2\")\n 3: optional string userName (go.redact)\n}\n\nexception Does_Not_Exist_Exception_Collision {\n /** Key that was missing. */\n 1: required string key\n 2: optional string Error (go.name=\"Error2\")\n} (go.name=\"DoesNotExistException2\")\n" diff --git a/gen/internal/tests/structs/structs.go b/gen/internal/tests/structs/structs.go index b1d8e938..6ca9c416 100644 --- a/gen/internal/tests/structs/structs.go +++ b/gen/internal/tests/structs/structs.go @@ -4751,7 +4751,8 @@ func (v *Omit) GetHidden() (o string) { } type PersonalInfo struct { - Age *int32 `json:"age,omitempty"` + Age *int32 `json:"age,omitempty"` + Race *string `json:"race,omitempty"` } // ToWire translates a PersonalInfo struct into a Thrift-level intermediate @@ -4771,7 +4772,7 @@ type PersonalInfo struct { // } func (v *PersonalInfo) ToWire() (wire.Value, error) { var ( - fields [1]wire.Field + fields [2]wire.Field i int = 0 w wire.Value err error @@ -4785,6 +4786,14 @@ func (v *PersonalInfo) ToWire() (wire.Value, error) { fields[i] = wire.Field{ID: 1, Value: w} i++ } + if v.Race != nil { + w, err = wire.NewValueString(*(v.Race)), error(nil) + if err != nil { + return w, err + } + fields[i] = wire.Field{ID: 2, Value: w} + i++ + } return wire.NewValueStruct(wire.Struct{Fields: fields[:i]}), nil } @@ -4820,6 +4829,16 @@ func (v *PersonalInfo) FromWire(w wire.Value) error { return err } + } + case 2: + if field.Value.Type() == wire.TBinary { + var x string + x, err = field.Value.GetString(), error(nil) + v.Race = &x + if err != nil { + return err + } + } } } @@ -4848,6 +4867,18 @@ func (v *PersonalInfo) Encode(sw stream.Writer) error { } } + if v.Race != nil { + if err := sw.WriteFieldBegin(stream.FieldHeader{ID: 2, Type: wire.TBinary}); err != nil { + return err + } + if err := sw.WriteString(*(v.Race)); err != nil { + return err + } + if err := sw.WriteFieldEnd(); err != nil { + return err + } + } + return sw.WriteStructEnd() } @@ -4877,6 +4908,14 @@ func (v *PersonalInfo) Decode(sr stream.Reader) error { return err } + case fh.ID == 2 && fh.Type == wire.TBinary: + var x string + x, err = sr.ReadString() + v.Race = &x + if err != nil { + return err + } + default: if err := sr.Skip(fh.Type); err != nil { return err @@ -4906,12 +4945,16 @@ func (v *PersonalInfo) String() string { return "" } - var fields [1]string + var fields [2]string i := 0 if v.Age != nil { fields[i] = fmt.Sprintf("Age: %v", *(v.Age)) i++ } + if v.Race != nil { + fields[i] = "Race: " + i++ + } return fmt.Sprintf("PersonalInfo{%v}", strings.Join(fields[:i], ", ")) } @@ -4929,6 +4972,9 @@ func (v *PersonalInfo) Equals(rhs *PersonalInfo) bool { if !_I32_EqualsPtr(v.Age, rhs.Age) { return false } + if !_String_EqualsPtr(v.Race, rhs.Race) { + return false + } return true } @@ -4942,6 +4988,9 @@ func (v *PersonalInfo) MarshalLogObject(enc zapcore.ObjectEncoder) (err error) { if v.Age != nil { enc.AddInt32("age", *v.Age) } + if v.Race != nil { + enc.AddString("race", "") + } return err } @@ -4960,6 +5009,21 @@ func (v *PersonalInfo) IsSetAge() bool { return v != nil && v.Age != nil } +// GetRace returns the value of Race if it is set or its +// zero value if it is unset. +func (v *PersonalInfo) GetRace() (o string) { + if v != nil && v.Race != nil { + return *v.Race + } + + return +} + +// IsSetRace returns true if Race is not nil. +func (v *PersonalInfo) IsSetRace() bool { + return v != nil && v.Race != nil +} + // A point in 2D space. type Point struct { X float64 `json:"x,required"` @@ -8296,11 +8360,11 @@ var ThriftModule = &thriftreflect.ThriftModule{ Name: "structs", Package: "go.uber.org/thriftrw/gen/internal/tests/structs", FilePath: "structs.thrift", - SHA1: "2f43f38d30f0f9e3b089118d7ffc6b4dc532ada6", + SHA1: "29ff87a73c860add2d19995adbd0351cf24423c3", Includes: []*thriftreflect.ThriftModule{ enums.ThriftModule, }, Raw: rawIDL, } -const rawIDL = "include \"./enums.thrift\"\n\nstruct EmptyStruct {}\n\n//////////////////////////////////////////////////////////////////////////////\n// Structs with primitives\n\n/**\n * A struct that contains primitive fields exclusively.\n *\n * All fields are required.\n */\nstruct PrimitiveRequiredStruct {\n 1: required bool boolField\n 2: required byte byteField\n 3: required i16 int16Field\n 4: required i32 int32Field\n 5: required i64 int64Field\n 6: required double doubleField\n 7: required string stringField\n 8: required binary binaryField\n}\n\n/**\n * A struct that contains primitive fields exclusively.\n *\n * All fields are optional.\n */\nstruct PrimitiveOptionalStruct {\n 1: optional bool boolField\n 2: optional byte byteField\n 3: optional i16 int16Field\n 4: optional i32 int32Field\n 5: optional i64 int64Field\n 6: optional double doubleField\n 7: optional string stringField\n 8: optional binary binaryField\n}\n\n//////////////////////////////////////////////////////////////////////////////\n// Nested structs (Required)\n\n/**\n * A point in 2D space.\n */\nstruct Point {\n 1: required double x\n 2: required double y\n}\n\n/**\n * Size of something.\n */\nstruct Size {\n /**\n * Width in pixels.\n */\n 1: required double width\n /** Height in pixels. */\n 2: required double height\n}\n\nstruct Frame {\n 1: required Point topLeft\n 2: required Size size\n}\n\nstruct Edge {\n 1: required Point startPoint\n 2: required Point endPoint\n}\n\n/**\n * A graph is comprised of zero or more edges.\n */\nstruct Graph {\n /**\n * List of edges in the graph.\n *\n * May be empty.\n */\n 1: required list edges\n}\n\n//////////////////////////////////////////////////////////////////////////////\n// Nested structs (Optional)\n\nstruct ContactInfo {\n 1: required string emailAddress\n}\n\nstruct PersonalInfo {\n 1: optional i32 age\n}\n\nstruct User {\n 1: required string name\n 2: optional ContactInfo contact\n 3: optional PersonalInfo personal\n}\n\ntypedef map UserMap\n\n//////////////////////////////////////////////////////////////////////////////\n// self-referential struct\n\ntypedef Node List\n\n/**\n * Node is linked list of values.\n * All values are 32-bit integers.\n */\nstruct Node {\n 1: required i32 value\n 2: optional List tail\n}\n\n//////////////////////////////////////////////////////////////////////////////\n// JSON tagged structs\n\nstruct Rename {\n 1: required string Default (go.tag = 'json:\"default\"')\n 2: required string camelCase (go.tag = 'json:\"snake_case\"')\n}\n\nstruct Omit {\n 1: required string serialized\n 2: required string hidden (go.tag = 'json:\"-\"')\n}\n\nstruct GoTags {\n 1: required string Foo (go.tag = 'json:\"-\" foo:\"bar\"')\n 2: optional string Bar (go.tag = 'bar:\"foo\"')\n 3: required string FooBar (go.tag = 'json:\"foobar,option1,option2\" bar:\"foo,option1\" foo:\"foobar\"')\n 4: required string FooBarWithSpace (go.tag = 'json:\"foobarWithSpace\" foo:\"foo bar foobar barfoo\"')\n 5: optional string FooBarWithOmitEmpty (go.tag = 'json:\"foobarWithOmitEmpty,omitempty\"')\n 6: required string FooBarWithRequired (go.tag = 'json:\"foobarWithRequired,required\"')\n}\n\nstruct NotOmitEmpty {\n 1: optional string NotOmitEmptyString (go.tag = 'json:\"notOmitEmptyString,!omitempty\"')\n 2: optional string NotOmitEmptyInt (go.tag = 'json:\"notOmitEmptyInt,!omitempty\"')\n 3: optional string NotOmitEmptyBool (go.tag = 'json:\"notOmitEmptyBool,!omitempty\"')\n 4: optional list NotOmitEmptyList (go.tag = 'json:\"notOmitEmptyList,!omitempty\"')\n 5: optional map NotOmitEmptyMap (go.tag = 'json:\"notOmitEmptyMap,!omitempty\"')\n 6: optional list NotOmitEmptyListMixedWithOmitEmpty (go.tag = 'json:\"notOmitEmptyListMixedWithOmitEmpty,!omitempty,omitempty\"')\n 7: optional list NotOmitEmptyListMixedWithOmitEmptyV2 (go.tag = 'json:\"notOmitEmptyListMixedWithOmitEmptyV2,omitempty,!omitempty\"')\n 8: optional string OmitEmptyString (go.tag = 'json:\"omitEmptyString,omitempty\"') // to test that there can be a mix of fields that do and don't have !omitempty\n}\n\n//////////////////////////////////////////////////////////////////////////////\n// Default values\n\nstruct DefaultsStruct {\n 1: required i32 requiredPrimitive = 100\n 2: optional i32 optionalPrimitive = 200\n\n 3: required enums.EnumDefault requiredEnum = enums.EnumDefault.Bar\n 4: optional enums.EnumDefault optionalEnum = 2\n\n 5: required list requiredList = [\"hello\", \"world\"]\n 6: optional list optionalList = [1, 2.0, 3]\n\n 7: required Frame requiredStruct = {\n \"topLeft\": {\"x\": 1, \"y\": 2},\n \"size\": {\"width\": 100, \"height\": 200},\n }\n 8: optional Edge optionalStruct = {\n \"startPoint\": {\"x\": 1, \"y\": 2},\n \"endPoint\": {\"x\": 3, \"y\": 4},\n }\n\n 9: required bool requiredBoolDefaultTrue = true\n 10: optional bool optionalBoolDefaultTrue = true\n\n 11: required bool requiredBoolDefaultFalse = false\n 12: optional bool optionalBoolDefaultFalse = false\n}\n\n//////////////////////////////////////////////////////////////////////////////\n// Opt-out of Zap\n\nstruct ZapOptOutStruct {\n 1: required string name\n 2: required string optout (go.nolog)\n}\n\n//////////////////////////////////////////////////////////////////////////////\n// Field jabels\n\nstruct StructLabels {\n // reserved keyword as label\n 1: optional bool isRequired (go.label = \"required\")\n\n // go.tag's JSON tag takes precedence over go.label\n 2: optional string foo (go.label = \"bar\", go.tag = 'json:\"not_bar\"')\n\n // Empty label\n 3: optional string qux (go.label = \"\")\n\n // All-caps label\n 4: optional string quux (go.label = \"QUUX\")\n}\n" +const rawIDL = "include \"./enums.thrift\"\n\nstruct EmptyStruct {}\n\n//////////////////////////////////////////////////////////////////////////////\n// Structs with primitives\n\n/**\n * A struct that contains primitive fields exclusively.\n *\n * All fields are required.\n */\nstruct PrimitiveRequiredStruct {\n 1: required bool boolField\n 2: required byte byteField\n 3: required i16 int16Field\n 4: required i32 int32Field\n 5: required i64 int64Field\n 6: required double doubleField\n 7: required string stringField\n 8: required binary binaryField\n}\n\n/**\n * A struct that contains primitive fields exclusively.\n *\n * All fields are optional.\n */\nstruct PrimitiveOptionalStruct {\n 1: optional bool boolField\n 2: optional byte byteField\n 3: optional i16 int16Field\n 4: optional i32 int32Field\n 5: optional i64 int64Field\n 6: optional double doubleField\n 7: optional string stringField\n 8: optional binary binaryField\n}\n\n//////////////////////////////////////////////////////////////////////////////\n// Nested structs (Required)\n\n/**\n * A point in 2D space.\n */\nstruct Point {\n 1: required double x\n 2: required double y\n}\n\n/**\n * Size of something.\n */\nstruct Size {\n /**\n * Width in pixels.\n */\n 1: required double width\n /** Height in pixels. */\n 2: required double height\n}\n\nstruct Frame {\n 1: required Point topLeft\n 2: required Size size\n}\n\nstruct Edge {\n 1: required Point startPoint\n 2: required Point endPoint\n}\n\n/**\n * A graph is comprised of zero or more edges.\n */\nstruct Graph {\n /**\n * List of edges in the graph.\n *\n * May be empty.\n */\n 1: required list edges\n}\n\n//////////////////////////////////////////////////////////////////////////////\n// Nested structs (Optional)\n\nstruct ContactInfo {\n 1: required string emailAddress\n}\n\nstruct PersonalInfo {\n 1: optional i32 age\n 2: optional string race (go.redact)\n}\n\nstruct User {\n 1: required string name\n 2: optional ContactInfo contact\n 3: optional PersonalInfo personal\n}\n\ntypedef map UserMap\n\n//////////////////////////////////////////////////////////////////////////////\n// self-referential struct\n\ntypedef Node List\n\n/**\n * Node is linked list of values.\n * All values are 32-bit integers.\n */\nstruct Node {\n 1: required i32 value\n 2: optional List tail\n}\n\n//////////////////////////////////////////////////////////////////////////////\n// JSON tagged structs\n\nstruct Rename {\n 1: required string Default (go.tag = 'json:\"default\"')\n 2: required string camelCase (go.tag = 'json:\"snake_case\"')\n}\n\nstruct Omit {\n 1: required string serialized\n 2: required string hidden (go.tag = 'json:\"-\"')\n}\n\nstruct GoTags {\n 1: required string Foo (go.tag = 'json:\"-\" foo:\"bar\"')\n 2: optional string Bar (go.tag = 'bar:\"foo\"')\n 3: required string FooBar (go.tag = 'json:\"foobar,option1,option2\" bar:\"foo,option1\" foo:\"foobar\"')\n 4: required string FooBarWithSpace (go.tag = 'json:\"foobarWithSpace\" foo:\"foo bar foobar barfoo\"')\n 5: optional string FooBarWithOmitEmpty (go.tag = 'json:\"foobarWithOmitEmpty,omitempty\"')\n 6: required string FooBarWithRequired (go.tag = 'json:\"foobarWithRequired,required\"')\n}\n\nstruct NotOmitEmpty {\n 1: optional string NotOmitEmptyString (go.tag = 'json:\"notOmitEmptyString,!omitempty\"')\n 2: optional string NotOmitEmptyInt (go.tag = 'json:\"notOmitEmptyInt,!omitempty\"')\n 3: optional string NotOmitEmptyBool (go.tag = 'json:\"notOmitEmptyBool,!omitempty\"')\n 4: optional list NotOmitEmptyList (go.tag = 'json:\"notOmitEmptyList,!omitempty\"')\n 5: optional map NotOmitEmptyMap (go.tag = 'json:\"notOmitEmptyMap,!omitempty\"')\n 6: optional list NotOmitEmptyListMixedWithOmitEmpty (go.tag = 'json:\"notOmitEmptyListMixedWithOmitEmpty,!omitempty,omitempty\"')\n 7: optional list NotOmitEmptyListMixedWithOmitEmptyV2 (go.tag = 'json:\"notOmitEmptyListMixedWithOmitEmptyV2,omitempty,!omitempty\"')\n 8: optional string OmitEmptyString (go.tag = 'json:\"omitEmptyString,omitempty\"') // to test that there can be a mix of fields that do and don't have !omitempty\n}\n\n//////////////////////////////////////////////////////////////////////////////\n// Default values\n\nstruct DefaultsStruct {\n 1: required i32 requiredPrimitive = 100\n 2: optional i32 optionalPrimitive = 200\n\n 3: required enums.EnumDefault requiredEnum = enums.EnumDefault.Bar\n 4: optional enums.EnumDefault optionalEnum = 2\n\n 5: required list requiredList = [\"hello\", \"world\"]\n 6: optional list optionalList = [1, 2.0, 3]\n\n 7: required Frame requiredStruct = {\n \"topLeft\": {\"x\": 1, \"y\": 2},\n \"size\": {\"width\": 100, \"height\": 200},\n }\n 8: optional Edge optionalStruct = {\n \"startPoint\": {\"x\": 1, \"y\": 2},\n \"endPoint\": {\"x\": 3, \"y\": 4},\n }\n\n 9: required bool requiredBoolDefaultTrue = true\n 10: optional bool optionalBoolDefaultTrue = true\n\n 11: required bool requiredBoolDefaultFalse = false\n 12: optional bool optionalBoolDefaultFalse = false\n}\n\n//////////////////////////////////////////////////////////////////////////////\n// Opt-out of Zap\n\nstruct ZapOptOutStruct {\n 1: required string name\n 2: required string optout (go.nolog)\n}\n\n//////////////////////////////////////////////////////////////////////////////\n// Field jabels\n\nstruct StructLabels {\n // reserved keyword as label\n 1: optional bool isRequired (go.label = \"required\")\n\n // go.tag's JSON tag takes precedence over go.label\n 2: optional string foo (go.label = \"bar\", go.tag = 'json:\"not_bar\"')\n\n // Empty label\n 3: optional string qux (go.label = \"\")\n\n // All-caps label\n 4: optional string quux (go.label = \"QUUX\")\n}\n" diff --git a/gen/internal/tests/thrift/exceptions.thrift b/gen/internal/tests/thrift/exceptions.thrift index 725b1695..23ae55b8 100644 --- a/gen/internal/tests/thrift/exceptions.thrift +++ b/gen/internal/tests/thrift/exceptions.thrift @@ -7,6 +7,7 @@ exception DoesNotExistException { /** Key that was missing. */ 1: required string key 2: optional string Error (go.name="Error2") + 3: optional string userName (go.redact) } exception Does_Not_Exist_Exception_Collision { diff --git a/gen/internal/tests/thrift/structs.thrift b/gen/internal/tests/thrift/structs.thrift index b1b0efef..ef08a3f1 100644 --- a/gen/internal/tests/thrift/structs.thrift +++ b/gen/internal/tests/thrift/structs.thrift @@ -91,6 +91,7 @@ struct ContactInfo { struct PersonalInfo { 1: optional i32 age + 2: optional string race (go.redact) } struct User { diff --git a/gen/zap.go b/gen/zap.go index 91a43019..f00406f5 100644 --- a/gen/zap.go +++ b/gen/zap.go @@ -46,7 +46,7 @@ type zapGenerator struct { // zapEncoder returns the Zap type name of the root spec, determining what type // the Zap marshaler needs to log it as (i.e. AddString, AppendObject, etc.) -func (z *zapGenerator) zapEncoder(g Generator, spec compile.TypeSpec) string { +func (z *zapGenerator) zapEncoder(_ Generator, spec compile.TypeSpec) string { root := compile.RootTypeSpec(spec) switch t := root.(type) { diff --git a/gen/zap_test.go b/gen/zap_test.go index f5025b57..1d96ecc2 100644 --- a/gen/zap_test.go +++ b/gen/zap_test.go @@ -28,6 +28,7 @@ import ( gomock "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/thriftrw/compile" tc "go.uber.org/thriftrw/gen/internal/tests/containers" te "go.uber.org/thriftrw/gen/internal/tests/enums" tz "go.uber.org/thriftrw/gen/internal/tests/nozap" @@ -852,3 +853,31 @@ func TestLogNilStruct(t *testing.T) { require.NoError(t, x.MarshalLogObject(enc)) assert.Empty(t, enc.Fields) } + +func TestZapOptOut(t *testing.T) { + foo := &compile.FieldSpec{ + Name: "foo", + } + redact := &compile.FieldSpec{ + Name: "redact", + Annotations: compile.Annotations{RedactLabel: ""}, + } + nolog := &compile.FieldSpec{ + Name: "nolog", + Annotations: compile.Annotations{NoZapLabel: ""}, + } + tests := []struct { + name string + spec *compile.FieldSpec + want bool + }{ + {name: "no annotation", spec: foo, want: false}, + {name: "redact annotation", spec: redact, want: false}, + {name: "nolog annotation", spec: nolog, want: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, zapOptOut(tt.spec), "zapOptOut(%v)", tt.spec) + }) + } +}