gocctl model v20200819 (#18)

* rename snake、came method

* new: generate model from data source

* add change log md

* update model doc

* update  doc

* beauty code
master
Keson 4 years ago committed by GitHub
parent 50565c9765
commit db83843558
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -195,6 +195,13 @@ ts需要指定webapi所在目录
goctl api dart -api user/user.api -dir ./src
```
## 根据mysql ddl或者datasource生成model文件
```shell script
$ goctl model mysql -src={filename} -dir={dir} -cache={true|false}
```
详情参考[model文档](https://github.com/tal-tech/go-zero/blob/master/tools/goctl/model/sql/README.MD)
## 根据定义好的简单go文件生成mongo代码文件(仅限golang使用)
```shell
goctl model mongo -src {{yourDir}}/xiao/service/xhb/user/model/usermodel.go -cache yes
@ -218,7 +225,7 @@ type User struct {
o是改字段需要生产的操作函数 可以取得get,find,set 分别表示生成返回单个对象的查询方法,返回多个对象的查询方法,设置该字段方法
生成的目标文件会覆盖该简单go文件
## goctl rpc生成
## goctl rpc生成(业务剥离中,暂未开放)
命令 `goctl rpc proto -proto ${proto} -service ${serviceName} -project ${projectName} -dir ${directory} -shared ${shared}`
如: `goctl rpc proto -proto test.proto -service test -project xjy -dir .`

@ -4,6 +4,8 @@ import (
"fmt"
"os"
"github.com/urfave/cli"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/tools/goctl/api/apigen"
"github.com/tal-tech/go-zero/tools/goctl/api/dartgen"
@ -18,7 +20,6 @@ import (
"github.com/tal-tech/go-zero/tools/goctl/docker"
"github.com/tal-tech/go-zero/tools/goctl/feature"
"github.com/tal-tech/go-zero/tools/goctl/model/sql/command"
"github.com/urfave/cli"
)
var (
@ -191,6 +192,14 @@ var (
{
Name: "model",
Usage: "generate model code",
Subcommands: []cli.Command{
{
Name: "mysql",
Usage: `generate mysql model"`,
Subcommands: []cli.Command{
{
Name: "ddl",
Usage: `generate mysql model from ddl"`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "src, s",
@ -209,7 +218,38 @@ var (
Usage: "for idea plugin [optional]",
},
},
Action: command.Mysql,
Action: command.MysqlDDL,
},
{
Name: "datasource",
Usage: `generate model from datasource"`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "url",
Usage: `the data source of database,like "root:password@tcp(127.0.0.1:3306)/database"`,
},
cli.StringFlag{
Name: "table, t",
Usage: `source table,tables separated by commas,like "user,course"`,
},
cli.BoolFlag{
Name: "cache, c",
Usage: "generate code with cache [optional]",
},
cli.StringFlag{
Name: "dir, d",
Usage: "the target dir",
},
cli.BoolFlag{
Name: "idea",
Usage: "for idea plugin [optional]",
},
},
Action: command.MyDataSource,
},
},
},
},
},
{
Name: "config",

@ -0,0 +1,10 @@
# Change log
# 2020-08-20
* 新增支持通过连接数据库生成model
* 支持数据库多表生成
* 优化stringx
# 2020-08-19
* 重构model代码生成逻辑
* 实现从ddl解析表信息生成代码

@ -4,11 +4,11 @@ goctl model 为go-zero下的工具模块中的组件之一目前支持识别m
# 快速开始
```
$ goctl model -src ./sql/user.sql -dir ./model -c true
```
* 通过ddl生成
详情用法请参考[example](https://github.com/tal-tech/go-zero/tools/goctl/model/sql/example)
```shell script
$ goctl model mysql ddl -src="./sql/user.sql" -dir="./sql/model" -c=true
```
执行上述命令后即可快速生成CURD代码。
@ -17,8 +17,15 @@ model
│   ├── error.go
│   └── usermodel.go
```
* 通过datasource生成
```shell script
$ goctl model mysql datasource -url="user:password@tcp(127.0.0.1:3306)/database" -table="table1,table2" -dir="./model"
```
> 详情用法请参考[example](https://github.com/tal-tech/go-zero/tree/master/tools/goctl/model/sql/example)
> 注意这里的目录结构中有usercoursemodel.go目录在example中我为了体现带cache与不带cache代码的区别因此将sql文件分别使用了独立的sql文件(user.sql&course.sql)在实际项目开发中你可以将ddl建表语句放在一个sql文件中`goctl model`会自动解析并分割最终按照每个ddl建表语句为单位生成独立的go文件。
* 生成代码示例
@ -174,22 +181,22 @@ model
# 用法
```
$ goctl model -h
$ goctl model mysql -h
```
```
NAME:
goctl model - generate model code
goctl model mysql - generate mysql model"
USAGE:
goctl model [command options] [arguments...]
goctl model mysql command [command options] [arguments...]
OPTIONS:
--src value, -s value the file path of the ddl source file
--dir value, -d value the target dir
--cache, -c generate code with cache [optional]
--idea for idea plugin [optional]
COMMANDS:
ddl generate mysql model from ddl"
datasource generate model from datasource"
OPTIONS:
--help, -h show help
```
# 生成规则
@ -198,22 +205,43 @@ OPTIONS:
我们默认用户在建表时会创建createTime、updateTime字段(忽略大小写、下划线命名风格)且默认值均为`CURRENT_TIMESTAMP`而updateTime支持`ON UPDATE CURRENT_TIMESTAMP`,对于这两个字段生成`insert`、`update`时会被移除,不在赋值范畴内,当然,如果你不需要这两个字段那也无大碍。
* 带缓存模式
* ddl
```shell script
$ goctl model mysql -src={filename} -dir={dir} -cache=true
```
$ goctl model -src {filename} -dir {dir} -cache true
* datasource
```shell script
$ goctl model mysql datasource -url={datasource} -table={tables} -dir={dir} -cache=true
```
目前仅支持redis缓存如果选择带缓存模式即生成的`FindOne(ByXxx)`&`Delete`代码会生成带缓存逻辑的代码目前仅支持单索引字段除全文索引外对于联合索引我们默认认为不需要带缓存且不属于通用型代码因此没有放在代码生成行列如example中user表中的`id`、`name`、`mobile`字段均属于单字段索引。
* 不带缓存模式
* ddl
```shell script
$ goctl model -src={filename} -dir={dir}
```
$ goctl model -src {filename} -dir {dir}
* datasource
```shell script
$ goctl model mysql datasource -url={datasource} -table={tables} -dir={dir}
```
or
* ddl
```shell script
$ goctl model -src={filename} -dir={dir} -cache=false
```
$ goctl model -src {filename} -dir {dir} -cache false
* datasource
```shell script
$ goctl model mysql datasource -url={datasource} -table={tables} -dir={dir} -cache=false
```
生成代码仅基本的CURD结构。
# 缓存
@ -238,10 +266,6 @@ OPTIONS:
# QA
* goctl model支持根据数据库连接后选择表生成代码吗
目前暂时不支持,在后面会向这个方向扩展。
* goctl model除了命令行模式支持插件模式吗
很快支持idea插件。

@ -1,24 +1,85 @@
package command
import (
"io/ioutil"
"path/filepath"
"strings"
"github.com/urfave/cli"
"github.com/tal-tech/go-zero/core/collection"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/core/stores/sqlx"
"github.com/tal-tech/go-zero/tools/goctl/model/sql/gen"
"github.com/tal-tech/go-zero/tools/goctl/model/sql/model"
"github.com/tal-tech/go-zero/tools/goctl/util/console"
"github.com/urfave/cli"
)
func Mysql(ctx *cli.Context) error {
src := ctx.String("src")
dir := ctx.String("dir")
cache := ctx.Bool("cache")
idea := ctx.Bool("idea")
var log console.Console
if idea {
log = console.NewIdeaConsole()
} else {
log = console.NewColorConsole()
}
generator := gen.NewDefaultGenerator(src, dir, gen.WithConsoleOption(log))
err := generator.Start(cache)
const (
flagSrc = "src"
flagDir = "dir"
flagCache = "cache"
flagIdea = "idea"
flagUrl = "url"
flagTable = "table"
)
func MysqlDDL(ctx *cli.Context) error {
src := ctx.String(flagSrc)
dir := ctx.String(flagDir)
cache := ctx.Bool(flagCache)
idea := ctx.Bool(flagIdea)
log := console.NewConsole(idea)
fileSrc, err := filepath.Abs(src)
if err != nil {
return err
}
data, err := ioutil.ReadFile(fileSrc)
if err != nil {
return err
}
source := string(data)
generator := gen.NewDefaultGenerator(source, dir, gen.WithConsoleOption(log))
err = generator.Start(cache)
if err != nil {
log.Error("%v", err)
}
return nil
}
func MyDataSource(ctx *cli.Context) error {
url := strings.TrimSpace(ctx.String(flagUrl))
dir := strings.TrimSpace(ctx.String(flagDir))
cache := ctx.Bool(flagCache)
idea := ctx.Bool(flagIdea)
table := strings.TrimSpace(ctx.String(flagTable))
log := console.NewConsole(idea)
if len(url) == 0 {
log.Error("%v", "expected data source of mysql, but is empty")
return nil
}
if len(table) == 0 {
log.Error("%v", "expected table(s), but nothing found")
return nil
}
logx.Disable()
conn := sqlx.NewMysql(url)
m := model.NewDDLModel(conn)
tables := collection.NewSet()
for _, item := range strings.Split(table, ",") {
item = strings.TrimSpace(item)
if len(item) == 0 {
continue
}
tables.AddStr(item)
}
ddl, err := m.ShowDDL(tables.KeysStr()...)
if err != nil {
log.Error("%v", err)
return nil
}
generator := gen.NewDefaultGenerator(strings.Join(ddl, "\n"), dir, gen.WithConsoleOption(log))
err = generator.Start(cache)
if err != nil {
log.Error("%v", err)
}

@ -1,4 +1,7 @@
#!/bin/bash
# generate usermodel with cache
goctl model -src ./sql/user.sql -dir ./model -c true
# generate model with cache from ddl
goctl model mysql ddl -src="./sql/user.sql" -dir="./sql/model" -c=true
# generate model with cache from data source
goctl model mysql datasource -url="user:password@tcp(127.0.0.1:3306)/database" -table="table1,table2" -dir="./model"

@ -27,14 +27,14 @@ func genDelete(table Table, withCache bool) (string, error) {
break
}
}
camel := table.Name.Snake2Camel()
camel := table.Name.ToCamel()
output, err := templatex.With("delete").
Parse(template.Delete).
Execute(map[string]interface{}{
"upperStartCamelObject": camel,
"withCache": withCache,
"containsIndexCache": containsIndexCache,
"lowerStartCamelPrimaryKey": stringx.From(table.PrimaryKey.Name.Snake2Camel()).LowerStart(),
"lowerStartCamelPrimaryKey": stringx.From(table.PrimaryKey.Name.ToCamel()).UnTitle(),
"dataType": table.PrimaryKey.DataType,
"keys": strings.Join(keySet.KeysStr(), "\n"),
"originalPrimaryKey": table.PrimaryKey.Name.Source(),

@ -28,7 +28,7 @@ func genField(field parser.Field) (string, error) {
output, err := templatex.With("types").
Parse(template.Field).
Execute(map[string]interface{}{
"name": field.Name.Snake2Camel(),
"name": field.Name.ToCamel(),
"type": field.DataType,
"tag": tag,
"hasComment": field.Comment != "",

@ -7,15 +7,15 @@ import (
)
func genFindOne(table Table, withCache bool) (string, error) {
camel := table.Name.Snake2Camel()
camel := table.Name.ToCamel()
output, err := templatex.With("findOne").
Parse(template.FindOne).
Execute(map[string]interface{}{
"withCache": withCache,
"upperStartCamelObject": camel,
"lowerStartCamelObject": stringx.From(camel).LowerStart(),
"lowerStartCamelObject": stringx.From(camel).UnTitle(),
"originalPrimaryKey": table.PrimaryKey.Name.Source(),
"lowerStartCamelPrimaryKey": stringx.From(table.PrimaryKey.Name.Snake2Camel()).LowerStart(),
"lowerStartCamelPrimaryKey": stringx.From(table.PrimaryKey.Name.ToCamel()).UnTitle(),
"dataType": table.PrimaryKey.DataType,
"cacheKey": table.CacheKey[table.PrimaryKey.Name.Source()].KeyExpression,
"cacheKeyVariable": table.CacheKey[table.PrimaryKey.Name.Source()].Variable,

@ -12,23 +12,23 @@ import (
func genFineOneByField(table Table, withCache bool) (string, error) {
t := templatex.With("findOneByField").Parse(template.FindOneByField)
var list []string
camelTableName := table.Name.Snake2Camel()
camelTableName := table.Name.ToCamel()
for _, field := range table.Fields {
if field.IsPrimaryKey || !field.IsKey {
continue
}
camelFieldName := field.Name.Snake2Camel()
camelFieldName := field.Name.ToCamel()
output, err := t.Execute(map[string]interface{}{
"upperStartCamelObject": camelTableName,
"upperField": camelFieldName,
"in": fmt.Sprintf("%s %s", stringx.From(camelFieldName).LowerStart(), field.DataType),
"in": fmt.Sprintf("%s %s", stringx.From(camelFieldName).UnTitle(), field.DataType),
"withCache": withCache,
"cacheKey": table.CacheKey[field.Name.Source()].KeyExpression,
"cacheKeyVariable": table.CacheKey[field.Name.Source()].Variable,
"primaryKeyLeft": table.CacheKey[table.PrimaryKey.Name.Source()].Left,
"lowerStartCamelObject": stringx.From(camelTableName).LowerStart(),
"lowerStartCamelField": stringx.From(camelFieldName).LowerStart(),
"upperStartCamelPrimaryKey": table.PrimaryKey.Name.Snake2Camel(),
"lowerStartCamelObject": stringx.From(camelTableName).UnTitle(),
"lowerStartCamelField": stringx.From(camelFieldName).UnTitle(),
"upperStartCamelPrimaryKey": table.PrimaryKey.Name.ToCamel(),
"originalField": field.Name.Source(),
"originalPrimaryField": table.PrimaryKey.Name.Source(),
})

@ -23,21 +23,17 @@ const (
type (
defaultGenerator struct {
source string
src string
dir string
console.Console
}
Option func(generator *defaultGenerator)
)
func NewDefaultGenerator(src, dir string, opt ...Option) *defaultGenerator {
if src == "" {
src = pwd
}
func NewDefaultGenerator(source, dir string, opt ...Option) *defaultGenerator {
if dir == "" {
dir = pwd
}
generator := &defaultGenerator{src: src, dir: dir}
generator := &defaultGenerator{source: source, dir: dir}
var optionList []Option
optionList = append(optionList, newDefaultOption())
optionList = append(optionList, opt...)
@ -60,10 +56,6 @@ func newDefaultOption() Option {
}
func (g *defaultGenerator) Start(withCache bool) error {
fileSrc, err := filepath.Abs(g.src)
if err != nil {
return err
}
dirAbs, err := filepath.Abs(g.dir)
if err != nil {
return err
@ -72,18 +64,13 @@ func (g *defaultGenerator) Start(withCache bool) error {
if err != nil {
return err
}
data, err := ioutil.ReadFile(fileSrc)
if err != nil {
return err
}
g.source = string(data)
modelList, err := g.genFromDDL(withCache)
if err != nil {
return err
}
for tableName, code := range modelList {
name := fmt.Sprintf("%smodel.go", strings.ToLower(stringx.From(tableName).Snake2Camel()))
name := fmt.Sprintf("%smodel.go", strings.ToLower(stringx.From(tableName).ToCamel()))
filename := filepath.Join(dirAbs, name)
if util.FileExists(filename) {
g.Warning("%s already exists, ignored.", name)

@ -12,7 +12,7 @@ func genInsert(table Table, withCache bool) (string, error) {
expressions := make([]string, 0)
expressionValues := make([]string, 0)
for _, filed := range table.Fields {
camel := filed.Name.Snake2Camel()
camel := filed.Name.ToCamel()
if camel == "CreateTime" || camel == "UpdateTime" {
continue
}
@ -22,13 +22,13 @@ func genInsert(table Table, withCache bool) (string, error) {
expressions = append(expressions, "?")
expressionValues = append(expressionValues, "data."+camel)
}
camel := table.Name.Snake2Camel()
camel := table.Name.ToCamel()
output, err := templatex.With("insert").
Parse(template.Insert).
Execute(map[string]interface{}{
"withCache": withCache,
"upperStartCamelObject": camel,
"lowerStartCamelObject": stringx.From(camel).LowerStart(),
"lowerStartCamelObject": stringx.From(camel).UnTitle(),
"expression": strings.Join(expressions, ", "),
"expressionValues": strings.Join(expressionValues, ", "),
})

@ -26,14 +26,14 @@ type (
func genCacheKeys(table parser.Table) (map[string]Key, error) {
fields := table.Fields
m := make(map[string]Key)
camelTableName := table.Name.Snake2Camel()
lowerStartCamelTableName := stringx.From(camelTableName).LowerStart()
camelTableName := table.Name.ToCamel()
lowerStartCamelTableName := stringx.From(camelTableName).UnTitle()
for _, field := range fields {
if !field.IsKey {
continue
}
camelFieldName := field.Name.Snake2Camel()
lowerStartCamelFieldName := stringx.From(camelFieldName).LowerStart()
camelFieldName := field.Name.ToCamel()
lowerStartCamelFieldName := stringx.From(camelFieldName).UnTitle()
left := fmt.Sprintf("cache%s%sPrefix", camelTableName, camelFieldName)
right := fmt.Sprintf("cache#%s#%s#", camelTableName, lowerStartCamelFieldName)
variable := fmt.Sprintf("%s%sKey", lowerStartCamelTableName, camelFieldName)

@ -10,7 +10,7 @@ func genNew(table Table, withCache bool) (string, error) {
Parse(template.New).
Execute(map[string]interface{}{
"withCache": withCache,
"upperStartCamelObject": table.Name.Snake2Camel(),
"upperStartCamelObject": table.Name.ToCamel(),
})
if err != nil {
return "", err

@ -15,7 +15,7 @@ func genTypes(table Table, withCache bool) (string, error) {
Parse(template.Types).
Execute(map[string]interface{}{
"withCache": withCache,
"upperStartCamelObject": table.Name.Snake2Camel(),
"upperStartCamelObject": table.Name.ToCamel(),
"fields": fieldsString,
})
if err != nil {

@ -11,7 +11,7 @@ import (
func genUpdate(table Table, withCache bool) (string, error) {
expressionValues := make([]string, 0)
for _, filed := range table.Fields {
camel := filed.Name.Snake2Camel()
camel := filed.Name.ToCamel()
if camel == "CreateTime" || camel == "UpdateTime" {
continue
}
@ -20,8 +20,8 @@ func genUpdate(table Table, withCache bool) (string, error) {
}
expressionValues = append(expressionValues, "data."+camel)
}
expressionValues = append(expressionValues, "data."+table.PrimaryKey.Name.Snake2Camel())
camelTableName := table.Name.Snake2Camel()
expressionValues = append(expressionValues, "data."+table.PrimaryKey.Name.ToCamel())
camelTableName := table.Name.ToCamel()
output, err := templatex.With("update").
Parse(template.Update).
Execute(map[string]interface{}{
@ -29,7 +29,7 @@ func genUpdate(table Table, withCache bool) (string, error) {
"upperStartCamelObject": camelTableName,
"primaryCacheKey": table.CacheKey[table.PrimaryKey.Name.Source()].DataKeyExpression,
"primaryKeyVariable": table.CacheKey[table.PrimaryKey.Name.Source()].Variable,
"lowerStartCamelObject": stringx.From(camelTableName).LowerStart(),
"lowerStartCamelObject": stringx.From(camelTableName).UnTitle(),
"originalPrimaryKey": table.PrimaryKey.Name.Source(),
"expressionValues": strings.Join(expressionValues, ", "),
})

@ -13,12 +13,12 @@ func genVars(table Table, withCache bool) (string, error) {
for _, v := range table.CacheKey {
keys = append(keys, v.VarExpression)
}
camel := table.Name.Snake2Camel()
camel := table.Name.ToCamel()
output, err := templatex.With("var").
Parse(template.Vars).
GoFmt(true).
Execute(map[string]interface{}{
"lowerStartCamelObject": stringx.From(camel).LowerStart(),
"lowerStartCamelObject": stringx.From(camel).UnTitle(),
"upperStartCamelObject": camel,
"cacheKeys": strings.Join(keys, "\r\n"),
"autoIncrement": table.PrimaryKey.AutoIncrement,

@ -0,0 +1,33 @@
package model
import (
"github.com/tal-tech/go-zero/core/stores/sqlx"
)
type (
DDLModel struct {
conn sqlx.SqlConn
}
DDL struct {
Table string `db:"Table"`
DDL string `db:"Create Table"`
}
)
func NewDDLModel(conn sqlx.SqlConn) *DDLModel {
return &DDLModel{conn: conn}
}
func (m *DDLModel) ShowDDL(table ...string) ([]string, error) {
var ddl []string
for _, t := range table {
query := `show create table ` + t
var resp DDL
err := m.conn.QueryRow(&resp, query)
if err != nil {
return nil, err
}
ddl = append(ddl, resp.DDL)
}
return ddl, nil
}

@ -81,7 +81,7 @@ func Parse(ddl string) (*Table, error) {
}
column := index.Columns[0]
columnName := column.Column.String()
camelColumnName := stringx.From(columnName).Snake2Camel()
camelColumnName := stringx.From(columnName).ToCamel()
// by default, createTime|updateTime findOne is not used.
if camelColumnName == "CreateTime" || camelColumnName == "UpdateTime" {
continue

@ -19,6 +19,13 @@ type (
}
)
func NewConsole(idea bool) Console {
if idea {
return NewIdeaConsole()
}
return NewColorConsole()
}
func NewColorConsole() *colorConsole {
return &colorConsole{}
}

@ -6,10 +6,6 @@ import (
"unicode"
)
const (
emptyString = ""
)
type (
String struct {
source string
@ -31,15 +27,9 @@ func (s String) IsEmptyOrSpace() bool {
}
func (s String) Lower() string {
if s.IsEmptyOrSpace() {
return s.source
}
return strings.ToLower(s.source)
}
func (s String) Upper() string {
if s.IsEmptyOrSpace() {
return s.source
}
return strings.ToUpper(s.source)
}
func (s String) Title() string {
@ -50,10 +40,7 @@ func (s String) Title() string {
}
// snake->camel(upper start)
func (s String) Snake2Camel() string {
if s.IsEmptyOrSpace() {
return s.source
}
func (s String) ToCamel() string {
list := s.splitBy(func(r rune) bool {
return r == '_'
}, true)
@ -65,10 +52,7 @@ func (s String) Snake2Camel() string {
}
// camel->snake
func (s String) Camel2Snake() string {
if s.IsEmptyOrSpace() {
return s.source
}
func (s String) ToSnake() string {
list := s.splitBy(func(r rune) bool {
return unicode.IsUpper(r)
}, false)
@ -80,7 +64,7 @@ func (s String) Camel2Snake() string {
}
// return original string if rune is not letter at index 0
func (s String) LowerStart() string {
func (s String) UnTitle() string {
if s.IsEmptyOrSpace() {
return s.source
}

@ -17,26 +17,26 @@ func TestString_IsEmptyOrSpace(t *testing.T) {
}
func TestString_Snake2Camel(t *testing.T) {
ret := From("____this_is_snake").Snake2Camel()
ret := From("____this_is_snake").ToCamel()
assert.Equal(t, "ThisIsSnake", ret)
ret2 := From("测试_test_Data").Snake2Camel()
ret2 := From("测试_test_Data").ToCamel()
assert.Equal(t, "测试TestData", ret2)
ret3 := From("___").Snake2Camel()
ret3 := From("___").ToCamel()
assert.Equal(t, "", ret3)
ret4 := From("testData_").Snake2Camel()
ret4 := From("testData_").ToCamel()
assert.Equal(t, "TestData", ret4)
ret5 := From("testDataTestData").Snake2Camel()
ret5 := From("testDataTestData").ToCamel()
assert.Equal(t, "TestDataTestData", ret5)
}
func TestString_Camel2Snake(t *testing.T) {
ret := From("ThisIsCCCamel").Camel2Snake()
ret := From("ThisIsCCCamel").ToSnake()
assert.Equal(t, "this_is_c_c_camel", ret)
ret2 := From("测试Test_Data_test_data").Camel2Snake()
ret2 := From("测试Test_Data_test_data").ToSnake()
assert.Equal(t, "测试_test__data_test_data", ret2)
}

Loading…
Cancel
Save