Files
sponge/pkg/sql2code/parser/commonParser.go
2025-08-21 23:16:15 +08:00

421 lines
12 KiB
Go

package parser
import (
"encoding/json"
"fmt"
"strings"
"text/template"
"github.com/jinzhu/inflection"
)
// CrudInfo crud info for cache, dao, handler, service, protobuf, error
type CrudInfo struct {
TableNameCamel string `json:"tableNameCamel"` // camel case, example: FooBar
TableNameCamelFCL string `json:"tableNameCamelFCL"` // camel case and first character lower, example: fooBar
TableNamePluralCamel string `json:"tableNamePluralCamel"` // plural, camel case, example: FooBars
TableNamePluralCamelFCL string `json:"tableNamePluralCamelFCL"` // plural, camel case, example: fooBars
ColumnName string `json:"columnName"` // column name, example: first_name
ColumnNameCamel string `json:"columnNameCamel"` // column name, camel case, example: FirstName
ColumnNameCamelFCL string `json:"columnNameCamelFCL"` // column name, camel case and first character lower, example: firstName
ColumnNamePluralCamel string `json:"columnNamePluralCamel"` // column name, plural, camel case, example: FirstNames
ColumnNamePluralCamelFCL string `json:"columnNamePluralCamelFCL"` // column name, plural, camel case and first character lower, example: firstNames
GoType string `json:"goType"` // go type, example: string, uint64
GoTypeFCU string `json:"goTypeFCU"` // go type, first character upper, example: String, Uint64
ProtoType string `json:"protoType"` // proto type, example: string, uint64
IsStringType bool `json:"isStringType"` // go type is string or not
PrimaryKeyColumnName string `json:"PrimaryKeyColumnName"` // primary key, example: id
IsCommonType bool `json:"isCommonType"` // custom primary key name and type
IsStandardPrimaryKey bool `json:"isStandardPrimaryKey"` // standard primary key id
}
func isDesiredGoType(t string) bool {
switch t {
case "string", "uint64", "int64", "uint", "int", "uint32", "int32": //nolint
return true
}
return false
}
func setCrudInfo(field tmplField) *CrudInfo {
primaryKeyName := ""
if field.IsPrimaryKey {
primaryKeyName = field.ColName
}
pluralName := inflection.Plural(field.Name)
info := &CrudInfo{
ColumnName: field.ColName,
ColumnNameCamel: field.Name,
ColumnNameCamelFCL: customFirstLetterToLower(field.Name),
ColumnNamePluralCamel: customEndOfLetterToLower(field.Name, pluralName),
ColumnNamePluralCamelFCL: customFirstLetterToLower(customEndOfLetterToLower(field.Name, pluralName)),
GoType: field.GoType,
GoTypeFCU: firstLetterToUpper(field.GoType),
ProtoType: simpleGoTypeToProtoType(field.GoType),
IsStringType: field.GoType == "string",
PrimaryKeyColumnName: primaryKeyName,
IsStandardPrimaryKey: field.ColName == "id",
}
if info.ColumnNameCamel == info.ColumnNamePluralCamel {
info.ColumnNamePluralCamel += "s" // if singular and plural are the same, force the suffix 's' to distinguish them
}
if info.ColumnNameCamelFCL == info.ColumnNamePluralCamelFCL {
info.ColumnNamePluralCamelFCL += "s" // if singular and plural are the same, force the suffix 's' to distinguish them
}
return info
}
func newCrudInfo(data tmplData) *CrudInfo {
if len(data.Fields) == 0 {
return nil
}
var info *CrudInfo
for _, field := range data.Fields {
if field.IsPrimaryKey {
info = setCrudInfo(field)
break
}
}
// if not found primary key, find the first xxx_id column as primary key
if info == nil {
for _, field := range data.Fields {
if strings.HasSuffix(field.ColName, "_id") && isDesiredGoType(field.GoType) { // xxx_id
info = setCrudInfo(field)
break
}
}
}
// if not found xxx_id field, use the first field of integer or string type
if info == nil {
for _, field := range data.Fields {
if isDesiredGoType(field.GoType) {
info = setCrudInfo(field)
break
}
}
}
// use the first column as primary key
if info == nil {
info = setCrudInfo(data.Fields[0])
}
info.TableNameCamel = data.TableName
info.TableNameCamelFCL = data.TName
pluralName := inflection.Plural(data.TableName)
info.TableNamePluralCamel = customEndOfLetterToLower(data.TableName, pluralName)
info.TableNamePluralCamelFCL = customFirstLetterToLower(customEndOfLetterToLower(data.TableName, pluralName))
return info
}
func (info *CrudInfo) getCode() string {
if info == nil {
return ""
}
pkData, _ := json.Marshal(info)
return string(pkData)
}
func (info *CrudInfo) CheckCommonType() bool {
if info == nil {
return false
}
return info.IsCommonType
}
func (info *CrudInfo) isIDPrimaryKey() bool {
if info == nil {
return false
}
if info.ColumnName == "id" && (info.GoType == "uint64" ||
info.GoType == "int64" ||
info.GoType == "uint" ||
info.GoType == "int" ||
info.GoType == "uint32" ||
info.GoType == "int32") {
return true
}
return false
}
func (info *CrudInfo) GetGRPCProtoValidation() string {
if info == nil {
return ""
}
if info.ProtoType == "string" {
return `[(validate.rules).string.min_len = 1]`
}
return fmt.Sprintf(`[(validate.rules).%s.gt = 0]`, info.ProtoType)
}
func (info *CrudInfo) GetWebProtoValidation() string {
if info == nil {
return ""
}
if info.ProtoType == "string" {
return fmt.Sprintf(`[(validate.rules).string.min_len = 1, (tagger.tags) = "uri:\"%s\""]`, info.ColumnNameCamelFCL)
}
return fmt.Sprintf(`[(validate.rules).%s.gt = 0, (tagger.tags) = "uri:\"%s\""]`, info.ProtoType, info.ColumnNameCamelFCL)
}
func getCommonHandlerStructCodes(data tmplData, jsonNamedType int) (string, error) {
newFields := []tmplField{}
for _, field := range data.Fields {
if jsonNamedType == 0 { // snake case
field.JSONName = customToSnake(field.ColName)
} else {
field.JSONName = customToCamel(field.ColName) // camel case (default)
}
field.GoType = getHandlerGoType(&field)
newFields = append(newFields, field)
}
data.Fields = newFields
postStructCode, err := tmplExecuteWithFilter(data, handlerCreateStructCommonTmpl)
if err != nil {
return "", fmt.Errorf("handlerCreateStructTmpl error: %v", err)
}
putStructCode, err := tmplExecuteWithFilter(data, handlerUpdateStructCommonTmpl, columnID)
if err != nil {
return "", fmt.Errorf("handlerUpdateStructTmpl error: %v", err)
}
getStructCode, err := tmplExecuteWithFilter(data, handlerDetailStructCommonTmpl, columnID, columnCreatedAt, columnUpdatedAt)
if err != nil {
return "", fmt.Errorf("handlerDetailStructTmpl error: %v", err)
}
return postStructCode + putStructCode + getStructCode, nil
}
func getCommonServiceStructCode(data tmplData) (string, error) {
builder := strings.Builder{}
err := serviceStructCommonTmpl.Execute(&builder, data)
if err != nil {
return "", err
}
code := builder.String()
serviceCreateStructCode, err := tmplExecuteWithFilter(data, serviceCreateStructCommonTmpl)
if err != nil {
return "", fmt.Errorf("handle serviceCreateStructTmpl error: %v", err)
}
serviceCreateStructCode = strings.ReplaceAll(serviceCreateStructCode, "ID:", "Id:")
serviceUpdateStructCode, err := tmplExecuteWithFilter(data, serviceUpdateStructCommonTmpl, columnID)
if err != nil {
return "", fmt.Errorf("handle serviceUpdateStructTmpl error: %v", err)
}
serviceUpdateStructCode = strings.ReplaceAll(serviceUpdateStructCode, "ID:", "Id:")
code = strings.ReplaceAll(code, "// serviceCreateStructCode", serviceCreateStructCode)
code = strings.ReplaceAll(code, "// serviceUpdateStructCode", serviceUpdateStructCode)
return code, nil
}
func getCommonProtoFileCode(data tmplData, jsonNamedType int, isWebProto bool, isExtendedAPI bool) (string, error) {
data.Fields = goTypeToProto(data.Fields, jsonNamedType, true)
var err error
builder := strings.Builder{}
if isWebProto {
if isExtendedAPI {
err = protoFileForWebCommonTmpl.Execute(&builder, data)
} else {
err = protoFileForSimpleWebCommonTmpl.Execute(&builder, data)
}
if err != nil {
return "", err
}
} else {
if isExtendedAPI {
err = protoFileCommonTmpl.Execute(&builder, data)
} else {
err = protoFileSimpleCommonTmpl.Execute(&builder, data)
}
if err != nil {
return "", err
}
}
code := builder.String()
protoMessageCreateCode, err := tmplExecuteWithFilter2(data, protoMessageCreateCommonTmpl)
if err != nil {
return "", fmt.Errorf("handle protoMessageCreateCommonTmpl error: %v", err)
}
protoMessageUpdateCode, err := tmplExecuteWithFilter2(data, protoMessageUpdateCommonTmpl, columnID)
if err != nil {
return "", fmt.Errorf("handle protoMessageUpdateCommonTmpl error: %v", err)
}
if !isWebProto {
srcStr := fmt.Sprintf(`, (tagger.tags) = "uri:\"%s\""`, getProtoFieldName(data.Fields))
protoMessageUpdateCode = strings.ReplaceAll(protoMessageUpdateCode, srcStr, "")
}
protoMessageDetailCode, err := tmplExecuteWithFilter2(data, protoMessageDetailCommonTmpl, columnID, columnCreatedAt, columnUpdatedAt)
if err != nil {
return "", fmt.Errorf("handle protoMessageDetailCommonTmpl error: %v", err)
}
code = strings.ReplaceAll(code, "// protoMessageCreateCode", protoMessageCreateCode)
code = strings.ReplaceAll(code, "// protoMessageUpdateCode", protoMessageUpdateCode)
code = strings.ReplaceAll(code, "// protoMessageDetailCode", protoMessageDetailCode)
code = strings.ReplaceAll(code, "*time.Time", "int64")
code = strings.ReplaceAll(code, "time.Time", "int64")
code = strings.ReplaceAll(code, "left_curly_bracket", "{")
code = strings.ReplaceAll(code, "right_curly_bracket", "}")
code = adaptedDbType2(data, isWebProto, code)
return code, nil
}
func tmplExecuteWithFilter2(data tmplData, tmpl *template.Template, reservedColumns ...string) (string, error) {
var newFields = []tmplField{}
for _, field := range data.Fields {
if isIgnoreFields(field.ColName, reservedColumns...) {
continue
}
newFields = append(newFields, field)
}
data.Fields = newFields
builder := strings.Builder{}
err := tmpl.Execute(&builder, data)
if err != nil {
return "", fmt.Errorf("tmpl.Execute error: %v", err)
}
return builder.String(), nil
}
// nolint
func simpleGoTypeToProtoType(goType string) string {
var protoType string
switch goType {
case "int", "int32":
protoType = "int32"
case "uint", "uint32":
protoType = "uint32"
case "int64":
protoType = "int64"
case "uint64":
protoType = "uint64"
case "string":
protoType = "string"
case "time.Time", "*time.Time":
protoType = "string"
case "float32":
protoType = "float"
case "float64":
protoType = "double"
case goTypeInts, "[]int64":
protoType = "repeated int64"
case "[]int32":
protoType = "repeated int32"
case "[]byte":
protoType = "string"
case goTypeStrings:
protoType = "repeated string"
case jsonTypeName:
protoType = "string"
default:
protoType = "string"
}
return protoType
}
func adaptedDbType2(data tmplData, isWebProto bool, code string) string {
if isWebProto {
code = replaceProtoMessageFieldCode(code, webDefaultProtoMessageFieldCodes)
} else {
code = replaceProtoMessageFieldCode(code, grpcDefaultProtoMessageFieldCodes)
}
if data.ProtoSubStructs != "" {
code += "\n" + data.ProtoSubStructs
}
return code
}
func firstLetterToUpper(str string) string {
if len(str) == 0 {
return str
}
if (str[0] >= 'A' && str[0] <= 'Z') || (str[0] >= 'a' && str[0] <= 'z') {
return strings.ToUpper(str[:1]) + str[1:]
}
return str
}
func customFirstLetterToLower(str string) string {
str = firstLetterToLower(str)
if len(str) == 2 {
if str == "iD" {
str = "id"
} else if str == "iP" {
str = "ip"
}
} else if len(str) == 3 {
if str == "iDs" {
str = "ids"
} else if str == "iPs" {
str = "ips"
}
}
return str
}
func customEndOfLetterToLower(srcStr string, str string) string {
l := len(str) - len(srcStr)
if l == 1 {
if str[len(str)-1] == 'S' {
return str[:len(str)-1] + "s"
}
} else if l == 2 {
if str[len(str)-2:] == "ES" {
return str[:len(str)-2] + "es"
}
}
return str
}
func getHandlerGoType(field *tmplField) string {
var goType = field.GoType
if field.DBDriver == DBDriverMysql || field.DBDriver == DBDriverPostgresql || field.DBDriver == DBDriverTidb {
if field.rewriterField != nil {
switch field.rewriterField.goType {
case jsonTypeName:
goType = "string"
case boolTypeName, boolTypeTinyName:
goType = "*bool"
case decimalTypeName:
goType = "string"
}
}
}
if field.GoType == "time.Time" {
goType = "*time.Time"
}
return goType
}