Skip to content

Commit afc4cee

Browse files
authored
Merge pull request #516 from carsonkrueger/feat/json
add model json tag to args
2 parents d375d06 + 94ce642 commit afc4cee

File tree

4 files changed

+279
-7
lines changed

4 files changed

+279
-7
lines changed

cmd/jet/main.go

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"flag"
77
"fmt"
88
"os"
9+
"slices"
910
"strings"
1011

1112
_ "github.com/go-sql-driver/mysql"
@@ -17,6 +18,7 @@ import (
1718
postgresgen "github.com/go-jet/jet/v2/generator/postgres"
1819
sqlitegen "github.com/go-jet/jet/v2/generator/sqlite"
1920
"github.com/go-jet/jet/v2/generator/template"
21+
"github.com/go-jet/jet/v2/internal/3rdparty/snaker"
2022
"github.com/go-jet/jet/v2/internal/jet"
2123
"github.com/go-jet/jet/v2/internal/utils/errfmt"
2224
"github.com/go-jet/jet/v2/internal/utils/strslice"
@@ -54,10 +56,12 @@ var (
5456
tables string
5557
views string
5658
enums string
59+
60+
modelJsonTag string
5761
)
5862

5963
type templateFilter struct {
60-
names []string
64+
names []string
6165
ignore bool
6266
}
6367

@@ -93,6 +97,7 @@ func init() {
9397
flag.StringVar(&tablePkg, "rel-table-path", "table", "Relative path for the Table files package from the destination directory.")
9498
flag.StringVar(&viewPkg, "rel-view-path", "view", "Relative path for the View files package from the destination directory.")
9599
flag.StringVar(&enumPkg, "rel-enum-path", "enum", "Relative path for the Enum files package from the destination directory.")
100+
flag.StringVar(&modelJsonTag, "model-json-tag", "", "Json tag model to be included in Go structs. (optional)(default <empty>)(allowed values: <empty>, pascal-case, camel-case, snake-case")
96101

97102
flag.StringVar(&tables, "tables", "", `Comma-separated list of tables to generate.`)
98103
flag.StringVar(&views, "views", "", `Comma-separated list of views to generate.`)
@@ -107,6 +112,10 @@ func main() {
107112
printErrorAndExit("ERROR: required flag(s) missing")
108113
}
109114

115+
if !slices.Contains([]string{"", "snake-case", "pascal-case", "camel-case"}, modelJsonTag) {
116+
printErrorAndExit("ERROR: json tag does not contain correct value")
117+
}
118+
110119
source := getSource()
111120
tablesFilter := createTemplateFilter(ignoreTables, tables, "tables")
112121
viewsFilter := createTemplateFilter(ignoreViews, views, "views")
@@ -259,8 +268,6 @@ func parseList(list string) []string {
259268
return ret
260269
}
261270

262-
263-
264271
func genTemplate(dialect jet.Dialect, tablesFilter, viewsFilter, enumsFilter templateFilter) template.Template {
265272
return template.Default(dialect).
266273
UseSchema(func(schemaMetaData metadata.Schema) template.Schema {
@@ -270,13 +277,23 @@ func genTemplate(dialect jet.Dialect, tablesFilter, viewsFilter, enumsFilter tem
270277
if shouldSkipTable(table, tablesFilter) {
271278
return template.TableModel{Skip: true}
272279
}
273-
return template.DefaultTableModel(table)
280+
return template.DefaultTableModel(table).
281+
UseField(func(columnMetaData metadata.Column) template.TableModelField {
282+
defaultTableModelField := template.DefaultTableModelField(columnMetaData)
283+
tags := createModelTags(columnMetaData)
284+
return defaultTableModelField.UseTags(tags...)
285+
})
274286
}).
275287
UseView(func(view metadata.Table) template.ViewModel {
276288
if shouldSkipTable(view, viewsFilter) {
277289
return template.ViewModel{Skip: true}
278290
}
279-
return template.DefaultViewModel(view)
291+
return template.DefaultViewModel(view).
292+
UseField(func(columnMetaData metadata.Column) template.TableModelField {
293+
defaultTableModelField := template.DefaultTableModelField(columnMetaData)
294+
tags := createModelTags(columnMetaData)
295+
return defaultTableModelField.UseTags(tags...)
296+
})
280297
}).
281298
UseEnum(func(enum metadata.Enum) template.EnumModel {
282299
if shouldSkipEnum(enum, enumsFilter) {
@@ -318,13 +335,13 @@ func createTemplateFilter(ignoreList, allowList, filterType string) templateFilt
318335

319336
if allowList != "" {
320337
return templateFilter{
321-
names: parseList(allowList),
338+
names: parseList(allowList),
322339
ignore: false,
323340
}
324341
}
325342

326343
return templateFilter{
327-
names: parseList(ignoreList),
344+
names: parseList(ignoreList),
328345
ignore: true,
329346
}
330347
}
@@ -344,3 +361,16 @@ func shouldSkipEnum(enum metadata.Enum, filter templateFilter) bool {
344361

345362
return !strslice.Contains(filter.names, strings.ToLower(enum.Name))
346363
}
364+
365+
func createModelTags(columnMetaData metadata.Column) []string {
366+
var tags []string
367+
switch modelJsonTag {
368+
case "snake-case":
369+
tags = append(tags, fmt.Sprintf(`json:"%s"`, snaker.CamelToSnake(columnMetaData.Name)))
370+
case "camel-case":
371+
tags = append(tags, fmt.Sprintf(`json:"%s"`, snaker.SnakeToCamel(columnMetaData.Name, false)))
372+
case "pascal-case":
373+
tags = append(tags, fmt.Sprintf(`json:"%s"`, snaker.SnakeToCamel(columnMetaData.Name, true)))
374+
}
375+
return tags
376+
}

internal/3rdparty/snaker/snaker.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,44 @@ import (
88
"unicode"
99
)
1010

11+
// CamelToSnake converts a given string to snake case
12+
func CamelToSnake(s string) string {
13+
var result string
14+
var words []string
15+
var lastPos int
16+
rs := []rune(s)
17+
18+
for i := 0; i < len(rs); i++ {
19+
if i > 0 && unicode.IsUpper(rs[i]) {
20+
if initialism := startsWithInitialism(s[lastPos:]); initialism != "" {
21+
words = append(words, initialism)
22+
23+
i += len(initialism) - 1
24+
lastPos = i
25+
continue
26+
}
27+
28+
words = append(words, s[lastPos:i])
29+
lastPos = i
30+
}
31+
}
32+
33+
// append the last word
34+
if s[lastPos:] != "" {
35+
words = append(words, s[lastPos:])
36+
}
37+
38+
for k, word := range words {
39+
if k > 0 {
40+
result += "_"
41+
}
42+
43+
result += strings.ToLower(word)
44+
}
45+
46+
return result
47+
}
48+
1149
// SnakeToCamel returns a string converted from snake case to uppercase
1250
func SnakeToCamel(s string, firstLetterUppercase ...bool) string {
1351
upperCase := true
@@ -48,6 +86,18 @@ func snakeToCamel(s string, upperCase bool) string {
4886
return result
4987
}
5088

89+
// startsWithInitialism returns the initialism if the given string begins with it
90+
func startsWithInitialism(s string) string {
91+
var initialism string
92+
// the longest initialism is 5 char, the shortest 2
93+
for i := 1; i <= 5; i++ {
94+
if len(s) > i-1 && commonInitialisms[s[:i]] {
95+
initialism = s[:i]
96+
}
97+
}
98+
return initialism
99+
}
100+
51101
func toLowerFirstLetter(s string) string {
52102
if s == "" {
53103
return s

internal/3rdparty/snaker/snaker_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,13 @@ func TestSnakeToCamel(t *testing.T) {
1717
require.Equal(t, SnakeToCamel("id"), "ID")
1818
require.Equal(t, SnakeToCamel("oauth_client"), "OAuthClient")
1919
}
20+
21+
func TestCamelToSnake(t *testing.T) {
22+
require.Equal(t, "", CamelToSnake(""))
23+
require.Equal(t, "_", CamelToSnake("_"))
24+
require.Equal(t, "snake_case", CamelToSnake("snake_case"))
25+
require.Equal(t, "camel_case", CamelToSnake("camelCase"))
26+
require.Equal(t, "jet_is_cool_as_hell", CamelToSnake("jetIsCoolAsHell"))
27+
require.Equal(t, "jet_is_cool_as_hell", CamelToSnake("jet_is_cool_as_hell"))
28+
require.Equal(t, "id", CamelToSnake("ID"))
29+
}

tests/postgres/generator_test.go

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/go-jet/jet/v2/tests/.gentestdata/jetdb/dvds/model"
2222
"github.com/go-jet/jet/v2/tests/dbconfig"
2323
"github.com/go-jet/jet/v2/tests/internal/utils/file"
24+
file2 "github.com/go-jet/jet/v2/tests/internal/utils/file"
2425
)
2526

2627
func dsn(host string, port int, dbName, user, password string) string {
@@ -1505,3 +1506,184 @@ func TestAllowAndIgnoreEnums(t *testing.T) {
15051506
})
15061507
}
15071508
}
1509+
1510+
func TestJsonInvalidModelTags(t *testing.T) {
1511+
tests := []struct {
1512+
name string
1513+
args []string
1514+
}{
1515+
{
1516+
name: "with invalid json tag",
1517+
args: []string{
1518+
"-dsn=" + defaultDSN(),
1519+
"-schema=dvds",
1520+
"-tables=actor,ADDRESS,country, Film , cITY,",
1521+
"-views=Actor_info, FILM_LIST ,staff_list",
1522+
"-enums=mpaa_rating",
1523+
"-path=" + genTestDir2,
1524+
"-model-json-tag=invalid",
1525+
},
1526+
},
1527+
{
1528+
name: "with invalid json tag",
1529+
args: []string{
1530+
"-dsn=" + defaultDSN(),
1531+
"-schema=dvds",
1532+
"-tables=actor,ADDRESS,country, Film , cITY,",
1533+
"-views=Actor_info, FILM_LIST ,staff_list",
1534+
"-enums=mpaa_rating",
1535+
"-path=" + genTestDir2,
1536+
"-model-json-tag= invalid",
1537+
},
1538+
},
1539+
}
1540+
1541+
for _, tt := range tests {
1542+
t.Run(tt.name, func(t *testing.T) {
1543+
cmd := exec.Command("jet", tt.args...)
1544+
1545+
var stdOut bytes.Buffer
1546+
cmd.Stderr = os.Stderr
1547+
cmd.Stdout = &stdOut
1548+
1549+
err := cmd.Run()
1550+
require.Error(t, err)
1551+
require.Equal(t, "exit status 1", err.Error())
1552+
1553+
stdOutput := stdOut.String()
1554+
require.Contains(t, stdOutput, "ERROR: json tag does not contain correct value")
1555+
})
1556+
}
1557+
}
1558+
1559+
func TestSnakeCaseModelJsonTag(t *testing.T) {
1560+
tests := []struct {
1561+
name string
1562+
args []string
1563+
}{
1564+
{
1565+
name: "with snake-case",
1566+
args: []string{
1567+
"-dsn=" + defaultDSN(),
1568+
"-schema=dvds",
1569+
"-views=Actor_info",
1570+
"-tables=actor",
1571+
"-path=" + genTestDir2,
1572+
"-model-json-tag=snake-case",
1573+
},
1574+
},
1575+
}
1576+
1577+
for _, tt := range tests {
1578+
t.Run(tt.name, func(t *testing.T) {
1579+
cmd := exec.Command("jet", tt.args...)
1580+
1581+
var stdOut bytes.Buffer
1582+
cmd.Stderr = os.Stderr
1583+
cmd.Stdout = &stdOut
1584+
1585+
err := cmd.Run()
1586+
require.Nil(t, err)
1587+
1588+
actor := file2.Exists(t, genTestDir2+"/jetdb/dvds/model/actor.go")
1589+
require.Contains(t, actor, `json:"actor_id"`)
1590+
require.Contains(t, actor, `json:"first_name"`)
1591+
require.Contains(t, actor, `json:"last_name"`)
1592+
require.Contains(t, actor, `json:"last_update"`)
1593+
1594+
actorInfo := file2.Exists(t, genTestDir2+"/jetdb/dvds/model/actor_info.go")
1595+
require.Contains(t, actorInfo, `json:"actor_id"`)
1596+
require.Contains(t, actorInfo, `json:"first_name"`)
1597+
require.Contains(t, actorInfo, `json:"last_name"`)
1598+
require.Contains(t, actorInfo, `json:"film_info"`)
1599+
})
1600+
}
1601+
}
1602+
1603+
func TestPascalCaseModelJsonTag(t *testing.T) {
1604+
tests := []struct {
1605+
name string
1606+
args []string
1607+
}{
1608+
{
1609+
name: "with pascal-case",
1610+
args: []string{
1611+
"-dsn=" + defaultDSN(),
1612+
"-schema=dvds",
1613+
"-views=Actor_info",
1614+
"-tables=actor",
1615+
"-path=" + genTestDir2,
1616+
"-model-json-tag=pascal-case",
1617+
},
1618+
},
1619+
}
1620+
1621+
for _, tt := range tests {
1622+
t.Run(tt.name, func(t *testing.T) {
1623+
cmd := exec.Command("jet", tt.args...)
1624+
1625+
var stdOut bytes.Buffer
1626+
cmd.Stderr = os.Stderr
1627+
cmd.Stdout = &stdOut
1628+
1629+
err := cmd.Run()
1630+
require.Nil(t, err)
1631+
1632+
actor := file2.Exists(t, genTestDir2+"/jetdb/dvds/model/actor.go")
1633+
require.Contains(t, actor, `json:"ActorID"`)
1634+
require.Contains(t, actor, `json:"FirstName"`)
1635+
require.Contains(t, actor, `json:"LastName"`)
1636+
require.Contains(t, actor, `json:"LastUpdate"`)
1637+
1638+
actorInfo := file2.Exists(t, genTestDir2+"/jetdb/dvds/model/actor_info.go")
1639+
require.Contains(t, actorInfo, `json:"ActorID"`)
1640+
require.Contains(t, actorInfo, `json:"FirstName"`)
1641+
require.Contains(t, actorInfo, `json:"LastName"`)
1642+
require.Contains(t, actorInfo, `json:"FilmInfo"`)
1643+
})
1644+
}
1645+
}
1646+
1647+
func TestCamelCaseModelJsonTag(t *testing.T) {
1648+
tests := []struct {
1649+
name string
1650+
args []string
1651+
}{
1652+
{
1653+
name: "with camel-case",
1654+
args: []string{
1655+
"-dsn=" + defaultDSN(),
1656+
"-schema=dvds",
1657+
"-views=Actor_info",
1658+
"-tables=actor",
1659+
"-path=" + genTestDir2,
1660+
"-model-json-tag=camel-case",
1661+
},
1662+
},
1663+
}
1664+
1665+
for _, tt := range tests {
1666+
t.Run(tt.name, func(t *testing.T) {
1667+
cmd := exec.Command("jet", tt.args...)
1668+
1669+
var stdOut bytes.Buffer
1670+
cmd.Stderr = os.Stderr
1671+
cmd.Stdout = &stdOut
1672+
1673+
err := cmd.Run()
1674+
require.Nil(t, err)
1675+
1676+
actor := file2.Exists(t, genTestDir2+"/jetdb/dvds/model/actor.go")
1677+
require.Contains(t, actor, `json:"actorID"`)
1678+
require.Contains(t, actor, `json:"firstName"`)
1679+
require.Contains(t, actor, `json:"lastName"`)
1680+
require.Contains(t, actor, `json:"lastUpdate"`)
1681+
1682+
actorInfo := file2.Exists(t, genTestDir2+"/jetdb/dvds/model/actor_info.go")
1683+
require.Contains(t, actorInfo, `json:"actorID"`)
1684+
require.Contains(t, actorInfo, `json:"firstName"`)
1685+
require.Contains(t, actorInfo, `json:"lastName"`)
1686+
require.Contains(t, actorInfo, `json:"filmInfo"`)
1687+
})
1688+
}
1689+
}

0 commit comments

Comments
 (0)