mirror of
https://github.com/5rahim/seanime
synced 2026-05-02 22:42:11 +02:00
545 lines
17 KiB
Go
545 lines
17 KiB
Go
package codegen
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
)
|
|
|
|
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"`
|
|
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. User
|
|
TypescriptType string `json:"typescriptType"`
|
|
// 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_",
|
|
"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_",
|
|
"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_",
|
|
"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_",
|
|
"hibikemanga": "HibikeManga_",
|
|
"hibikeonlinestream": "HibikeOnlinestream_",
|
|
"hibiketorrent": "HibikeTorrent_",
|
|
}
|
|
|
|
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") {
|
|
// Parse the Go file
|
|
file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ParseComments)
|
|
if err != nil {
|
|
return 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
|
|
}
|
|
// 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)
|
|
|
|
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: fieldTypeToTypescriptType(typeSpec.Type, usedStructPkgName),
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
structs = append(structs, goStruct)
|
|
continue
|
|
}
|
|
|
|
//
|
|
// The type declaration is a struct
|
|
//
|
|
structType, ok := typeSpec.Type.(*ast.StructType)
|
|
if ok {
|
|
|
|
// 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: typeSpec.Name.Name,
|
|
FormattedName: getTypePrefix(packageName) + typeSpec.Name.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:
|
|
required = false
|
|
case *ast.ArrayType:
|
|
required = false
|
|
case *ast.MapType:
|
|
required = false
|
|
case *ast.SelectorExpr:
|
|
required = false
|
|
}
|
|
fieldName := field.Names[0].Name
|
|
|
|
usedStructType, usedStructPkgName := getUsedStructType(field.Type, packageName)
|
|
|
|
goStructField := &GoStructField{
|
|
Name: fieldName,
|
|
JsonName: jsonFieldName(field),
|
|
GoType: fieldTypeString(field.Type),
|
|
TypescriptType: fieldTypeToTypescriptType(field.Type, usedStructPkgName),
|
|
Required: required, // might need to parse struct tags to determine this
|
|
Public: field.Names[0].IsExported(),
|
|
UsedStructType: usedStructType,
|
|
Comments: comments,
|
|
}
|
|
goStruct.Fields = append(goStruct.Fields, goStructField)
|
|
}
|
|
structs = append(structs, goStruct)
|
|
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
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
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")
|
|
}
|
|
|
|
// 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 "", ""
|
|
}
|
|
|
|
if 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
|
|
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 "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
|
|
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"
|
|
}
|
|
|
|
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"
|
|
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
|
|
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, ",")
|
|
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
|
|
}
|