diff --git a/boilingcore/templates.go b/boilingcore/templates.go index a3a6b3c9..1988d5e5 100644 --- a/boilingcore/templates.go +++ b/boilingcore/templates.go @@ -13,8 +13,8 @@ import ( "text/template" "github.com/Masterminds/sprig/v3" - "github.com/friendsofgo/errors" "github.com/aarondl/strmangle" + "github.com/friendsofgo/errors" "github.com/aarondl/sqlboiler/v4/drivers" ) @@ -348,6 +348,37 @@ var templateFunctions = template.FuncMap{ "getTable": drivers.GetTable, } +// camelCaseNoInitialisms converts a snake_case string to camelCase for struct +// tags. Unlike strmangle.CamelCase, it does not uppercase common Go initialisms +// such as "ID" or "URL". For example, "account_id" becomes "accountId" instead +// of "accountID", following the conventional camelCase used in JSON APIs. +func camelCaseNoInitialisms(name string) string { + if name == "" { + return "" + } + + buf := strmangle.GetBuffer() + defer strmangle.PutBuffer(buf) + + firstWritten := false + for _, word := range strings.Split(name, "_") { + if word == "" { + continue + } + if !firstWritten { + buf.WriteString(strings.ToLower(word)) + firstWritten = true + } else { + buf.WriteString(strings.ToUpper(word[:1])) + if len(word) > 1 { + buf.WriteString(strings.ToLower(word[1:])) + } + } + } + + return buf.String() +} + func generateTagWithCase(tagName, tagValue, alias string, c TagCase, nullable bool) string { buf := strmangle.GetBuffer() defer strmangle.PutBuffer(buf) @@ -361,7 +392,7 @@ func generateTagWithCase(tagName, tagValue, alias string, c TagCase, nullable bo case TagCaseTitle: buf.WriteString(strmangle.TitleCase(tagValue)) case TagCaseCamel: - buf.WriteString(strmangle.CamelCase(tagValue)) + buf.WriteString(camelCaseNoInitialisms(tagValue)) case TagCaseAlias: buf.WriteString(alias) default: diff --git a/boilingcore/templates_test.go b/boilingcore/templates_test.go index 25e4777e..90bc9f28 100644 --- a/boilingcore/templates_test.go +++ b/boilingcore/templates_test.go @@ -35,6 +35,58 @@ func TestTemplateNameListSort(t *testing.T) { } } +func TestCamelCaseNoInitialisms(t *testing.T) { + t.Parallel() + + tests := []struct { + input string + expected string + }{ + {"account_id", "accountId"}, + {"user_name", "userName"}, + {"http_url", "httpUrl"}, + {"first_last_name", "firstLastName"}, + {"id", "id"}, + {"column_name_id", "columnNameId"}, + {"", ""}, + {"single", "single"}, + {"a_b_c", "aBC"}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + got := camelCaseNoInitialisms(tt.input) + if got != tt.expected { + t.Errorf("camelCaseNoInitialisms(%q) = %q, want %q", tt.input, got, tt.expected) + } + }) + } +} + +func TestGenerateTagWithCaseCamel(t *testing.T) { + t.Parallel() + + tests := []struct { + input string + expected string + }{ + {"account_id", `json:"accountId" `}, + {"user_name", `json:"userName" `}, + {"http_url", `json:"httpUrl" `}, + {"id", `json:"id" `}, + {"column_name_id", `json:"columnNameId" `}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + got := generateTagWithCase("json", tt.input, tt.input, TagCaseCamel, false) + if got != tt.expected { + t.Errorf("generateTagWithCase(%q, camel) = %q, want %q", tt.input, got, tt.expected) + } + }) + } +} + func TestTemplateList_Templates(t *testing.T) { t.Parallel()