From ba65c2ab0cf7d336e26fe6d405b646b974611cbb Mon Sep 17 00:00:00 2001 From: Carson Krueger Date: Sun, 3 Aug 2025 19:34:31 -0600 Subject: [PATCH 1/6] add json tag support --- cmd/jet/main.go | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/cmd/jet/main.go b/cmd/jet/main.go index 5230bd6..2460d16 100644 --- a/cmd/jet/main.go +++ b/cmd/jet/main.go @@ -6,6 +6,7 @@ import ( "flag" "fmt" "os" + "slices" "strings" _ "github.com/go-sql-driver/mysql" @@ -17,6 +18,7 @@ import ( postgresgen "github.com/go-jet/jet/v2/generator/postgres" sqlitegen "github.com/go-jet/jet/v2/generator/sqlite" "github.com/go-jet/jet/v2/generator/template" + "github.com/go-jet/jet/v2/internal/3rdparty/snaker" "github.com/go-jet/jet/v2/internal/jet" "github.com/go-jet/jet/v2/internal/utils/errfmt" "github.com/go-jet/jet/v2/internal/utils/strslice" @@ -54,10 +56,12 @@ var ( tables string views string enums string + + modelJsonTag string ) type templateFilter struct { - names []string + names []string ignore bool } @@ -93,6 +97,7 @@ func init() { flag.StringVar(&tablePkg, "rel-table-path", "table", "Relative path for the Table files package from the destination directory.") flag.StringVar(&viewPkg, "rel-view-path", "view", "Relative path for the View files package from the destination directory.") flag.StringVar(&enumPkg, "rel-enum-path", "enum", "Relative path for the Enum files package from the destination directory.") + flag.StringVar(&modelJsonTag, "model-json-tag", "", "Json tag model to be included in Go structs. (optional)(default )(allowed values: , pascal-case, camel-case, snake-case") flag.StringVar(&tables, "tables", "", `Comma-separated list of tables to generate.`) flag.StringVar(&views, "views", "", `Comma-separated list of views to generate.`) @@ -107,6 +112,10 @@ func main() { printErrorAndExit("ERROR: required flag(s) missing") } + if !slices.Contains([]string{"", "snake-case", "pascal-case", "camel-case"}, modelJsonTag) { + printErrorAndExit("ERROR: json tag does not contain correct value") + } + source := getSource() tablesFilter := createTemplateFilter(ignoreTables, tables, "tables") viewsFilter := createTemplateFilter(ignoreViews, views, "views") @@ -259,8 +268,6 @@ func parseList(list string) []string { return ret } - - func genTemplate(dialect jet.Dialect, tablesFilter, viewsFilter, enumsFilter templateFilter) template.Template { return template.Default(dialect). UseSchema(func(schemaMetaData metadata.Schema) template.Schema { @@ -270,7 +277,22 @@ func genTemplate(dialect jet.Dialect, tablesFilter, viewsFilter, enumsFilter tem if shouldSkipTable(table, tablesFilter) { return template.TableModel{Skip: true} } - return template.DefaultTableModel(table) + return template.DefaultTableModel(table). + UseField(func(columnMetaData metadata.Column) template.TableModelField { + defaultTableModelField := template.DefaultTableModelField(columnMetaData) + + var tags []string + switch modelJsonTag { + case "snake-case": + tags = append(tags, fmt.Sprintf(`json:"%s"`, columnMetaData.Name)) + case "camel-case": + tags = append(tags, fmt.Sprintf(`json:"%s"`, snaker.SnakeToCamel(columnMetaData.Name, false))) + case "pascal-case": + tags = append(tags, fmt.Sprintf(`json:"%s"`, snaker.SnakeToCamel(columnMetaData.Name, true))) + } + + return defaultTableModelField.UseTags(tags...) + }) }). UseView(func(view metadata.Table) template.ViewModel { if shouldSkipTable(view, viewsFilter) { @@ -318,13 +340,13 @@ func createTemplateFilter(ignoreList, allowList, filterType string) templateFilt if allowList != "" { return templateFilter{ - names: parseList(allowList), + names: parseList(allowList), ignore: false, } } return templateFilter{ - names: parseList(ignoreList), + names: parseList(ignoreList), ignore: true, } } From 9cc22af2dd270da08d34e8c3d48fec5b8f2ddf56 Mon Sep 17 00:00:00 2001 From: Carson Krueger Date: Sun, 3 Aug 2025 22:19:14 -0600 Subject: [PATCH 2/6] add tests for model-json-tag --- tests/postgres/generator_test.go | 94 ++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/tests/postgres/generator_test.go b/tests/postgres/generator_test.go index 89aa9da..4fc7821 100644 --- a/tests/postgres/generator_test.go +++ b/tests/postgres/generator_test.go @@ -21,6 +21,7 @@ import ( "github.com/go-jet/jet/v2/tests/.gentestdata/jetdb/dvds/model" "github.com/go-jet/jet/v2/tests/dbconfig" "github.com/go-jet/jet/v2/tests/internal/utils/file" + file2 "github.com/go-jet/jet/v2/tests/internal/utils/file" ) func dsn(host string, port int, dbName, user, password string) string { @@ -1505,3 +1506,96 @@ func TestAllowAndIgnoreEnums(t *testing.T) { }) } } + +func TestJsonInvalidModelTags(t *testing.T) { + tests := []struct { + name string + args []string + }{ + { + name: "with invalid json tag", + args: []string{ + "-dsn=" + defaultDSN(), + "-schema=dvds", + "-tables=actor,ADDRESS,country, Film , cITY,", + "-views=Actor_info, FILM_LIST ,staff_list", + "-enums=mpaa_rating", + "-path=" + genTestDir2, + "-model-json-tag=invalid", + }, + }, + { + name: "with invalid json tag", + args: []string{ + "-dsn=" + defaultDSN(), + "-schema=dvds", + "-tables=actor,ADDRESS,country, Film , cITY,", + "-views=Actor_info, FILM_LIST ,staff_list", + "-enums=mpaa_rating", + "-path=" + genTestDir2, + "-model-json-tag= invalid", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := exec.Command("jet", tt.args...) + + var stdOut bytes.Buffer + cmd.Stderr = os.Stderr + cmd.Stdout = &stdOut + + err := cmd.Run() + require.Error(t, err) + require.Equal(t, "exit status 1", err.Error()) + + stdOutput := stdOut.String() + require.Contains(t, stdOutput, "ERROR: json tag does not contain correct value") + }) + } +} + +func TestSnakeCaseModelJsonTag(t *testing.T) { + tests := []struct { + name string + args []string + }{ + { + name: "with snake-case", + args: []string{ + "-dsn=" + defaultDSN(), + "-schema=dvds", + "-views=Actor_info", + "-tables=actor", + "-path=" + genTestDir2, + "-model-json-tag=snake-case", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := exec.Command("jet", tt.args...) + + var stdOut bytes.Buffer + cmd.Stderr = os.Stderr + cmd.Stdout = &stdOut + + err := cmd.Run() + require.Nil(t, err) + + actor := file2.Exists(t, genTestDir2+"/jetdb/dvds/model", "actor.go") + require.Contains(t, actor, `json:"actor_id"`) + require.Contains(t, actor, `json:"first_name"`) + require.Contains(t, actor, `json:"last_name"`) + require.Contains(t, actor, `json:"last_update"`) + + actorInfo := file2.Exists(t, genTestDir2+"/jetdb/dvds/model", "actor_info.go") + require.Contains(t, actorInfo, `json:"actor_id"`) + require.Contains(t, actorInfo, `json:"first_name"`) + require.Contains(t, actorInfo, `json:"last_name"`) + require.Contains(t, actorInfo, `json:"file_info"`) + }) + } +} From ede9118a9a0390318a66182491d45df80cbe3144 Mon Sep 17 00:00:00 2001 From: Carson Krueger Date: Mon, 4 Aug 2025 22:20:29 -0600 Subject: [PATCH 3/6] fix json tags for view models --- cmd/jet/main.go | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/cmd/jet/main.go b/cmd/jet/main.go index 2460d16..de12d5f 100644 --- a/cmd/jet/main.go +++ b/cmd/jet/main.go @@ -280,17 +280,7 @@ func genTemplate(dialect jet.Dialect, tablesFilter, viewsFilter, enumsFilter tem return template.DefaultTableModel(table). UseField(func(columnMetaData metadata.Column) template.TableModelField { defaultTableModelField := template.DefaultTableModelField(columnMetaData) - - var tags []string - switch modelJsonTag { - case "snake-case": - tags = append(tags, fmt.Sprintf(`json:"%s"`, columnMetaData.Name)) - case "camel-case": - tags = append(tags, fmt.Sprintf(`json:"%s"`, snaker.SnakeToCamel(columnMetaData.Name, false))) - case "pascal-case": - tags = append(tags, fmt.Sprintf(`json:"%s"`, snaker.SnakeToCamel(columnMetaData.Name, true))) - } - + tags := createModelTags(columnMetaData) return defaultTableModelField.UseTags(tags...) }) }). @@ -298,7 +288,12 @@ func genTemplate(dialect jet.Dialect, tablesFilter, viewsFilter, enumsFilter tem if shouldSkipTable(view, viewsFilter) { return template.ViewModel{Skip: true} } - return template.DefaultViewModel(view) + return template.DefaultViewModel(view). + UseField(func(columnMetaData metadata.Column) template.TableModelField { + defaultTableModelField := template.DefaultTableModelField(columnMetaData) + tags := createModelTags(columnMetaData) + return defaultTableModelField.UseTags(tags...) + }) }). UseEnum(func(enum metadata.Enum) template.EnumModel { if shouldSkipEnum(enum, enumsFilter) { @@ -366,3 +361,16 @@ func shouldSkipEnum(enum metadata.Enum, filter templateFilter) bool { return !strslice.Contains(filter.names, strings.ToLower(enum.Name)) } + +func createModelTags(columnMetaData metadata.Column) []string { + var tags []string + switch modelJsonTag { + case "snake-case": + tags = append(tags, fmt.Sprintf(`json:"%s"`, columnMetaData.Name)) + case "camel-case": + tags = append(tags, fmt.Sprintf(`json:"%s"`, snaker.SnakeToCamel(columnMetaData.Name, false))) + case "pascal-case": + tags = append(tags, fmt.Sprintf(`json:"%s"`, snaker.SnakeToCamel(columnMetaData.Name, true))) + } + return tags +} From 8f9994800438707219ce24395698a7561afb197d Mon Sep 17 00:00:00 2001 From: Carson Krueger Date: Mon, 4 Aug 2025 22:20:34 -0600 Subject: [PATCH 4/6] add more tests --- tests/postgres/generator_test.go | 94 +++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 3 deletions(-) diff --git a/tests/postgres/generator_test.go b/tests/postgres/generator_test.go index 4fc7821..d8f468c 100644 --- a/tests/postgres/generator_test.go +++ b/tests/postgres/generator_test.go @@ -1585,17 +1585,105 @@ func TestSnakeCaseModelJsonTag(t *testing.T) { err := cmd.Run() require.Nil(t, err) - actor := file2.Exists(t, genTestDir2+"/jetdb/dvds/model", "actor.go") + actor := file2.Exists(t, genTestDir2+"/jetdb/dvds/model/actor.go") require.Contains(t, actor, `json:"actor_id"`) require.Contains(t, actor, `json:"first_name"`) require.Contains(t, actor, `json:"last_name"`) require.Contains(t, actor, `json:"last_update"`) - actorInfo := file2.Exists(t, genTestDir2+"/jetdb/dvds/model", "actor_info.go") + actorInfo := file2.Exists(t, genTestDir2+"/jetdb/dvds/model/actor_info.go") require.Contains(t, actorInfo, `json:"actor_id"`) require.Contains(t, actorInfo, `json:"first_name"`) require.Contains(t, actorInfo, `json:"last_name"`) - require.Contains(t, actorInfo, `json:"file_info"`) + require.Contains(t, actorInfo, `json:"film_info"`) + }) + } +} + +func TestPascalCaseModelJsonTag(t *testing.T) { + tests := []struct { + name string + args []string + }{ + { + name: "with pascal-case", + args: []string{ + "-dsn=" + defaultDSN(), + "-schema=dvds", + "-views=Actor_info", + "-tables=actor", + "-path=" + genTestDir2, + "-model-json-tag=pascal-case", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := exec.Command("jet", tt.args...) + + var stdOut bytes.Buffer + cmd.Stderr = os.Stderr + cmd.Stdout = &stdOut + + err := cmd.Run() + require.Nil(t, err) + + actor := file2.Exists(t, genTestDir2+"/jetdb/dvds/model/actor.go") + require.Contains(t, actor, `json:"ActorID"`) + require.Contains(t, actor, `json:"FirstName"`) + require.Contains(t, actor, `json:"LastName"`) + require.Contains(t, actor, `json:"LastUpdate"`) + + actorInfo := file2.Exists(t, genTestDir2+"/jetdb/dvds/model/actor_info.go") + require.Contains(t, actorInfo, `json:"ActorID"`) + require.Contains(t, actorInfo, `json:"FirstName"`) + require.Contains(t, actorInfo, `json:"LastName"`) + require.Contains(t, actorInfo, `json:"FilmInfo"`) + }) + } +} + +func TestCamelCaseModelJsonTag(t *testing.T) { + tests := []struct { + name string + args []string + }{ + { + name: "with camel-case", + args: []string{ + "-dsn=" + defaultDSN(), + "-schema=dvds", + "-views=Actor_info", + "-tables=actor", + "-path=" + genTestDir2, + "-model-json-tag=camel-case", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := exec.Command("jet", tt.args...) + + var stdOut bytes.Buffer + cmd.Stderr = os.Stderr + cmd.Stdout = &stdOut + + err := cmd.Run() + require.Nil(t, err) + + actor := file2.Exists(t, genTestDir2+"/jetdb/dvds/model/actor.go") + require.Contains(t, actor, `json:"actorID"`) + require.Contains(t, actor, `json:"firstName"`) + require.Contains(t, actor, `json:"lastName"`) + require.Contains(t, actor, `json:"lastUpdate"`) + + actorInfo := file2.Exists(t, genTestDir2+"/jetdb/dvds/model/actor_info.go") + require.Contains(t, actorInfo, `json:"actorID"`) + require.Contains(t, actorInfo, `json:"firstName"`) + require.Contains(t, actorInfo, `json:"lastName"`) + require.Contains(t, actorInfo, `json:"filmInfo"`) }) } } From d3f8754cbf486155129f57d37ac942e6dafebe8c Mon Sep 17 00:00:00 2001 From: Carson Krueger Date: Thu, 7 Aug 2025 08:48:53 -0600 Subject: [PATCH 5/6] add CamelToSnake --- cmd/jet/main.go | 2 +- internal/3rdparty/snaker/snaker.go | 50 ++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/cmd/jet/main.go b/cmd/jet/main.go index de12d5f..2b0eaf2 100644 --- a/cmd/jet/main.go +++ b/cmd/jet/main.go @@ -366,7 +366,7 @@ func createModelTags(columnMetaData metadata.Column) []string { var tags []string switch modelJsonTag { case "snake-case": - tags = append(tags, fmt.Sprintf(`json:"%s"`, columnMetaData.Name)) + tags = append(tags, fmt.Sprintf(`json:"%s"`, snaker.CamelToSnake(columnMetaData.Name))) case "camel-case": tags = append(tags, fmt.Sprintf(`json:"%s"`, snaker.SnakeToCamel(columnMetaData.Name, false))) case "pascal-case": diff --git a/internal/3rdparty/snaker/snaker.go b/internal/3rdparty/snaker/snaker.go index 4177e1c..497774f 100644 --- a/internal/3rdparty/snaker/snaker.go +++ b/internal/3rdparty/snaker/snaker.go @@ -8,6 +8,44 @@ import ( "unicode" ) +// CamelToSnake converts a given string to snake case +func CamelToSnake(s string) string { + var result string + var words []string + var lastPos int + rs := []rune(s) + + for i := 0; i < len(rs); i++ { + if i > 0 && unicode.IsUpper(rs[i]) { + if initialism := startsWithInitialism(s[lastPos:]); initialism != "" { + words = append(words, initialism) + + i += len(initialism) - 1 + lastPos = i + continue + } + + words = append(words, s[lastPos:i]) + lastPos = i + } + } + + // append the last word + if s[lastPos:] != "" { + words = append(words, s[lastPos:]) + } + + for k, word := range words { + if k > 0 { + result += "_" + } + + result += strings.ToLower(word) + } + + return result +} + // SnakeToCamel returns a string converted from snake case to uppercase func SnakeToCamel(s string, firstLetterUppercase ...bool) string { upperCase := true @@ -48,6 +86,18 @@ func snakeToCamel(s string, upperCase bool) string { return result } +// startsWithInitialism returns the initialism if the given string begins with it +func startsWithInitialism(s string) string { + var initialism string + // the longest initialism is 5 char, the shortest 2 + for i := 1; i <= 5; i++ { + if len(s) > i-1 && commonInitialisms[s[:i]] { + initialism = s[:i] + } + } + return initialism +} + func toLowerFirstLetter(s string) string { if s == "" { return s From 94ce6424ceff8e571cd61bdb2460081b602818b4 Mon Sep 17 00:00:00 2001 From: Carson Krueger Date: Thu, 7 Aug 2025 22:53:41 -0600 Subject: [PATCH 6/6] add test --- internal/3rdparty/snaker/snaker_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/internal/3rdparty/snaker/snaker_test.go b/internal/3rdparty/snaker/snaker_test.go index e05ca23..c208269 100644 --- a/internal/3rdparty/snaker/snaker_test.go +++ b/internal/3rdparty/snaker/snaker_test.go @@ -17,3 +17,13 @@ func TestSnakeToCamel(t *testing.T) { require.Equal(t, SnakeToCamel("id"), "ID") require.Equal(t, SnakeToCamel("oauth_client"), "OAuthClient") } + +func TestCamelToSnake(t *testing.T) { + require.Equal(t, "", CamelToSnake("")) + require.Equal(t, "_", CamelToSnake("_")) + require.Equal(t, "snake_case", CamelToSnake("snake_case")) + require.Equal(t, "camel_case", CamelToSnake("camelCase")) + require.Equal(t, "jet_is_cool_as_hell", CamelToSnake("jetIsCoolAsHell")) + require.Equal(t, "jet_is_cool_as_hell", CamelToSnake("jet_is_cool_as_hell")) + require.Equal(t, "id", CamelToSnake("ID")) +}