From 2a21ae5c1cf24a5a4b449e89551ff295f695571c Mon Sep 17 00:00:00 2001 From: chavacava Date: Sat, 20 Sep 2025 18:54:36 +0200 Subject: [PATCH] feature: support `cbor` struct tag in `struc-tag` rule --- rule/struct_tag.go | 62 +++++++++++++++++++++++++++++ test/struct_tag_test.go | 1 + testdata/struct_tag.go | 21 ++++++++++ testdata/struct_tag_user_options.go | 8 ++++ 4 files changed, 92 insertions(+) diff --git a/rule/struct_tag.go b/rule/struct_tag.go index e0f2066..02110bf 100644 --- a/rule/struct_tag.go +++ b/rule/struct_tag.go @@ -24,6 +24,7 @@ type tagKey string const ( keyASN1 tagKey = "asn1" keyBSON tagKey = "bson" + keyCbor tagKey = "cbor" keyCodec tagKey = "codec" keyDatastore tagKey = "datastore" keyDefault tagKey = "default" @@ -45,6 +46,7 @@ type tagChecker func(checkCtx *checkContext, tag *structtag.Tag, field *ast.Fiel var tagCheckers = map[tagKey]tagChecker{ keyASN1: checkASN1Tag, keyBSON: checkBSONTag, + keyCbor: checkCborTag, keyCodec: checkCodecTag, keyDatastore: checkDatastoreTag, keyDefault: checkDefaultTag, @@ -385,6 +387,66 @@ func checkBSONTag(checkCtx *checkContext, tag *structtag.Tag, _ *ast.Field) (mes return "", true } +func checkCborTag(checkCtx *checkContext, tag *structtag.Tag, _ *ast.Field) (message string, succeeded bool) { + hasToArray := false + hasOmitEmptyOrZero := false + hasKeyAsInt := false + + for _, opt := range tag.Options { + switch opt { + case "omitempty", "omitzero": + hasOmitEmptyOrZero = true + case "toarray": + if tag.Name != "" { + return `tag name for option "toarray" should be empty`, false + } + hasToArray = true + case "keyasint": + intKey, err := strconv.Atoi(tag.Name) + if err != nil { + return `tag name for option "keyasint" should be an integer`, false + } + + _, ok := checkCtx.usedTagNbr[intKey] + if ok { + return fmt.Sprintf("duplicated integer key %d", intKey), false + } + + checkCtx.usedTagNbr[intKey] = true + hasKeyAsInt = true + continue + + default: + if !checkCtx.isUserDefined(keyCbor, opt) { + return fmt.Sprintf(msgUnknownOption, opt), false + } + } + } + + // Check for duplicated tag names + if tag.Name != "" { + _, ok := checkCtx.usedTagName[tag.Name] + if ok { + return fmt.Sprintf("duplicated tag name %s", tag.Name), false + } + checkCtx.usedTagName[tag.Name] = true + } + + // Check for integer tag names without keyasint option + if !hasKeyAsInt { + _, err := strconv.Atoi(tag.Name) + if err == nil { + return `integer tag names are only allowed in presence of "keyasint" option`, false + } + } + + if hasToArray && hasOmitEmptyOrZero { + return `options "omitempty" and "omitzero" are ignored in presence of "toarray" option`, false + } + + return "", true +} + const structTagCodecSpecialField = "_struct" func checkCodecTag(checkCtx *checkContext, tag *structtag.Tag, field *ast.Field) (message string, succeeded bool) { diff --git a/test/struct_tag_test.go b/test/struct_tag_test.go index dce3c61..5f521de 100644 --- a/test/struct_tag_test.go +++ b/test/struct_tag_test.go @@ -23,6 +23,7 @@ func TestStructTagWithUserOptions(t *testing.T) { "toml,unknown", "spanner,mySpannerOption", "codec,myCodecOption", + "cbor,myCborOption", }, }) } diff --git a/testdata/struct_tag.go b/testdata/struct_tag.go index 869acc3..f6b70f8 100644 --- a/testdata/struct_tag.go +++ b/testdata/struct_tag.go @@ -215,3 +215,24 @@ type TestDuplicatedCodecTags struct { B int `codec:"field_a"` // MATCH /duplicated tag name "field_a" in codec tag/ C int `codec:"field_c"` } + +type Cbor struct { + RepeatedStr string `cbor:"errors"` + Repeated string `cbor:"1,keyasint"` + Useless string `cbor:"-,omitempty"` // MATCH /useless option omitempty for ignored field in cbor tag/ + Inputs string `cbor:",keyasint"` // MATCH /tag name for option "keyasint" should be an integer in cbor tag/ + Outputs string `cbor:"inputs,keyasint"` // MATCH /tag name for option "keyasint" should be an integer in cbor tag/ + Errors string `cbor:"errors,optempty,keyasint"` // MATCH /unknown option "optempty" in cbor tag/ + Inputs2 string `cbor:"-10,omitempty"` // MATCH /integer tag names are only allowed in presence of "keyasint" option in cbor tag/ + Outputs2 string `cbor:"-12,toarray"` // MATCH /tag name for option "toarray" should be empty in cbor tag/ + RepeatedInt string `cbor:"1,keyasint"` // MATCH /duplicated integer key 1 in cbor tag/ + RepeatedStr string `cbor:"errors,omitempty"` // MATCH /duplicated tag name errors in cbor tag/ + Useless string `cbor:",toarray,omitempty"` // MATCH /options "omitempty" and "omitzero" are ignored in presence of "toarray" option in cbor tag/ + Useless2 string `cbor:",omitzero,toarray"` // MATCH /options "omitempty" and "omitzero" are ignored in presence of "toarray" option in cbor tag/ + // OK + InputsOk string `cbor:"8,keyasint"` + OutputsOk string `cbor:"-100,keyasint"` + ErrorsOk string `cbor:"-1,keyasint"` + InputsOk2 string `cbor:"inputs,omitempty"` + OutputsOk2 string `cbor:",toarray"` +} diff --git a/testdata/struct_tag_user_options.go b/testdata/struct_tag_user_options.go index a60fa3d..6f3f520 100644 --- a/testdata/struct_tag_user_options.go +++ b/testdata/struct_tag_user_options.go @@ -102,3 +102,11 @@ type CodecUserOptions struct { ID int `codec:"user_id,myCodecOption"` Name string `codec:"full_name,unknownOption"` // MATCH /unknown option "unknownOption" in codec tag/ } + +type CborUserOptions struct { + InputsOk string `cbor:"8,keyasint,myCborOption"` + OutputsOk string `cbor:"-100,keyasint,unknownOption"` // MATCH /unknown option "unknownOption" in cbor tag/ + ErrorsOk string `cbor:"-1,keyasint"` + InputsOk2 string `cbor:"inputs,omitempty"` + OutputsOk2 string `cbor:",toarray"` +}