Files
seanime-docker/seanime-2.9.10/codegen/internal/generate_structs.go
2025-09-20 14:08:38 +01:00

811 lines
24 KiB
Go

package codegen
import (
"encoding/json"
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"reflect"
"strings"
"unicode"
)
type GoStruct struct {
Filepath string `json:"filepath"`
Filename string `json:"filename"`
Name string `json:"name"`
FormattedName string `json:"formattedName"` // name with package prefix e.g. models.User => Models_User
Package string `json:"package"`
Fields []*GoStructField `json:"fields"`
AliasOf *GoAlias `json:"aliasOf,omitempty"`
Comments []string `json:"comments"`
EmbeddedStructTypes []string `json:"embeddedStructNames,omitempty"`
}
type GoAlias struct {
GoType string `json:"goType"`
TypescriptType string `json:"typescriptType"`
UsedTypescriptType string `json:"usedTypescriptType,omitempty"`
DeclaredValues []string `json:"declaredValues"`
UsedStructType string `json:"usedStructName,omitempty"`
}
type GoStructField struct {
Name string `json:"name"`
JsonName string `json:"jsonName"`
// e.g. map[string]models.User
GoType string `json:"goType"`
// e.g. []struct{Test string `json:"test"`, Test2 string `json:"test2"`}
InlineStructType string `json:"inlineStructType,omitempty"`
// e.g. User
TypescriptType string `json:"typescriptType"`
// e.g. TypescriptType = Array<Models_User> => UsedTypescriptType = Models_User
UsedTypescriptType string `json:"usedTypescriptType,omitempty"`
// e.g. GoType = map[string]models.User => TypescriptType = User => UsedStructType = models.User
UsedStructType string `json:"usedStructName,omitempty"`
// If no 'omitempty' and not a pointer
Required bool `json:"required"`
Public bool `json:"public"`
Comments []string `json:"comments"`
}
var typePrefixesByPackage = map[string]string{
"anilist": "AL_",
"auto_downloader": "AutoDownloader_",
"autodownloader": "AutoDownloader_",
"entities": "",
"db": "DB_",
"db_bridge": "DB_",
"models": "Models_",
"playbackmanager": "PlaybackManager_",
"torrent_client": "TorrentClient_",
"events": "Events_",
"torrent": "Torrent_",
"manga": "Manga_",
"autoscanner": "AutoScanner_",
"listsync": "ListSync_",
"util": "Util_",
"scanner": "Scanner_",
"offline": "Offline_",
"discordrpc": "DiscordRPC_",
"discordrpc_presence": "DiscordRPC_",
"anizip": "Anizip_",
"animap": "Animap_",
"onlinestream": "Onlinestream_",
"onlinestream_providers": "Onlinestream_",
"onlinestream_sources": "Onlinestream_",
"manga_providers": "Manga_",
"chapter_downloader": "ChapterDownloader_",
"manga_downloader": "MangaDownloader_",
"docs": "INTERNAL_",
"tvdb": "TVDB_",
"metadata": "Metadata_",
"mappings": "Mappings_",
"mal": "MAL_",
"handlers": "",
"animetosho": "AnimeTosho_",
"updater": "Updater_",
"anime": "Anime_",
"anime_types": "Anime_",
"summary": "Summary_",
"filesystem": "Filesystem_",
"filecache": "Filecache_",
"core": "INTERNAL_",
"comparison": "Comparison_",
"mediastream": "Mediastream_",
"torrentstream": "Torrentstream_",
"extension": "Extension_",
"extension_repo": "ExtensionRepo_",
//"vendor_hibike_manga": "HibikeManga_",
//"vendor_hibike_onlinestream": "HibikeOnlinestream_",
//"vendor_hibike_torrent": "HibikeTorrent_",
//"vendor_hibike_mediaplayer": "HibikeMediaPlayer_",
//"vendor_hibike_extension": "HibikeExtension_",
"hibikemanga": "HibikeManga_",
"hibikeonlinestream": "HibikeOnlinestream_",
"hibiketorrent": "HibikeTorrent_",
"hibikemediaplayer": "HibikeMediaPlayer_",
"hibikeextension": "HibikeExtension_",
"continuity": "Continuity_",
"local": "Local_",
"debrid": "Debrid_",
"debrid_client": "DebridClient_",
"report": "Report_",
"habari": "Habari_",
"vendor_habari": "Habari_",
"discordrpc_client": "DiscordRPC_",
"directstream": "Directstream_",
"nativeplayer": "NativePlayer_",
"mkvparser": "MKVParser_",
"nakama": "Nakama_",
}
func getTypePrefix(packageName string) string {
if prefix, ok := typePrefixesByPackage[packageName]; ok {
return prefix
}
return ""
}
func ExtractStructs(dir string, outDir string) {
structs := make([]*GoStruct, 0)
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && strings.HasSuffix(info.Name(), ".go") {
res, err := getGoStructsFromFile(path, info)
if err != nil {
return err
}
structs = append(structs, res...)
}
return nil
})
if err != nil {
fmt.Println("Error:", err)
return
}
// Write structs to file
_ = os.MkdirAll(outDir, os.ModePerm)
file, err := os.Create(outDir + "/public_structs.json")
if err != nil {
fmt.Println("Error:", err)
return
}
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
if err := encoder.Encode(structs); err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Public structs extracted and saved to public_structs.json")
}
func getGoStructsFromFile(path string, info os.FileInfo) (structs []*GoStruct, err error) {
// Parse the Go file
file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ParseComments)
if err != nil {
return nil, err
}
packageName := file.Name.Name
// Extract public structs
for _, decl := range file.Decls {
genDecl, ok := decl.(*ast.GenDecl)
if !ok || genDecl.Tok != token.TYPE {
continue
}
//
// Go through each type declaration
//
for _, spec := range genDecl.Specs {
typeSpec, ok := spec.(*ast.TypeSpec)
if !ok {
continue
}
if !typeSpec.Name.IsExported() {
continue
}
//
// The type declaration is an alias
// e.g. alias.Name: string, typeSpec.Name.Name: MediaListStatus
//
alias, ok := typeSpec.Type.(*ast.Ident)
if ok {
if alias.Name == typeSpec.Name.Name {
continue
}
goStruct := goStructFromAlias(path, info, genDecl, typeSpec, packageName, alias, file)
structs = append(structs, goStruct)
continue
}
//
// The type declaration is a struct
//
structType, ok := typeSpec.Type.(*ast.StructType)
if ok {
subStructs := make([]*GoStruct, 0)
for _, field := range structType.Fields.List {
if field.Names != nil && len(field.Names) > 0 {
subStructType, ok := field.Type.(*ast.StructType)
if ok {
name := fmt.Sprintf("%s_%s", typeSpec.Name.Name, field.Names[0].Name)
subStruct := goStructFromStruct(path, info, genDecl, name, packageName, subStructType)
subStructs = append(subStructs, subStruct)
continue
}
}
}
goStruct := goStructFromStruct(path, info, genDecl, typeSpec.Name.Name, packageName, structType)
// Replace struct fields with sub structs
for _, field := range goStruct.Fields {
if field.GoType == "__STRUCT__" {
for _, subStruct := range subStructs {
if subStruct.Name == fmt.Sprintf("%s_%s", typeSpec.Name.Name, field.Name) {
field.GoType = subStruct.FormattedName
field.TypescriptType = subStruct.FormattedName
field.UsedStructType = fmt.Sprintf("%s.%s", subStruct.Package, subStruct.Name)
break
}
}
}
}
structs = append(structs, goStruct)
structs = append(structs, subStructs...)
continue
}
mapType, ok := typeSpec.Type.(*ast.MapType)
if ok {
goStruct := &GoStruct{
Filepath: path,
Filename: info.Name(),
Name: typeSpec.Name.Name,
FormattedName: getTypePrefix(packageName) + typeSpec.Name.Name,
Package: packageName,
Fields: make([]*GoStructField, 0),
}
usedStructType, usedStructPkgName := getUsedStructType(mapType, packageName)
goStruct.AliasOf = &GoAlias{
GoType: fieldTypeString(mapType),
TypescriptType: fieldTypeToTypescriptType(mapType, usedStructPkgName),
UsedStructType: usedStructType,
}
structs = append(structs, goStruct)
continue
}
sliceType, ok := typeSpec.Type.(*ast.ArrayType)
if ok {
goStruct := &GoStruct{
Filepath: path,
Filename: info.Name(),
Name: typeSpec.Name.Name,
FormattedName: getTypePrefix(packageName) + typeSpec.Name.Name,
Package: packageName,
Fields: make([]*GoStructField, 0),
}
usedStructType, usedStructPkgName := getUsedStructType(sliceType, packageName)
goStruct.AliasOf = &GoAlias{
GoType: fieldTypeString(sliceType),
TypescriptType: fieldTypeToTypescriptType(sliceType, usedStructPkgName),
UsedStructType: usedStructType,
}
structs = append(structs, goStruct)
continue
}
}
}
return structs, nil
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Example:
//
// type User struct {
// ID int `json:"id"`
// Name string `json:"name"`
// }
func goStructFromStruct(path string, info os.FileInfo, genDecl *ast.GenDecl, name string, packageName string, structType *ast.StructType) *GoStruct {
// Get comments
comments := make([]string, 0)
if genDecl.Doc != nil && genDecl.Doc.List != nil && len(genDecl.Doc.List) > 0 {
for _, comment := range genDecl.Doc.List {
comments = append(comments, strings.TrimPrefix(comment.Text, "//"))
}
}
goStruct := &GoStruct{
Filepath: filepath.ToSlash(path),
Filename: info.Name(),
Name: name,
FormattedName: getTypePrefix(packageName) + name,
Package: packageName,
Fields: make([]*GoStructField, 0),
EmbeddedStructTypes: make([]string, 0),
Comments: comments,
}
// Get fields
for _, field := range structType.Fields.List {
if field.Names == nil || len(field.Names) == 0 {
if len(field.Names) == 0 {
switch field.Type.(type) {
case *ast.Ident, *ast.StarExpr, *ast.SelectorExpr:
usedStructType, _ := getUsedStructType(field.Type, packageName)
goStruct.EmbeddedStructTypes = append(goStruct.EmbeddedStructTypes, usedStructType)
}
}
continue
}
// Get fields comments
comments := make([]string, 0)
if field.Comment != nil && field.Comment.List != nil && len(field.Comment.List) > 0 {
for _, comment := range field.Comment.List {
comments = append(comments, strings.TrimPrefix(comment.Text, "//"))
}
}
required := true
if field.Tag != nil {
tag := reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1])
jsonTag := tag.Get("json")
if jsonTag != "" {
jsonParts := strings.Split(jsonTag, ",")
if len(jsonParts) > 1 && jsonParts[1] == "omitempty" {
required = false
}
}
}
switch field.Type.(type) {
case *ast.StarExpr, *ast.ArrayType, *ast.MapType, *ast.SelectorExpr:
required = false
}
fieldName := field.Names[0].Name
usedStructType, usedStructPkgName := getUsedStructType(field.Type, packageName)
tsType := fieldTypeToTypescriptType(field.Type, usedStructPkgName)
goStructField := &GoStructField{
Name: fieldName,
JsonName: jsonFieldName(field),
GoType: fieldTypeString(field.Type),
TypescriptType: tsType,
UsedTypescriptType: fieldTypeToUsedTypescriptType(tsType),
Required: required,
Public: field.Names[0].IsExported(),
UsedStructType: usedStructType,
Comments: comments,
}
// If it's an inline struct, capture the full definition as a string
if goStructField.GoType == "__STRUCT__" {
if structType, ok := field.Type.(*ast.StructType); ok {
goStructField.InlineStructType = formatInlineStruct(structType)
}
} else {
// Check if it's a slice of inline structs
if arrayType, ok := field.Type.(*ast.ArrayType); ok {
if structType, ok := arrayType.Elt.(*ast.StructType); ok {
goStructField.InlineStructType = "[]" + formatInlineStruct(structType)
}
}
// Check if it's a map with inline struct values
if mapType, ok := field.Type.(*ast.MapType); ok {
if structType, ok := mapType.Value.(*ast.StructType); ok {
goStructField.InlineStructType = "map[" + fieldTypeString(mapType.Key) + "]" + formatInlineStruct(structType)
}
}
}
goStruct.Fields = append(goStruct.Fields, goStructField)
}
return goStruct
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func goStructFromAlias(path string, info os.FileInfo, genDecl *ast.GenDecl, typeSpec *ast.TypeSpec, packageName string, alias *ast.Ident, file *ast.File) *GoStruct {
// Get comments
comments := make([]string, 0)
if genDecl.Doc != nil && genDecl.Doc.List != nil && len(genDecl.Doc.List) > 0 {
for _, comment := range genDecl.Doc.List {
comments = append(comments, strings.TrimPrefix(comment.Text, "//"))
}
}
usedStructType, usedStructPkgName := getUsedStructType(typeSpec.Type, packageName)
tsType := fieldTypeToTypescriptType(typeSpec.Type, usedStructPkgName)
goStruct := &GoStruct{
Filepath: filepath.ToSlash(path),
Filename: info.Name(),
Name: typeSpec.Name.Name,
Package: packageName,
FormattedName: getTypePrefix(packageName) + typeSpec.Name.Name,
Fields: make([]*GoStructField, 0),
Comments: comments,
AliasOf: &GoAlias{
GoType: alias.Name,
TypescriptType: tsType,
UsedTypescriptType: fieldTypeToUsedTypescriptType(tsType),
UsedStructType: usedStructType,
},
}
// Get declared values - useful for building enums or union types
// e.g. const Something AliasType = "something"
goStruct.AliasOf.DeclaredValues = make([]string, 0)
for _, decl := range file.Decls {
genDecl, ok := decl.(*ast.GenDecl)
if !ok || genDecl.Tok != token.CONST {
continue
}
for _, spec := range genDecl.Specs {
valueSpec, ok := spec.(*ast.ValueSpec)
if !ok {
continue
}
valueSpecType := fieldTypeString(valueSpec.Type)
if len(valueSpec.Names) == 1 && valueSpec.Names[0].IsExported() && valueSpecType == typeSpec.Name.Name {
for _, value := range valueSpec.Values {
name, ok := value.(*ast.BasicLit)
if !ok {
continue
}
goStruct.AliasOf.DeclaredValues = append(goStruct.AliasOf.DeclaredValues, name.Value)
}
}
}
}
return goStruct
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// getUsedStructType returns the used struct type for a given type declaration.
// For example, if the type declaration is `map[string]models.User`, the used struct type is `models.User`.
// If the type declaration is `[]User`, the used struct type is `{packageName}.User`.
func getUsedStructType(expr ast.Expr, packageName string) (string, string) {
usedStructType := fieldTypeToUsedStructType(expr)
switch usedStructType {
case "string", "bool", "byte", "uint", "uint8", "uint16", "uint32", "uint64", "int", "int8", "int16", "int32", "int64", "float", "float32", "float64":
return "", ""
case "__STRUCT__":
return "", ""
}
if usedStructType != "__STRUCT__" && usedStructType != "" && !strings.Contains(usedStructType, ".") {
usedStructType = packageName + "." + usedStructType
}
pkgName := strings.Split(usedStructType, ".")[0]
return usedStructType, pkgName
}
// fieldTypeString returns the field type as a string.
// For example, if the field type is `[]*models.User`, the return value is `[]models.User`.
// If the field type is `[]InternalStruct`, the return value is `[]InternalStruct`.
func fieldTypeString(fieldType ast.Expr) string {
switch t := fieldType.(type) {
case *ast.Ident:
return t.Name
case *ast.StarExpr:
//return "*" + fieldTypeString(t.X)
return fieldTypeString(t.X)
case *ast.ArrayType:
if fieldTypeString(t.Elt) == "byte" {
return "string"
}
return "[]" + fieldTypeString(t.Elt)
case *ast.MapType:
return "map[" + fieldTypeString(t.Key) + "]" + fieldTypeString(t.Value)
case *ast.SelectorExpr:
return fieldTypeString(t.X) + "." + t.Sel.Name
case *ast.StructType:
return "__STRUCT__"
default:
return ""
}
}
// fieldTypeToTypescriptType returns the field type as a string in TypeScript format.
// For example, if the field type is `[]*models.User`, the return value is `Array<Models_User>`.
func fieldTypeToTypescriptType(fieldType ast.Expr, usedStructPkgName string) string {
switch t := fieldType.(type) {
case *ast.Ident:
switch t.Name {
case "string":
return "string"
case "uint", "uint8", "uint16", "uint32", "uint64", "int", "int8", "int16", "int32", "int64", "float", "float32", "float64":
return "number"
case "bool":
return "boolean"
case "byte":
return "string"
case "time.Time":
return "string"
case "nil":
return "null"
default:
return getTypePrefix(usedStructPkgName) + t.Name
}
case *ast.StarExpr:
return fieldTypeToTypescriptType(t.X, usedStructPkgName)
case *ast.ArrayType:
if fieldTypeToTypescriptType(t.Elt, usedStructPkgName) == "byte" {
return "string"
}
return "Array<" + fieldTypeToTypescriptType(t.Elt, usedStructPkgName) + ">"
case *ast.MapType:
return "Record<" + fieldTypeToTypescriptType(t.Key, usedStructPkgName) + ", " + fieldTypeToTypescriptType(t.Value, usedStructPkgName) + ">"
case *ast.SelectorExpr:
if t.Sel.Name == "Time" {
return "string"
}
return getTypePrefix(usedStructPkgName) + t.Sel.Name
case *ast.StructType:
s := "{ "
for _, field := range t.Fields.List {
s += jsonFieldName(field) + ": " + fieldTypeToTypescriptType(field.Type, usedStructPkgName) + "; "
}
s += "}"
return s
default:
return "any"
}
}
func stringGoTypeToTypescriptType(goType string) string {
switch goType {
case "string":
return "string"
case "uint", "uint8", "uint16", "uint32", "uint64", "int", "int8", "int16", "int32", "int64", "float", "float32", "float64":
return "number"
case "nil":
return "null"
case "bool":
return "boolean"
case "time.Time":
return "string"
}
if strings.HasPrefix(goType, "[]") {
return "Array<" + stringGoTypeToTypescriptType(goType[2:]) + ">"
}
if strings.HasPrefix(goType, "*") {
return stringGoTypeToTypescriptType(goType[1:])
}
if strings.HasPrefix(goType, "map[") {
s := strings.TrimPrefix(goType, "map[")
key := ""
value := ""
for i, c := range s {
if c == ']' {
key = s[:i]
value = s[i+1:]
break
}
}
return "Record<" + stringGoTypeToTypescriptType(key) + ", " + stringGoTypeToTypescriptType(value) + ">"
}
if strings.Contains(goType, ".") {
parts := strings.Split(goType, ".")
return getTypePrefix(parts[0]) + parts[1]
}
return goType
}
func goTypeToTypescriptType(goType string) string {
switch goType {
case "string":
return "string"
case "uint", "uint8", "uint16", "uint32", "uint64", "int", "int8", "int16", "int32", "int64", "float", "float32", "float64":
return "number"
case "bool":
return "boolean"
case "nil":
return "null"
case "time.Time":
return "string"
default:
return "unknown"
}
}
// fieldTypeUnformattedString returns the field type as a string without formatting.
// For example, if the field type is `[]*models.User`, the return value is `models.User`.
// /!\ Caveat: this assumes that the map key is always a string.
func fieldTypeUnformattedString(fieldType ast.Expr) string {
switch t := fieldType.(type) {
case *ast.Ident:
return t.Name
case *ast.StarExpr:
//return "*" + fieldTypeString(t.X)
return fieldTypeUnformattedString(t.X)
case *ast.ArrayType:
return fieldTypeUnformattedString(t.Elt)
case *ast.MapType:
return fieldTypeUnformattedString(t.Value)
case *ast.SelectorExpr:
return fieldTypeString(t.X) + "." + t.Sel.Name
default:
return ""
}
}
// fieldTypeToUsedStructType returns the used struct type for a given field type.
// For example, if the field type is `[]*models.User`, the return value is `models.User`.
func fieldTypeToUsedStructType(fieldType ast.Expr) string {
switch t := fieldType.(type) {
case *ast.StarExpr:
return fieldTypeString(t.X)
case *ast.ArrayType:
return fieldTypeString(t.Elt)
case *ast.MapType:
return fieldTypeUnformattedString(t.Value)
case *ast.SelectorExpr:
return fieldTypeString(t)
case *ast.Ident:
return t.Name
case *ast.StructType:
return "__STRUCT__"
default:
return ""
}
}
func jsonFieldName(field *ast.Field) string {
if field.Tag != nil {
tag := reflect.StructTag(strings.ReplaceAll(field.Tag.Value[1:len(field.Tag.Value)-1], "\\\"", "\""))
jsonTag := tag.Get("json")
if jsonTag != "" {
jsonParts := strings.Split(jsonTag, ",")
if jsonParts[0] == "-" {
return ""
}
if jsonParts[0] != "" {
return jsonParts[0]
}
return jsonParts[0]
}
}
return field.Names[0].Name
}
func jsonFieldOmitEmpty(field *ast.Field) bool {
if field.Tag != nil {
tag := reflect.StructTag(strings.ReplaceAll(field.Tag.Value[1:len(field.Tag.Value)-1], "\\\"", "\""))
jsonTag := tag.Get("json")
if jsonTag != "" {
jsonParts := strings.Split(jsonTag, ",")
return len(jsonParts) > 1 && jsonParts[1] == "omitempty"
}
}
return false
}
func isCustomStruct(goType string) bool {
return goTypeToTypescriptType(goType) == "unknown"
}
var nameExceptions = map[string]string{"OAuth2": "oauth2"}
func convertGoToJSName(name string) string {
if v, ok := nameExceptions[name]; ok {
return v
}
startUppercase := make([]rune, 0, len(name))
for _, c := range name {
if c != '_' && !unicode.IsUpper(c) && !unicode.IsDigit(c) {
break
}
startUppercase = append(startUppercase, c)
}
totalStartUppercase := len(startUppercase)
// all uppercase eg. "JSON" -> "json"
if len(name) == totalStartUppercase {
return strings.ToLower(name)
}
// eg. "JSONField" -> "jsonField"
if totalStartUppercase > 1 {
return strings.ToLower(name[0:totalStartUppercase-1]) + name[totalStartUppercase-1:]
}
// eg. "GetField" -> "getField"
if totalStartUppercase == 1 {
return strings.ToLower(name[0:1]) + name[1:]
}
return name
}
// fieldTypeToUsedTypescriptType extracts the core TypeScript type from complex type expressions
// For example, if the type is Array<Models_User>, it returns Models_User
// If the type is Record<string, Models_User>, it returns Models_User
func fieldTypeToUsedTypescriptType(tsType string) string {
// Handle arrays: Array<Type> -> Type
if strings.HasPrefix(tsType, "Array<") && strings.HasSuffix(tsType, ">") {
innerType := strings.TrimPrefix(strings.TrimSuffix(tsType, ">"), "Array<")
return fieldTypeToUsedTypescriptType(innerType)
}
// Handle records: Record<Key, Value> -> Value
if strings.HasPrefix(tsType, "Record<") && strings.HasSuffix(tsType, ">") {
innerType := strings.TrimPrefix(strings.TrimSuffix(tsType, ">"), "Record<")
// Find the comma that separates key and value
commaIndex := -1
bracketCount := 0
for i, char := range innerType {
if char == '<' {
bracketCount++
} else if char == '>' {
bracketCount--
} else if char == ',' && bracketCount == 0 {
commaIndex = i
break
}
}
if commaIndex != -1 {
valueType := strings.TrimSpace(innerType[commaIndex+1:])
return fieldTypeToUsedTypescriptType(valueType)
}
}
// Handle primitive types
switch tsType {
case "string", "number", "boolean", "any", "null", "undefined":
return ""
}
return tsType
}
// formatInlineStruct formats an inline struct definition as a string
// e.g. struct{Test string `json:"test"`, Test2 string `json:"test2"`}
func formatInlineStruct(structType *ast.StructType) string {
result := "struct{\n"
for i, field := range structType.Fields.List {
if i > 0 {
result += "\n"
}
if field.Names != nil && len(field.Names) > 0 {
result += field.Names[0].Name + " " + fieldTypeString(field.Type)
if field.Tag != nil {
result += " " + field.Tag.Value
}
} else {
result += fieldTypeString(field.Type)
}
}
result += "}"
return result
}