Skip to content

Commit

Permalink
chore: add test to catch invalid struct tags (#3035)
Browse files Browse the repository at this point in the history
* chore: add test to catch invalid struct tags

* chore: add typeName for better output

* chore: add more context
  • Loading branch information
markphelps authored May 1, 2024
1 parent 188a072 commit 2868dd3
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ require (
github.com/hashicorp/cap v0.6.0
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/iancoleman/strcase v0.3.0
github.com/jackc/pgx/v5 v5.5.5
github.com/libsql/libsql-client-go v0.0.0-20230917132930-48c310b27e7b
github.com/magefile/mage v1.15.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,8 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
Expand Down
93 changes: 93 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"testing"
"time"

"github.com/iancoleman/strcase"
"github.com/santhosh-tekuri/jsonschema/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -1475,3 +1476,95 @@ func TestGetConfigFile(t *testing.T) {
})
}
}

var (
// add any struct tags to match their camelCase equivalents here.
camelCaseMatchers = map[string]string{
"requireTLS": "requireTLS",
"discoveryURL": "discoveryURL",
}
)

func TestStructTags(t *testing.T) {
configType := reflect.TypeOf(Config{})
configTags := getStructTags(configType)

for k, v := range camelCaseMatchers {
strcase.ConfigureAcronym(k, v)
}

// Validate the struct tags for the Config struct.
// recursively validate the struct tags for all sub-structs.
validateStructTags(t, configTags, configType)
}

func validateStructTags(t *testing.T, tags map[string]map[string]string, tType reflect.Type) {
tName := tType.Name()
for fieldName, fieldTags := range tags {
fieldType, ok := tType.FieldByName(fieldName)
require.True(t, ok, "field %s not found in type %s", fieldName, tName)

// Validate the `json` struct tag.
jsonTag, ok := fieldTags["json"]
if ok {
require.True(t, isCamelCase(jsonTag), "json tag for field '%s.%s' should be camelCase but is '%s'", tName, fieldName, jsonTag)
}

// Validate the `mapstructure` struct tag.
mapstructureTag, ok := fieldTags["mapstructure"]
if ok {
require.True(t, isSnakeCase(mapstructureTag), "mapstructure tag for field '%s.%s' should be snake_case but is '%s'", tName, fieldName, mapstructureTag)
}

// Validate the `yaml` struct tag.
yamlTag, ok := fieldTags["yaml"]
if ok {
require.True(t, isSnakeCase(yamlTag), "yaml tag for field '%s.%s' should be snake_case but is '%s'", tName, fieldName, yamlTag)
}

// recursively validate the struct tags for all sub-structs.
if fieldType.Type.Kind() == reflect.Struct {
validateStructTags(t, getStructTags(fieldType.Type), fieldType.Type)
}
}
}

func isCamelCase(s string) bool {
return s == strcase.ToLowerCamel(s)
}

func isSnakeCase(s string) bool {
return s == strcase.ToSnake(s)
}

func getStructTags(t reflect.Type) map[string]map[string]string {
tags := make(map[string]map[string]string)

for i := 0; i < t.NumField(); i++ {
field := t.Field(i)

// Get the field name.
fieldName := field.Name

// Get the field tags.
fieldTags := make(map[string]string)
for _, tag := range []string{"json", "mapstructure", "yaml"} {
tagValue := field.Tag.Get(tag)
if tagValue == "-" {
fieldTags[tag] = "skip"
continue
}
values := strings.Split(tagValue, ",")
if len(values) > 1 {
tagValue = values[0]
}
if tagValue != "" {
fieldTags[tag] = tagValue
}
}

tags[fieldName] = fieldTags
}

return tags
}

0 comments on commit 2868dd3

Please sign in to comment.