diff --git a/plugin/modelgen/models.go b/plugin/modelgen/models.go index 016623f1071..44ae557c0f9 100644 --- a/plugin/modelgen/models.go +++ b/plugin/modelgen/models.go @@ -450,17 +450,71 @@ func GoTagFieldHook(td *ast.Definition, fd *ast.FieldDefinition, f *Field) (*Fie return f, nil } +// splitTagsBySpace split tags by space, except when space is inside quotes +func splitTagsBySpace(tagsString string) []string { + var tags []string + var currentTag string + inQuotes := false + + for _, c := range tagsString { + if c == '"' { + inQuotes = !inQuotes + } + if c == ' ' && !inQuotes { + tags = append(tags, currentTag) + currentTag = "" + } else { + currentTag += string(c) + } + } + tags = append(tags, currentTag) + + return tags +} + +// containsInvalidSpace checks if the tagsString contains invalid space +func containsInvalidSpace(valuesString string) bool { + // get rid of quotes + valuesString = strings.ReplaceAll(valuesString, "\"", "") + if strings.Contains(valuesString, ",") { + // split by comma, + values := strings.Split(valuesString, ",") + for _, value := range values { + if strings.TrimSpace(value) != value { + return true + } + } + return false + } + if strings.Contains(valuesString, ";") { + // split by semicolon, which is common in gorm + values := strings.Split(valuesString, ";") + for _, value := range values { + if strings.TrimSpace(value) != value { + return true + } + } + return false + } + // single value + if strings.TrimSpace(valuesString) != valuesString { + return true + } + return false +} + func removeDuplicateTags(t string) string { processed := make(map[string]bool) - tt := strings.Split(t, " ") + tt := splitTagsBySpace(t) returnTags := "" // iterate backwards through tags so appended goTag directives are prioritized for i := len(tt) - 1; i >= 0; i-- { ti := tt[i] // check if ti contains ":", and not contains any empty space. if not, tag is in wrong format - if !strings.Contains(ti, ":") || strings.Contains(ti, " ") { - panic(fmt.Errorf("wrong format of tags: %s. goTag directive should be in format: @goTag(key: \"something\", value:\"value1,value2,etc\"), no empty space is allowed", t)) + // correct example: json:"name" + if !strings.Contains(ti, ":") { + panic(fmt.Errorf("wrong format of tags: %s. goTag directive should be in format: @goTag(key: \"something\", value:\"value\"), ", t)) } kv := strings.Split(ti, ":") @@ -472,6 +526,12 @@ func removeDuplicateTags(t string) string { if len(returnTags) > 0 { returnTags = " " + returnTags } + + isContained := containsInvalidSpace(kv[1]) + if isContained { + panic(fmt.Errorf("tag value should not contain any leading or trailing spaces: %s", kv[1])) + } + returnTags = kv[0] + ":" + kv[1] + returnTags } diff --git a/plugin/modelgen/models_test.go b/plugin/modelgen/models_test.go index 03d7448e737..fb7a65fb3e0 100644 --- a/plugin/modelgen/models_test.go +++ b/plugin/modelgen/models_test.go @@ -428,13 +428,37 @@ func TestRemoveDuplicate(t *testing.T) { want: "something:\"name2\" json:\"name3\"", }, { - name: "Test value with empty space", + name: "Test tag value with leading empty space", args: args{ t: "json:\"name, name2\"", }, want: "json:\"name, name2\"", wantPanic: true, }, + { + name: "Test tag value with trailing empty space", + args: args{ + t: "json:\"name,name2 \"", + }, + want: "json:\"name,name2 \"", + wantPanic: true, + }, + { + name: "Test tag value with space in between", + args: args{ + t: "gorm:\"unique;not null\"", + }, + want: "gorm:\"unique;not null\"", + wantPanic: false, + }, + { + name: "Test mix use of gorm and json tags", + args: args{ + t: "gorm:\"unique;not null\" json:\"name,name2\"", + }, + want: "gorm:\"unique;not null\" json:\"name,name2\"", + wantPanic: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -448,3 +472,93 @@ func TestRemoveDuplicate(t *testing.T) { }) } } + +func Test_containsInvalidSpace(t *testing.T) { + type args struct { + valuesString string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "Test tag value with leading empty space", + args: args{ + valuesString: "name, name2", + }, + want: true, + }, + { + name: "Test tag value with trailing empty space", + args: args{ + valuesString: "name ,name2", + }, + want: true, + }, + { + name: "Test tag value with valid empty space in words", + args: args{ + valuesString: "accept this,name2", + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, containsInvalidSpace(tt.args.valuesString), "containsInvalidSpace(%v)", tt.args.valuesString) + }) + } +} + +func Test_splitTagsBySpace(t *testing.T) { + type args struct { + tagsString string + } + tests := []struct { + name string + args args + want []string + }{ + { + name: "multiple tags, single value", + args: args{ + tagsString: "json:\"name\" something:\"name2\" json:\"name3\"", + }, + want: []string{"json:\"name\"", "something:\"name2\"", "json:\"name3\""}, + }, + { + name: "multiple tag, multiple values", + args: args{ + tagsString: "json:\"name\" something:\"name2\" json:\"name3,name4\"", + }, + want: []string{"json:\"name\"", "something:\"name2\"", "json:\"name3,name4\""}, + }, + { + name: "single tag, single value", + args: args{ + tagsString: "json:\"name\"", + }, + want: []string{"json:\"name\""}, + }, + { + name: "single tag, multiple values", + args: args{ + tagsString: "json:\"name,name2\"", + }, + want: []string{"json:\"name,name2\""}, + }, + { + name: "space in value", + args: args{ + tagsString: "gorm:\"not nul,name2\"", + }, + want: []string{"gorm:\"not nul,name2\""}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, splitTagsBySpace(tt.args.tagsString), "splitTagsBySpace(%v)", tt.args.tagsString) + }) + } +}