node build fixed
This commit is contained in:
26
seanime-2.9.10/codegen/internal/examples/structs1.go
Normal file
26
seanime-2.9.10/codegen/internal/examples/structs1.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"seanime/internal/api/anilist"
|
||||
hibiketorrent "seanime/internal/extension/hibike/torrent"
|
||||
)
|
||||
|
||||
//type Struct1 struct {
|
||||
// Struct2
|
||||
//}
|
||||
//
|
||||
//type Struct2 struct {
|
||||
// Text string `json:"text"`
|
||||
//}
|
||||
|
||||
//type Struct3 []string
|
||||
|
||||
type Struct4 struct {
|
||||
Torrents []hibiketorrent.AnimeTorrent `json:"torrents"`
|
||||
Destination string `json:"destination"`
|
||||
SmartSelect struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
MissingEpisodeNumbers []int `json:"missingEpisodeNumbers"`
|
||||
} `json:"smartSelect"`
|
||||
Media *anilist.BaseAnime `json:"media"`
|
||||
}
|
||||
308
seanime-2.9.10/codegen/internal/generate_handlers.go
Normal file
308
seanime-2.9.10/codegen/internal/generate_handlers.go
Normal file
@@ -0,0 +1,308 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
RouteHandler struct {
|
||||
Name string `json:"name"`
|
||||
TrimmedName string `json:"trimmedName"`
|
||||
Comments []string `json:"comments"`
|
||||
Filepath string `json:"filepath"`
|
||||
Filename string `json:"filename"`
|
||||
Api *RouteHandlerApi `json:"api"`
|
||||
}
|
||||
|
||||
RouteHandlerApi struct {
|
||||
Summary string `json:"summary"`
|
||||
Descriptions []string `json:"descriptions"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Methods []string `json:"methods"`
|
||||
Params []*RouteHandlerParam `json:"params"`
|
||||
BodyFields []*RouteHandlerParam `json:"bodyFields"`
|
||||
Returns string `json:"returns"`
|
||||
ReturnGoType string `json:"returnGoType"`
|
||||
ReturnTypescriptType string `json:"returnTypescriptType"`
|
||||
}
|
||||
|
||||
RouteHandlerParam struct {
|
||||
Name string `json:"name"`
|
||||
JsonName string `json:"jsonName"`
|
||||
GoType string `json:"goType"` // e.g., []models.User
|
||||
InlineStructType string `json:"inlineStructType,omitempty"` // e.g., struct{Test string `json:"test"`}
|
||||
UsedStructType string `json:"usedStructType"` // e.g., models.User
|
||||
TypescriptType string `json:"typescriptType"` // e.g., Array<User>
|
||||
Required bool `json:"required"`
|
||||
Descriptions []string `json:"descriptions"`
|
||||
}
|
||||
)
|
||||
|
||||
func GenerateHandlers(dir string, outDir string) {
|
||||
|
||||
handlers := make([]*RouteHandler, 0)
|
||||
|
||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if !strings.HasSuffix(info.Name(), ".go") || strings.HasPrefix(info.Name(), "_") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse the file
|
||||
file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, decl := range file.Decls {
|
||||
// Check if the declaration is a function
|
||||
fn, ok := decl.(*ast.FuncDecl)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if the function has comments
|
||||
if fn.Doc == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the comments
|
||||
comments := strings.Split(fn.Doc.Text(), "\n")
|
||||
if len(comments) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the function name
|
||||
name := fn.Name.Name
|
||||
trimmedName := strings.TrimPrefix(name, "Handle")
|
||||
|
||||
// Get the filename
|
||||
filep := strings.ReplaceAll(strings.ReplaceAll(path, "\\", "/"), "../", "")
|
||||
filename := filepath.Base(path)
|
||||
|
||||
// Get the endpoint
|
||||
endpoint := ""
|
||||
var methods []string
|
||||
params := make([]*RouteHandlerParam, 0)
|
||||
summary := ""
|
||||
descriptions := make([]string, 0)
|
||||
returns := "bool"
|
||||
|
||||
for _, comment := range comments {
|
||||
cmt := strings.TrimSpace(strings.TrimPrefix(comment, "//"))
|
||||
if strings.HasPrefix(cmt, "@summary") {
|
||||
summary = strings.TrimSpace(strings.TrimPrefix(cmt, "@summary"))
|
||||
}
|
||||
|
||||
if strings.HasPrefix(cmt, "@desc") {
|
||||
descriptions = append(descriptions, strings.TrimSpace(strings.TrimPrefix(cmt, "@desc")))
|
||||
}
|
||||
|
||||
if strings.HasPrefix(cmt, "@route") {
|
||||
endpointParts := strings.Split(strings.TrimSpace(strings.TrimPrefix(cmt, "@route")), " ")
|
||||
if len(endpointParts) == 2 {
|
||||
endpoint = endpointParts[0]
|
||||
methods = strings.Split(endpointParts[1][1:len(endpointParts[1])-1], ",")
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(cmt, "@param") {
|
||||
paramParts := strings.Split(strings.TrimSpace(strings.TrimPrefix(cmt, "@param")), " - ")
|
||||
if len(paramParts) == 4 {
|
||||
required := paramParts[2] == "true"
|
||||
params = append(params, &RouteHandlerParam{
|
||||
Name: paramParts[0],
|
||||
JsonName: paramParts[0],
|
||||
GoType: paramParts[1],
|
||||
TypescriptType: goTypeToTypescriptType(paramParts[1]),
|
||||
Required: required,
|
||||
Descriptions: []string{strings.ReplaceAll(paramParts[3], "\"", "")},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(cmt, "@returns") {
|
||||
returns = strings.TrimSpace(strings.TrimPrefix(cmt, "@returns"))
|
||||
}
|
||||
}
|
||||
|
||||
bodyFields := make([]*RouteHandlerParam, 0)
|
||||
// To get the request body fields, we need to look at the function body for a struct called "body"
|
||||
|
||||
// Get the function body
|
||||
body := fn.Body
|
||||
if body != nil {
|
||||
for _, stmt := range body.List {
|
||||
// Check if the statement is a declaration
|
||||
declStmt, ok := stmt.(*ast.DeclStmt)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// Check if the declaration is a gen decl
|
||||
genDecl, ok := declStmt.Decl.(*ast.GenDecl)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// Check if the declaration is a type
|
||||
if genDecl.Tok != token.TYPE {
|
||||
continue
|
||||
}
|
||||
// Check if the type is a struct
|
||||
if len(genDecl.Specs) != 1 {
|
||||
continue
|
||||
}
|
||||
typeSpec, ok := genDecl.Specs[0].(*ast.TypeSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
structType, ok := typeSpec.Type.(*ast.StructType)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// Check if the struct is called "body"
|
||||
if typeSpec.Name.Name != "body" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the fields
|
||||
for _, field := range structType.Fields.List {
|
||||
// Get the field name
|
||||
fieldName := field.Names[0].Name
|
||||
|
||||
// Get the field type
|
||||
fieldType := field.Type
|
||||
|
||||
jsonName := fieldName
|
||||
// Get the field tag
|
||||
required := !jsonFieldOmitEmpty(field)
|
||||
jsonField := jsonFieldName(field)
|
||||
if jsonField != "" {
|
||||
jsonName = jsonField
|
||||
}
|
||||
|
||||
// Get field comments
|
||||
fieldComments := make([]string, 0)
|
||||
cmtsTxt := field.Doc.Text()
|
||||
if cmtsTxt != "" {
|
||||
fieldComments = strings.Split(cmtsTxt, "\n")
|
||||
}
|
||||
for _, cmt := range fieldComments {
|
||||
cmt = strings.TrimSpace(strings.TrimPrefix(cmt, "//"))
|
||||
if cmt != "" {
|
||||
fieldComments = append(fieldComments, cmt)
|
||||
}
|
||||
}
|
||||
|
||||
switch fieldType.(type) {
|
||||
case *ast.StarExpr:
|
||||
required = false
|
||||
}
|
||||
|
||||
goType := fieldTypeString(fieldType)
|
||||
goTypeUnformatted := fieldTypeUnformattedString(fieldType)
|
||||
packageName := "handlers"
|
||||
if strings.Contains(goTypeUnformatted, ".") {
|
||||
parts := strings.Split(goTypeUnformatted, ".")
|
||||
packageName = parts[0]
|
||||
}
|
||||
|
||||
tsType := fieldTypeToTypescriptType(fieldType, packageName)
|
||||
|
||||
usedStructType := goTypeUnformatted
|
||||
switch goTypeUnformatted {
|
||||
case "string", "int", "int64", "float64", "float32", "bool", "nil", "uint", "uint64", "uint32", "uint16", "uint8", "byte", "rune", "[]byte", "interface{}", "error":
|
||||
usedStructType = ""
|
||||
}
|
||||
|
||||
// Add the request body field
|
||||
bodyFields = append(bodyFields, &RouteHandlerParam{
|
||||
Name: fieldName,
|
||||
JsonName: jsonName,
|
||||
GoType: goType,
|
||||
UsedStructType: usedStructType,
|
||||
TypescriptType: tsType,
|
||||
Required: required,
|
||||
Descriptions: fieldComments,
|
||||
})
|
||||
|
||||
// Check if it's an inline struct and capture its definition
|
||||
if structType, ok := fieldType.(*ast.StructType); ok {
|
||||
bodyFields[len(bodyFields)-1].InlineStructType = formatInlineStruct(structType)
|
||||
} else {
|
||||
// Check if it's a slice of inline structs
|
||||
if arrayType, ok := fieldType.(*ast.ArrayType); ok {
|
||||
if structType, ok := arrayType.Elt.(*ast.StructType); ok {
|
||||
bodyFields[len(bodyFields)-1].InlineStructType = "[]" + formatInlineStruct(structType)
|
||||
}
|
||||
}
|
||||
// Check if it's a map with inline struct values
|
||||
if mapType, ok := fieldType.(*ast.MapType); ok {
|
||||
if structType, ok := mapType.Value.(*ast.StructType); ok {
|
||||
bodyFields[len(bodyFields)-1].InlineStructType = "map[" + fieldTypeString(mapType.Key) + "]" + formatInlineStruct(structType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the route handler
|
||||
routeHandler := &RouteHandler{
|
||||
Name: name,
|
||||
TrimmedName: trimmedName,
|
||||
Comments: comments,
|
||||
Filepath: filep,
|
||||
Filename: filename,
|
||||
Api: &RouteHandlerApi{
|
||||
Summary: summary,
|
||||
Descriptions: descriptions,
|
||||
Endpoint: endpoint,
|
||||
Methods: methods,
|
||||
Params: params,
|
||||
BodyFields: bodyFields,
|
||||
Returns: returns,
|
||||
ReturnGoType: getUnformattedGoType(returns),
|
||||
ReturnTypescriptType: stringGoTypeToTypescriptType(returns),
|
||||
},
|
||||
}
|
||||
|
||||
handlers = append(handlers, routeHandler)
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Write structs to file
|
||||
_ = os.MkdirAll(outDir, os.ModePerm)
|
||||
file, err := os.Create(outDir + "/handlers.json")
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
encoder := json.NewEncoder(file)
|
||||
encoder.SetIndent("", " ")
|
||||
if err := encoder.Encode(handlers); err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
146
seanime-2.9.10/codegen/internal/generate_hook_events_handlers.go
Normal file
146
seanime-2.9.10/codegen/internal/generate_hook_events_handlers.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GenerateHandlerHookEvents generates hook_events.go file for handlers
|
||||
func GenerateHandlerHookEvents(handlersJsonPath string, outputDir string) {
|
||||
// Create output directory if it doesn't exist
|
||||
err := os.MkdirAll(outputDir, os.ModePerm)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Read handlers.json
|
||||
handlersJson, err := os.ReadFile(handlersJsonPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Parse handlers.json
|
||||
var handlers []RouteHandler
|
||||
err = json.Unmarshal(handlersJson, &handlers)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Create hook_events.go file
|
||||
outFilePath := filepath.Join(outputDir, "hook_events.go")
|
||||
f, err := os.Create(outFilePath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Write package declaration and imports
|
||||
f.WriteString("package handlers\n\n")
|
||||
f.WriteString("import (\n")
|
||||
//f.WriteString("\t\"seanime/internal/hook_resolver\"\n")
|
||||
|
||||
imports := []string{
|
||||
"\"seanime/internal/api/anilist\"",
|
||||
"\"seanime/internal/api/tvdb\"",
|
||||
"\"seanime/internal/continuity\"",
|
||||
"\"seanime/internal/database/models\"",
|
||||
"\"seanime/internal/debrid/client\"",
|
||||
"\"seanime/internal/debrid/debrid\"",
|
||||
"\"seanime/internal/extension\"",
|
||||
"hibikemanga \"seanime/internal/extension/hibike/manga\"",
|
||||
"hibikeonlinestream \"seanime/internal/extension/hibike/onlinestream\"",
|
||||
"hibiketorrent \"seanime/internal/extension/hibike/torrent\"",
|
||||
"\"seanime/internal/extension_playground\"",
|
||||
"\"seanime/internal/extension_repo\"",
|
||||
"\"seanime/internal/hook_resolver\"",
|
||||
"\"seanime/internal/library/anime\"",
|
||||
"\"seanime/internal/library/summary\"",
|
||||
"\"seanime/internal/manga\"",
|
||||
"\"seanime/internal/manga/downloader\"",
|
||||
"\"seanime/internal/mediastream\"",
|
||||
"\"seanime/internal/onlinestream\"",
|
||||
"\"seanime/internal/report\"",
|
||||
"\"seanime/internal/sync\"",
|
||||
"\"seanime/internal/torrent_clients/torrent_client\"",
|
||||
"\"seanime/internal/torrents/torrent\"",
|
||||
"\"seanime/internal/torrentstream\"",
|
||||
"\"seanime/internal/updater\"",
|
||||
}
|
||||
|
||||
for _, imp := range imports {
|
||||
f.WriteString("\t" + imp + "\n")
|
||||
}
|
||||
|
||||
f.WriteString(")\n\n")
|
||||
|
||||
// Generate events for each handler
|
||||
for _, handler := range handlers {
|
||||
// Skip if handler name is empty or doesn't start with 'Handle'
|
||||
if handler.Name == "" || !strings.HasPrefix(handler.Name, "Handle") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Generate the "Requested" event
|
||||
f.WriteString(fmt.Sprintf("// %sRequestedEvent is triggered when %s is requested.\n", handler.Name, handler.TrimmedName))
|
||||
f.WriteString("// Prevent default to skip the default behavior and return your own data.\n")
|
||||
f.WriteString(fmt.Sprintf("type %sRequestedEvent struct {\n", handler.Name))
|
||||
f.WriteString("\thook_resolver.Event\n")
|
||||
|
||||
// Add path parameters
|
||||
for _, param := range handler.Api.Params {
|
||||
f.WriteString(fmt.Sprintf("\t%s %s `json:\"%s\"`\n", pascalCase(param.Name), param.GoType, param.JsonName))
|
||||
}
|
||||
|
||||
// Add body fields
|
||||
for _, field := range handler.Api.BodyFields {
|
||||
goType := field.GoType
|
||||
if goType == "__STRUCT__" || goType == "[]__STRUCT__" || (strings.HasPrefix(goType, "map[") && strings.Contains(goType, "__STRUCT__")) {
|
||||
goType = field.InlineStructType
|
||||
}
|
||||
goType = strings.Replace(goType, "handlers.", "", 1)
|
||||
addPointer := isCustomStruct(goType)
|
||||
if addPointer {
|
||||
goType = "*" + goType
|
||||
}
|
||||
f.WriteString(fmt.Sprintf("\t%s %s `json:\"%s\"`\n", pascalCase(field.Name), goType, field.JsonName))
|
||||
}
|
||||
|
||||
// If handler returns something other than bool or true, add a Data field to store the result
|
||||
if handler.Api.ReturnGoType != "" && handler.Api.ReturnGoType != "true" && handler.Api.ReturnGoType != "bool" {
|
||||
returnGoType := strings.Replace(handler.Api.ReturnGoType, "handlers.", "", 1)
|
||||
addPointer := isCustomStruct(returnGoType)
|
||||
if addPointer {
|
||||
returnGoType = "*" + returnGoType
|
||||
}
|
||||
f.WriteString(fmt.Sprintf("\t// Empty data object, will be used if the hook prevents the default behavior\n"))
|
||||
f.WriteString(fmt.Sprintf("\tData %s `json:\"data\"`\n", returnGoType))
|
||||
}
|
||||
|
||||
f.WriteString("}\n\n")
|
||||
|
||||
// Generate the response event if handler returns something other than bool or true
|
||||
if handler.Api.ReturnGoType != "" && handler.Api.ReturnGoType != "true" && handler.Api.ReturnGoType != "bool" {
|
||||
returnGoType := strings.Replace(handler.Api.ReturnGoType, "handlers.", "", 1)
|
||||
addPointer := isCustomStruct(returnGoType)
|
||||
if addPointer {
|
||||
returnGoType = "*" + returnGoType
|
||||
}
|
||||
f.WriteString(fmt.Sprintf("// %sEvent is triggered after processing %s.\n", handler.Name, handler.TrimmedName))
|
||||
f.WriteString(fmt.Sprintf("type %sEvent struct {\n", handler.Name))
|
||||
f.WriteString("\thook_resolver.Event\n")
|
||||
f.WriteString(fmt.Sprintf("\tData %s `json:\"data\"`\n", returnGoType))
|
||||
f.WriteString("}\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command("gofmt", "-w", outFilePath)
|
||||
cmd.Run()
|
||||
}
|
||||
|
||||
func pascalCase(s string) string {
|
||||
return strings.ReplaceAll(strings.Title(strings.ReplaceAll(s, "_", " ")), " ", "")
|
||||
}
|
||||
797
seanime-2.9.10/codegen/internal/generate_plugin_events.go
Normal file
797
seanime-2.9.10/codegen/internal/generate_plugin_events.go
Normal file
@@ -0,0 +1,797 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
var (
|
||||
additionalStructNamesForHooks = []string{
|
||||
"discordrpc_presence.MangaActivity",
|
||||
"discordrpc_presence.AnimeActivity",
|
||||
"discordrpc_presence.LegacyAnimeActivity",
|
||||
"anilist.ListAnime",
|
||||
"anilist.ListManga",
|
||||
"anilist.MediaSort",
|
||||
"anilist.ListRecentAnime",
|
||||
"anilist.AnimeCollectionWithRelations",
|
||||
"onlinestream.Episode",
|
||||
"continuity.WatchHistoryItem",
|
||||
"continuity.WatchHistoryItemResponse",
|
||||
"continuity.UpdateWatchHistoryItemOptions",
|
||||
"continuity.WatchHistory",
|
||||
"torrent_client.Torrent",
|
||||
}
|
||||
)
|
||||
|
||||
func GeneratePluginEventFile(inFilePath string, outDir string) {
|
||||
// Parse the input file
|
||||
file, err := parser.ParseFile(token.NewFileSet(), inFilePath, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Create output directory if it doesn't exist
|
||||
_ = os.MkdirAll(outDir, os.ModePerm)
|
||||
|
||||
const OutFileName = "plugin-events.ts"
|
||||
|
||||
// Create output file
|
||||
f, err := os.Create(filepath.Join(outDir, OutFileName))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Write imports
|
||||
f.WriteString(`// This file is auto-generated. Do not edit.
|
||||
import { useWebsocketPluginMessageListener, useWebsocketSender } from "@/app/(main)/_hooks/handle-websockets"
|
||||
import { useCallback } from "react"
|
||||
|
||||
`)
|
||||
|
||||
// Extract client and server event types
|
||||
clientEvents := make([]string, 0)
|
||||
serverEvents := make([]string, 0)
|
||||
clientPayloads := make(map[string]string)
|
||||
serverPayloads := make(map[string]string)
|
||||
clientEventValues := make(map[string]string)
|
||||
serverEventValues := make(map[string]string)
|
||||
|
||||
for _, decl := range file.Decls {
|
||||
genDecl, ok := decl.(*ast.GenDecl)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find const declarations
|
||||
if genDecl.Tok == token.CONST {
|
||||
for _, spec := range genDecl.Specs {
|
||||
valueSpec, ok := spec.(*ast.ValueSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(valueSpec.Names) == 1 && len(valueSpec.Values) == 1 {
|
||||
name := valueSpec.Names[0].Name
|
||||
if strings.HasPrefix(name, "Client") && strings.HasSuffix(name, "Event") {
|
||||
eventName := name[len("Client") : len(name)-len("Event")]
|
||||
// Get the string literal value for the enum
|
||||
if basicLit, ok := valueSpec.Values[0].(*ast.BasicLit); ok {
|
||||
eventValue := strings.Trim(basicLit.Value, "\"")
|
||||
clientEvents = append(clientEvents, eventName)
|
||||
// Get payload type name
|
||||
payloadType := name + "Payload"
|
||||
clientPayloads[eventName] = payloadType
|
||||
// Store the original string value
|
||||
clientEventValues[eventName] = eventValue
|
||||
}
|
||||
} else if strings.HasPrefix(name, "Server") && strings.HasSuffix(name, "Event") {
|
||||
eventName := name[len("Server") : len(name)-len("Event")]
|
||||
// Get the string literal value for the enum
|
||||
if basicLit, ok := valueSpec.Values[0].(*ast.BasicLit); ok {
|
||||
eventValue := strings.Trim(basicLit.Value, "\"")
|
||||
serverEvents = append(serverEvents, eventName)
|
||||
// Get payload type name
|
||||
payloadType := name + "Payload"
|
||||
serverPayloads[eventName] = payloadType
|
||||
// Store the original string value
|
||||
serverEventValues[eventName] = eventValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write enums
|
||||
f.WriteString("export enum PluginClientEvents {\n")
|
||||
for _, event := range clientEvents {
|
||||
enumName := toPascalCase(event)
|
||||
f.WriteString(fmt.Sprintf(" %s = \"%s\",\n", enumName, clientEventValues[event]))
|
||||
}
|
||||
f.WriteString("}\n\n")
|
||||
|
||||
f.WriteString("export enum PluginServerEvents {\n")
|
||||
for _, event := range serverEvents {
|
||||
enumName := toPascalCase(event)
|
||||
f.WriteString(fmt.Sprintf(" %s = \"%s\",\n", enumName, serverEventValues[event]))
|
||||
}
|
||||
f.WriteString("}\n\n")
|
||||
|
||||
// Write client to server section
|
||||
f.WriteString("/////////////////////////////////////////////////////////////////////////////////////\n")
|
||||
f.WriteString("// Client to server\n")
|
||||
f.WriteString("/////////////////////////////////////////////////////////////////////////////////////\n\n")
|
||||
|
||||
// Write client event types and hooks
|
||||
for _, event := range clientEvents {
|
||||
// Get the payload type
|
||||
payloadType := clientPayloads[event]
|
||||
payloadFound := false
|
||||
|
||||
// Find the payload type in the AST
|
||||
for _, decl := range file.Decls {
|
||||
genDecl, ok := decl.(*ast.GenDecl)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if genDecl.Tok == token.TYPE {
|
||||
for _, spec := range genDecl.Specs {
|
||||
typeSpec, ok := spec.(*ast.TypeSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if typeSpec.Name.Name == payloadType {
|
||||
payloadFound = true
|
||||
// Write the payload type
|
||||
f.WriteString(fmt.Sprintf("export type Plugin_Client_%sEventPayload = {\n", toPascalCase(event)))
|
||||
|
||||
if structType, ok := typeSpec.Type.(*ast.StructType); ok {
|
||||
for _, field := range structType.Fields.List {
|
||||
if len(field.Names) > 0 {
|
||||
fieldName := jsonFieldName(field)
|
||||
fieldType := fieldTypeToTypescriptType(field.Type, "")
|
||||
f.WriteString(fmt.Sprintf(" %s: %s\n", fieldName, fieldType))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f.WriteString("}\n\n")
|
||||
|
||||
// Write the hook
|
||||
hookName := fmt.Sprintf("usePluginSend%sEvent", toPascalCase(event))
|
||||
f.WriteString(fmt.Sprintf("export function %s() {\n", hookName))
|
||||
f.WriteString(" const { sendPluginMessage } = useWebsocketSender()\n")
|
||||
f.WriteString("\n")
|
||||
f.WriteString(fmt.Sprintf(" const send%sEvent = useCallback((payload: Plugin_Client_%sEventPayload, extensionID?: string) => {\n",
|
||||
toPascalCase(event), toPascalCase(event)))
|
||||
f.WriteString(fmt.Sprintf(" sendPluginMessage(PluginClientEvents.%s, payload, extensionID)\n",
|
||||
toPascalCase(event)))
|
||||
f.WriteString(" }, [])\n")
|
||||
f.WriteString("\n")
|
||||
f.WriteString(" return {\n")
|
||||
f.WriteString(fmt.Sprintf(" send%sEvent,\n", toPascalCase(event)))
|
||||
f.WriteString(" }\n")
|
||||
f.WriteString("}\n\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If payload type not found, write empty object type
|
||||
if !payloadFound {
|
||||
f.WriteString(fmt.Sprintf("export type Plugin_Client_%sEventPayload = {}\n\n", toPascalCase(event)))
|
||||
|
||||
// Write the hook
|
||||
hookName := fmt.Sprintf("usePluginSend%sEvent", toPascalCase(event))
|
||||
f.WriteString(fmt.Sprintf("export function %s() {\n", hookName))
|
||||
f.WriteString(" const { sendPluginMessage } = useWebsocketSender()\n")
|
||||
f.WriteString("\n")
|
||||
f.WriteString(fmt.Sprintf(" const sendPlugin%sEvent = useCallback((payload: Plugin_Client_%sEventPayload, extensionID?: string) => {\n",
|
||||
toPascalCase(event), toPascalCase(event)))
|
||||
f.WriteString(fmt.Sprintf(" sendPluginMessage(PluginClientEvents.%s, payload, extensionID)\n",
|
||||
toPascalCase(event)))
|
||||
f.WriteString(" }, [])\n")
|
||||
f.WriteString("\n")
|
||||
f.WriteString(" return {\n")
|
||||
f.WriteString(fmt.Sprintf(" send%sEvent,\n", toPascalCase(event)))
|
||||
f.WriteString(" }\n")
|
||||
f.WriteString("}\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Write server to client section
|
||||
f.WriteString("/////////////////////////////////////////////////////////////////////////////////////\n")
|
||||
f.WriteString("// Server to client\n")
|
||||
f.WriteString("/////////////////////////////////////////////////////////////////////////////////////\n\n")
|
||||
|
||||
// Write server event types and hooks
|
||||
for _, event := range serverEvents {
|
||||
// Get the payload type
|
||||
payloadType := serverPayloads[event]
|
||||
payloadFound := false
|
||||
|
||||
// Find the payload type in the AST
|
||||
for _, decl := range file.Decls {
|
||||
genDecl, ok := decl.(*ast.GenDecl)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if genDecl.Tok == token.TYPE {
|
||||
for _, spec := range genDecl.Specs {
|
||||
typeSpec, ok := spec.(*ast.TypeSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if typeSpec.Name.Name == payloadType {
|
||||
payloadFound = true
|
||||
// Write the payload type
|
||||
f.WriteString(fmt.Sprintf("export type Plugin_Server_%sEventPayload = {\n", toPascalCase(event)))
|
||||
|
||||
if structType, ok := typeSpec.Type.(*ast.StructType); ok {
|
||||
for _, field := range structType.Fields.List {
|
||||
if len(field.Names) > 0 {
|
||||
fieldName := jsonFieldName(field)
|
||||
fieldType := fieldTypeToTypescriptType(field.Type, "")
|
||||
f.WriteString(fmt.Sprintf(" %s: %s\n", fieldName, fieldType))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f.WriteString("}\n\n")
|
||||
|
||||
// Write the hook
|
||||
hookName := fmt.Sprintf("usePluginListen%sEvent", toPascalCase(event))
|
||||
f.WriteString(fmt.Sprintf("export function %s(cb: (payload: Plugin_Server_%sEventPayload, extensionId: string) => void, extensionID: string) {\n",
|
||||
hookName, toPascalCase(event)))
|
||||
f.WriteString(" return useWebsocketPluginMessageListener<Plugin_Server_" + toPascalCase(event) + "EventPayload>({\n")
|
||||
f.WriteString(" extensionId: extensionID,\n")
|
||||
f.WriteString(fmt.Sprintf(" type: PluginServerEvents.%s,\n", toPascalCase(event)))
|
||||
f.WriteString(" onMessage: cb,\n")
|
||||
f.WriteString(" })\n")
|
||||
f.WriteString("}\n\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If payload type not found, write empty object type
|
||||
if !payloadFound {
|
||||
f.WriteString(fmt.Sprintf("export type Plugin_Server_%sEventPayload = {}\n\n", toPascalCase(event)))
|
||||
|
||||
// Write the hook
|
||||
hookName := fmt.Sprintf("usePluginListen%sEvent", toPascalCase(event))
|
||||
f.WriteString(fmt.Sprintf("export function %s(cb: (payload: Plugin_Server_%sEventPayload, extensionId: string) => void, extensionID: string) {\n",
|
||||
hookName, toPascalCase(event)))
|
||||
f.WriteString(" return useWebsocketPluginMessageListener<Plugin_Server_" + toPascalCase(event) + "EventPayload>({\n")
|
||||
f.WriteString(" extensionId: extensionID,\n")
|
||||
f.WriteString(fmt.Sprintf(" type: PluginServerEvents.%s,\n", toPascalCase(event)))
|
||||
f.WriteString(" onMessage: cb,\n")
|
||||
f.WriteString(" })\n")
|
||||
f.WriteString("}\n\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var execptions = map[string]string{
|
||||
"playbackmanager": "PlaybackManager ",
|
||||
}
|
||||
|
||||
func toPascalCase(s string) string {
|
||||
if exception, ok := execptions[s]; ok {
|
||||
return exception
|
||||
}
|
||||
s = strings.ReplaceAll(s, "-", " ")
|
||||
s = strings.ReplaceAll(s, "_", " ")
|
||||
s = cases.Title(language.English, cases.NoLower).String(s)
|
||||
return strings.ReplaceAll(s, " ", "")
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type HookEventDefinition struct {
|
||||
Package string `json:"package"`
|
||||
GoStruct *GoStruct `json:"goStruct"`
|
||||
}
|
||||
|
||||
func GeneratePluginHooksDefinitionFile(outDir string, publicStructsFilePath string, genOutDir string) {
|
||||
// Create output file
|
||||
f, err := os.Create(filepath.Join(outDir, "app.d.ts"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
mdFile, err := os.Create(filepath.Join(genOutDir, "hooks.mdx"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer mdFile.Close()
|
||||
|
||||
goStructs := LoadPublicStructs(publicStructsFilePath)
|
||||
|
||||
// e.g. map["models.User"]*GoStruct
|
||||
goStructsMap := make(map[string]*GoStruct)
|
||||
|
||||
for _, goStruct := range goStructs {
|
||||
goStructsMap[goStruct.Package+"."+goStruct.Name] = goStruct
|
||||
}
|
||||
|
||||
// Expand the structs with embedded structs
|
||||
for _, goStruct := range goStructs {
|
||||
for _, embeddedStructType := range goStruct.EmbeddedStructTypes {
|
||||
if embeddedStructType != "" {
|
||||
if usedStruct, ok := goStructsMap[embeddedStructType]; ok {
|
||||
for _, usedField := range usedStruct.Fields {
|
||||
goStruct.Fields = append(goStruct.Fields, usedField)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Key = package
|
||||
eventGoStructsMap := make(map[string][]*GoStruct)
|
||||
for _, goStruct := range goStructs {
|
||||
if goStruct.Filename == "hook_events.go" {
|
||||
if _, ok := eventGoStructsMap[goStruct.Package]; !ok {
|
||||
eventGoStructsMap[goStruct.Package] = make([]*GoStruct, 0)
|
||||
}
|
||||
eventGoStructsMap[goStruct.Package] = append(eventGoStructsMap[goStruct.Package], goStruct)
|
||||
}
|
||||
}
|
||||
|
||||
// Create `hooks.json`
|
||||
hookEventDefinitions := make([]*HookEventDefinition, 0)
|
||||
for _, goStruct := range goStructs {
|
||||
if goStruct.Filename == "hook_events.go" {
|
||||
hookEventDefinitions = append(hookEventDefinitions, &HookEventDefinition{
|
||||
Package: goStruct.Package,
|
||||
GoStruct: goStruct,
|
||||
})
|
||||
}
|
||||
}
|
||||
jsonFile, err := os.Create(filepath.Join(genOutDir, "hooks.json"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer jsonFile.Close()
|
||||
encoder := json.NewEncoder(jsonFile)
|
||||
encoder.SetIndent("", " ")
|
||||
if err := encoder.Encode(hookEventDefinitions); err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
// Write `app.d.ts`
|
||||
// Write namespace declaration
|
||||
////////////////////////////////////////////////////
|
||||
f.WriteString("declare namespace $app {\n")
|
||||
|
||||
packageNames := make([]string, 0)
|
||||
for packageName := range eventGoStructsMap {
|
||||
packageNames = append(packageNames, packageName)
|
||||
}
|
||||
slices.Sort(packageNames)
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
// Get referenced structs so we can write them at the end
|
||||
//////////////////////////////////////////////////////////
|
||||
sharedStructs := make([]*GoStruct, 0)
|
||||
otherStructs := make([]*GoStruct, 0)
|
||||
|
||||
// Go through all the event structs' fields, and get the types that are structs
|
||||
sharedStructsMap := make(map[string]*GoStruct)
|
||||
for _, goStructs := range eventGoStructsMap {
|
||||
for _, goStruct := range goStructs {
|
||||
for _, field := range goStruct.Fields {
|
||||
if isCustomStruct(field.GoType) {
|
||||
if _, ok := sharedStructsMap[field.GoType]; !ok && goStructsMap[field.UsedStructType] != nil {
|
||||
sharedStructsMap[field.UsedStructType] = goStructsMap[field.UsedStructType]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add additional structs to otherStructs
|
||||
for _, structName := range additionalStructNamesForHooks {
|
||||
if _, ok := sharedStructsMap[structName]; !ok {
|
||||
sharedStructsMap[structName] = goStructsMap[structName]
|
||||
}
|
||||
}
|
||||
|
||||
for _, goStruct := range sharedStructsMap {
|
||||
//fmt.Println(goStruct.FormattedName)
|
||||
if goStruct.Package != "" {
|
||||
sharedStructs = append(sharedStructs, goStruct)
|
||||
}
|
||||
}
|
||||
|
||||
referencedStructsMap, ok := getReferencedStructsRecursively(sharedStructs, otherStructs, goStructsMap)
|
||||
if !ok {
|
||||
panic("Failed to get referenced structs")
|
||||
}
|
||||
|
||||
for _, packageName := range packageNames {
|
||||
writePackageEventGoStructs(f, packageName, eventGoStructsMap[packageName], goStructsMap)
|
||||
}
|
||||
|
||||
f.WriteString(" ///////////////////////////////////////////////////////////////////////////////////////////////////////////////\n")
|
||||
f.WriteString(" ///////////////////////////////////////////////////////////////////////////////////////////////////////////////\n")
|
||||
f.WriteString(" ///////////////////////////////////////////////////////////////////////////////////////////////////////////////\n\n")
|
||||
|
||||
referencedStructs := make([]*GoStruct, 0)
|
||||
for _, goStruct := range referencedStructsMap {
|
||||
//fmt.Println(goStruct.FormattedName)
|
||||
referencedStructs = append(referencedStructs, goStruct)
|
||||
}
|
||||
slices.SortFunc(referencedStructs, func(a, b *GoStruct) int {
|
||||
return strings.Compare(a.FormattedName, b.FormattedName)
|
||||
})
|
||||
|
||||
// Write the shared structs at the end
|
||||
for _, goStruct := range referencedStructs {
|
||||
if goStruct.Package != "" {
|
||||
writeEventTypescriptType(f, goStruct, make(map[string]*GoStruct))
|
||||
}
|
||||
}
|
||||
|
||||
f.WriteString("}\n")
|
||||
|
||||
// Generate markdown documentation
|
||||
writeMarkdownFile(mdFile, hookEventDefinitions, referencedStructsMap, referencedStructs)
|
||||
|
||||
}
|
||||
|
||||
func writePackageEventGoStructs(f *os.File, packageName string, goStructs []*GoStruct, allGoStructs map[string]*GoStruct) {
|
||||
// Header comment block
|
||||
f.WriteString(fmt.Sprintf("\n /**\n * @package %s\n */\n\n", packageName))
|
||||
|
||||
// Declare the hook functions
|
||||
for _, goStruct := range goStructs {
|
||||
// Write comments
|
||||
comments := ""
|
||||
comments += fmt.Sprintf("\n * @event %s\n", goStruct.Name)
|
||||
comments += fmt.Sprintf(" * @file %s\n", strings.TrimPrefix(goStruct.Filepath, "../"))
|
||||
|
||||
shouldAddPreventDefault := false
|
||||
|
||||
if len(goStruct.Comments) > 0 {
|
||||
comments += fmt.Sprintf(" * @description\n")
|
||||
}
|
||||
for _, comment := range goStruct.Comments {
|
||||
if strings.Contains(strings.ToLower(comment), "prevent default") {
|
||||
shouldAddPreventDefault = true
|
||||
}
|
||||
comments += fmt.Sprintf(" * %s\n", strings.TrimSpace(comment))
|
||||
}
|
||||
f.WriteString(fmt.Sprintf(" /**%s */\n", comments))
|
||||
|
||||
//////// Write hook function
|
||||
f.WriteString(fmt.Sprintf(" function on%s(cb: (event: %s) => void): void;\n\n", strings.TrimSuffix(goStruct.Name, "Event"), goStruct.Name))
|
||||
|
||||
/////// Write event interface
|
||||
f.WriteString(fmt.Sprintf(" interface %s {\n", goStruct.Name))
|
||||
f.WriteString(fmt.Sprintf(" next(): void;\n\n"))
|
||||
if shouldAddPreventDefault {
|
||||
f.WriteString(fmt.Sprintf(" preventDefault(): void;\n\n"))
|
||||
}
|
||||
// Write the fields
|
||||
for _, field := range goStruct.Fields {
|
||||
if field.Name == "next" || field.Name == "preventDefault" || field.Name == "DefaultPrevented" {
|
||||
continue
|
||||
}
|
||||
if field.JsonName == "" {
|
||||
continue
|
||||
}
|
||||
// Field type
|
||||
fieldNameSuffix := ""
|
||||
if !field.Required {
|
||||
fieldNameSuffix = "?"
|
||||
}
|
||||
|
||||
if len(field.Comments) > 0 {
|
||||
f.WriteString(fmt.Sprintf(" /**\n"))
|
||||
for _, cmt := range field.Comments {
|
||||
f.WriteString(fmt.Sprintf(" * %s\n", strings.TrimSpace(cmt)))
|
||||
}
|
||||
f.WriteString(fmt.Sprintf(" */\n"))
|
||||
}
|
||||
|
||||
typeText := field.TypescriptType
|
||||
|
||||
f.WriteString(fmt.Sprintf(" %s%s: %s;\n", field.JsonName, fieldNameSuffix, typeText))
|
||||
}
|
||||
f.WriteString(fmt.Sprintf(" }\n\n"))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func writeEventTypescriptType(f *os.File, goStruct *GoStruct, writtenTypes map[string]*GoStruct) {
|
||||
f.WriteString(" /**\n")
|
||||
f.WriteString(fmt.Sprintf(" * - Filepath: %s\n", strings.TrimPrefix(goStruct.Filepath, "../")))
|
||||
if len(goStruct.Comments) > 0 {
|
||||
f.WriteString(fmt.Sprintf(" * @description\n"))
|
||||
for _, cmt := range goStruct.Comments {
|
||||
f.WriteString(fmt.Sprintf(" * %s\n", strings.TrimSpace(cmt)))
|
||||
}
|
||||
}
|
||||
f.WriteString(" */\n")
|
||||
|
||||
if len(goStruct.Fields) > 0 {
|
||||
f.WriteString(fmt.Sprintf(" interface %s {\n", goStruct.FormattedName))
|
||||
for _, field := range goStruct.Fields {
|
||||
fieldNameSuffix := ""
|
||||
if !field.Required {
|
||||
fieldNameSuffix = "?"
|
||||
}
|
||||
if field.JsonName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(field.Comments) > 0 {
|
||||
f.WriteString(fmt.Sprintf(" /**\n"))
|
||||
for _, cmt := range field.Comments {
|
||||
f.WriteString(fmt.Sprintf(" * %s\n", strings.TrimSpace(cmt)))
|
||||
}
|
||||
f.WriteString(fmt.Sprintf(" */\n"))
|
||||
}
|
||||
|
||||
typeText := field.TypescriptType
|
||||
if typeText == "Habari_Metadata" {
|
||||
typeText = "$habari.Metadata"
|
||||
}
|
||||
|
||||
f.WriteString(fmt.Sprintf(" %s%s: %s;\n", field.JsonName, fieldNameSuffix, typeText))
|
||||
}
|
||||
f.WriteString(" }\n\n")
|
||||
}
|
||||
|
||||
if goStruct.AliasOf != nil {
|
||||
if goStruct.AliasOf.DeclaredValues != nil && len(goStruct.AliasOf.DeclaredValues) > 0 {
|
||||
union := ""
|
||||
if len(goStruct.AliasOf.DeclaredValues) > 5 {
|
||||
union = strings.Join(goStruct.AliasOf.DeclaredValues, " |\n ")
|
||||
} else {
|
||||
union = strings.Join(goStruct.AliasOf.DeclaredValues, " | ")
|
||||
}
|
||||
f.WriteString(fmt.Sprintf(" export type %s = %s;\n\n", goStruct.FormattedName, union))
|
||||
} else {
|
||||
f.WriteString(fmt.Sprintf(" export type %s = %s;\n\n", goStruct.FormattedName, goStruct.AliasOf.TypescriptType))
|
||||
}
|
||||
}
|
||||
|
||||
// Add the struct to the written types
|
||||
writtenTypes[goStruct.Package+"."+goStruct.Name] = goStruct
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// writeMarkdownFile generates a well-formatted Markdown documentation for hooks
|
||||
func writeMarkdownFile(mdFile *os.File, hookEventDefinitions []*HookEventDefinition, referencedStructsMap map[string]*GoStruct, referencedStructs []*GoStruct) {
|
||||
|
||||
mdFile.WriteString("---\n")
|
||||
mdFile.WriteString("title: Hooks\n")
|
||||
mdFile.WriteString("description: How to use hooks\n")
|
||||
mdFile.WriteString("---")
|
||||
mdFile.WriteString("\n\n")
|
||||
|
||||
// Group hooks by package
|
||||
packageHooks := make(map[string][]*HookEventDefinition)
|
||||
for _, hook := range hookEventDefinitions {
|
||||
packageHooks[hook.Package] = append(packageHooks[hook.Package], hook)
|
||||
}
|
||||
|
||||
// Sort packages alphabetically
|
||||
packageNames := make([]string, 0, len(packageHooks))
|
||||
for pkg := range packageHooks {
|
||||
packageNames = append(packageNames, pkg)
|
||||
}
|
||||
slices.Sort(packageNames)
|
||||
|
||||
// Write each package section
|
||||
for _, pkg := range packageNames {
|
||||
hooks := packageHooks[pkg]
|
||||
|
||||
mdFile.WriteString(fmt.Sprintf("<a id=\"%s\"></a>\n", pkg))
|
||||
mdFile.WriteString(fmt.Sprintf("# %s\n\n", toPascalCase(pkg)))
|
||||
|
||||
// Write each hook in the package
|
||||
for _, hook := range hooks {
|
||||
goStruct := hook.GoStruct
|
||||
eventName := goStruct.Name
|
||||
hookName := fmt.Sprintf("on%s", strings.TrimSuffix(eventName, "Event"))
|
||||
|
||||
mdFile.WriteString(fmt.Sprintf("<a id=\"on%s\"></a>\n", strings.ToLower(strings.TrimSuffix(eventName, "Event"))))
|
||||
mdFile.WriteString(fmt.Sprintf("## %s\n\n", hookName))
|
||||
|
||||
// Write description
|
||||
if len(goStruct.Comments) > 0 {
|
||||
for _, comment := range goStruct.Comments {
|
||||
mdFile.WriteString(fmt.Sprintf("%s\n", strings.TrimSpace(comment)))
|
||||
}
|
||||
mdFile.WriteString("\n")
|
||||
}
|
||||
|
||||
// Check if it has preventDefault
|
||||
hasPreventDefault := false
|
||||
for _, comment := range goStruct.Comments {
|
||||
if strings.Contains(strings.ToLower(comment), "prevent default") {
|
||||
hasPreventDefault = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hasPreventDefault {
|
||||
mdFile.WriteString("**Can prevent default:** Yes\n\n")
|
||||
} else {
|
||||
mdFile.WriteString("**Can prevent default:** No\n\n")
|
||||
}
|
||||
|
||||
// Write event interface
|
||||
mdFile.WriteString("**Event Interface:**\n\n")
|
||||
mdFile.WriteString("```typescript\n")
|
||||
mdFile.WriteString(fmt.Sprintf("interface %s {\n", eventName))
|
||||
mdFile.WriteString(" next();\n")
|
||||
if hasPreventDefault {
|
||||
mdFile.WriteString(" preventDefault();\n")
|
||||
}
|
||||
|
||||
// Write fields
|
||||
for _, field := range goStruct.Fields {
|
||||
if field.Name == "next" || field.Name == "preventDefault" || field.Name == "DefaultPrevented" {
|
||||
continue
|
||||
}
|
||||
if field.JsonName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fieldNameSuffix := ""
|
||||
if !field.Required {
|
||||
fieldNameSuffix = "?"
|
||||
}
|
||||
|
||||
// Add comments if available
|
||||
if len(field.Comments) > 0 {
|
||||
mdFile.WriteString("\n /**\n")
|
||||
for _, comment := range field.Comments {
|
||||
mdFile.WriteString(fmt.Sprintf(" * %s\n", strings.TrimSpace(comment)))
|
||||
}
|
||||
mdFile.WriteString(" */\n")
|
||||
}
|
||||
|
||||
mdFile.WriteString(fmt.Sprintf(" %s%s: %s;\n", field.JsonName, fieldNameSuffix, field.TypescriptType))
|
||||
}
|
||||
|
||||
mdFile.WriteString("}\n")
|
||||
mdFile.WriteString("```\n\n")
|
||||
|
||||
referenced := make([]*GoStruct, 0)
|
||||
for _, field := range goStruct.Fields {
|
||||
if !isCustomStruct(field.GoType) {
|
||||
continue
|
||||
}
|
||||
goStruct, ok := referencedStructsMap[field.UsedStructType]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
referenced = append(referenced, goStruct)
|
||||
}
|
||||
|
||||
// Add a list of referenced structs links
|
||||
if len(referenced) > 0 {
|
||||
mdFile.WriteString("**Event types:**\n\n")
|
||||
}
|
||||
for _, goStruct := range referenced {
|
||||
mdFile.WriteString(fmt.Sprintf("- [%s](#%s)\n", goStruct.FormattedName, goStruct.FormattedName))
|
||||
}
|
||||
mdFile.WriteString("\n")
|
||||
|
||||
// Add example usage
|
||||
mdFile.WriteString("**Example:**\n\n")
|
||||
mdFile.WriteString("```typescript\n")
|
||||
mdFile.WriteString(fmt.Sprintf("$app.%s((e) => {\n", hookName))
|
||||
|
||||
// Generate example code based on fields
|
||||
for _, field := range goStruct.Fields {
|
||||
if field.Name == "next" || field.Name == "preventDefault" || field.Name == "DefaultPrevented" {
|
||||
continue
|
||||
}
|
||||
|
||||
mdFile.WriteString(fmt.Sprintf(" // console.log(e.%s);\n", field.JsonName))
|
||||
}
|
||||
|
||||
if hasPreventDefault {
|
||||
mdFile.WriteString("\n // Prevent default behavior if needed\n")
|
||||
mdFile.WriteString(" // e.preventDefault();\n")
|
||||
}
|
||||
|
||||
mdFile.WriteString(" \n e.next();\n")
|
||||
mdFile.WriteString("});\n")
|
||||
mdFile.WriteString("```\n\n")
|
||||
|
||||
// Add separator between hooks
|
||||
mdFile.WriteString("---\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Write the referenced structs
|
||||
if len(referencedStructs) > 0 {
|
||||
mdFile.WriteString("\n# Referenced Types\n\n")
|
||||
}
|
||||
for _, goStruct := range referencedStructs {
|
||||
|
||||
mdFile.WriteString(fmt.Sprintf("#### %s\n\n", goStruct.FormattedName))
|
||||
mdFile.WriteString(fmt.Sprintf("<div id=\"%s\"></div>\n\n", goStruct.FormattedName))
|
||||
mdFile.WriteString(fmt.Sprintf("**Filepath:** `%s`\n\n", strings.TrimPrefix(goStruct.Filepath, "../")))
|
||||
|
||||
if len(goStruct.Fields) > 0 {
|
||||
mdFile.WriteString("**Fields:**\n\n")
|
||||
|
||||
mdFile.WriteString("<Table>\n")
|
||||
mdFile.WriteString("<TableCaption>Fields</TableCaption>\n")
|
||||
mdFile.WriteString("<TableHeader>\n")
|
||||
mdFile.WriteString("<TableRow>\n")
|
||||
mdFile.WriteString("<TableHead>Property</TableHead>\n")
|
||||
mdFile.WriteString("<TableHead>Type</TableHead>\n")
|
||||
mdFile.WriteString("<TableHead>Description</TableHead>\n")
|
||||
mdFile.WriteString("</TableRow>\n")
|
||||
mdFile.WriteString("</TableHeader>\n")
|
||||
mdFile.WriteString("<TableBody>\n")
|
||||
for _, field := range goStruct.Fields {
|
||||
mdFile.WriteString(fmt.Sprintf("<TableRow>\n"))
|
||||
mdFile.WriteString(fmt.Sprintf("<TableCell className=\"py-1 px-2 max-w-[200px] break-all\">%s</TableCell>\n", field.JsonName))
|
||||
|
||||
typeContainsReference := false
|
||||
if field.UsedStructType != "" && isCustomStruct(field.UsedStructType) {
|
||||
typeContainsReference = true
|
||||
}
|
||||
if typeContainsReference {
|
||||
link := fmt.Sprintf("<a href=\"#%s\">`%s`</a>", field.UsedTypescriptType, field.TypescriptType)
|
||||
mdFile.WriteString(fmt.Sprintf("<TableCell className=\"py-1 px-2 break-all\">%s</TableCell>\n", link))
|
||||
} else {
|
||||
mdFile.WriteString(fmt.Sprintf("<TableCell className=\"py-1 px-2 break-all\">`%s`</TableCell>\n", field.TypescriptType))
|
||||
}
|
||||
mdFile.WriteString(fmt.Sprintf("<TableCell className=\"py-1 px-2 max-w-[200px] break-all\">%s</TableCell>\n", cmp.Or(strings.Join(field.Comments, "\n"), "-")))
|
||||
mdFile.WriteString("</TableRow>\n")
|
||||
}
|
||||
mdFile.WriteString("</TableBody>\n")
|
||||
mdFile.WriteString("</Table>\n")
|
||||
|
||||
}
|
||||
|
||||
if goStruct.AliasOf != nil {
|
||||
if goStruct.AliasOf.DeclaredValues != nil && len(goStruct.AliasOf.DeclaredValues) > 0 {
|
||||
union := ""
|
||||
if len(goStruct.AliasOf.DeclaredValues) > 5 {
|
||||
union = strings.Join(goStruct.AliasOf.DeclaredValues, " |\n ")
|
||||
} else {
|
||||
union = strings.Join(goStruct.AliasOf.DeclaredValues, " | ")
|
||||
}
|
||||
mdFile.WriteString(fmt.Sprintf("`%s`\n\n", union))
|
||||
} else {
|
||||
mdFile.WriteString(fmt.Sprintf("`%s`\n\n", goStruct.AliasOf.TypescriptType))
|
||||
}
|
||||
}
|
||||
|
||||
mdFile.WriteString("\n")
|
||||
}
|
||||
}
|
||||
810
seanime-2.9.10/codegen/internal/generate_structs.go
Normal file
810
seanime-2.9.10/codegen/internal/generate_structs.go
Normal file
@@ -0,0 +1,810 @@
|
||||
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
|
||||
}
|
||||
23
seanime-2.9.10/codegen/internal/generate_structs_test.go
Normal file
23
seanime-2.9.10/codegen/internal/generate_structs_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/stretchr/testify/require"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetGoStructsFromFile(t *testing.T) {
|
||||
|
||||
testPath := filepath.Join(".", "examples", "structs1.go")
|
||||
|
||||
info, err := os.Stat(testPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
goStructs, err := getGoStructsFromFile(testPath, info)
|
||||
require.NoError(t, err)
|
||||
|
||||
spew.Dump(goStructs)
|
||||
|
||||
}
|
||||
465
seanime-2.9.10/codegen/internal/generate_ts_endpoints.go
Normal file
465
seanime-2.9.10/codegen/internal/generate_ts_endpoints.go
Normal file
@@ -0,0 +1,465 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
const (
|
||||
typescriptEndpointsFileName = "endpoints.ts"
|
||||
typescriptEndpointTypesFileName = "endpoint.types.ts"
|
||||
typescriptHooksFileName = "hooks_template.ts"
|
||||
goEndpointsFileName = "endpoints.go"
|
||||
space = " "
|
||||
)
|
||||
|
||||
var additionalStructNamesForEndpoints = []string{}
|
||||
|
||||
func GenerateTypescriptEndpointsFile(handlersJsonPath string, structsJsonPath string, outDir string, eventDir string) []string {
|
||||
handlers := LoadHandlers(handlersJsonPath)
|
||||
structs := LoadPublicStructs(structsJsonPath)
|
||||
|
||||
_ = os.MkdirAll(outDir, os.ModePerm)
|
||||
f, err := os.Create(filepath.Join(outDir, typescriptEndpointsFileName))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
typeF, err := os.Create(filepath.Join(outDir, typescriptEndpointTypesFileName))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer typeF.Close()
|
||||
|
||||
hooksF, err := os.Create(filepath.Join(outDir, typescriptHooksFileName))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer hooksF.Close()
|
||||
|
||||
f.WriteString("// This code was generated by codegen/main.go. DO NOT EDIT.\n\n")
|
||||
|
||||
f.WriteString(`export type ApiEndpoints = Record<string, Record<string, {
|
||||
key: string,
|
||||
methods: ("POST" | "GET" | "PATCH" | "PUT" | "DELETE")[],
|
||||
endpoint: string
|
||||
}>>
|
||||
|
||||
`)
|
||||
|
||||
f.WriteString("export const API_ENDPOINTS = {\n")
|
||||
|
||||
groupedByFile := make(map[string][]*RouteHandler)
|
||||
for _, handler := range handlers {
|
||||
if _, ok := groupedByFile[handler.Filename]; !ok {
|
||||
groupedByFile[handler.Filename] = make([]*RouteHandler, 0)
|
||||
}
|
||||
groupedByFile[handler.Filename] = append(groupedByFile[handler.Filename], handler)
|
||||
}
|
||||
|
||||
filenames := make([]string, 0)
|
||||
for k := range groupedByFile {
|
||||
filenames = append(filenames, k)
|
||||
}
|
||||
|
||||
slices.SortStableFunc(filenames, func(i, j string) int {
|
||||
return strings.Compare(i, j)
|
||||
})
|
||||
|
||||
// Store the endpoints
|
||||
endpointsMap := make(map[string]string)
|
||||
|
||||
for _, filename := range filenames {
|
||||
routes := groupedByFile[filename]
|
||||
if len(routes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if lo.EveryBy(routes, func(route *RouteHandler) bool {
|
||||
return route.Api == nil || len(route.Api.Methods) == 0
|
||||
}) {
|
||||
continue
|
||||
}
|
||||
|
||||
groupName := strings.ToUpper(strings.TrimSuffix(filename, ".go"))
|
||||
|
||||
writeLine(f, fmt.Sprintf("\t%s: {", groupName)) // USERS: {
|
||||
|
||||
for _, route := range groupedByFile[filename] {
|
||||
if route.Api == nil || len(route.Api.Methods) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(route.Api.Descriptions) > 0 {
|
||||
writeLine(f, " /**")
|
||||
f.WriteString(fmt.Sprintf(" * @description\n"))
|
||||
f.WriteString(fmt.Sprintf(" * Route %s\n", route.Api.Summary))
|
||||
for _, cmt := range route.Api.Descriptions {
|
||||
writeLine(f, fmt.Sprintf(" * %s", strings.TrimSpace(cmt)))
|
||||
}
|
||||
writeLine(f, " */")
|
||||
}
|
||||
|
||||
writeLine(f, fmt.Sprintf("\t\t%s: {", strings.TrimPrefix(route.Name, "Handle"))) // GetAnimeCollection: {
|
||||
|
||||
methodStr := ""
|
||||
if len(route.Api.Methods) > 1 {
|
||||
methodStr = fmt.Sprintf("\"%s\"", strings.Join(route.Api.Methods, "\", \""))
|
||||
} else {
|
||||
methodStr = fmt.Sprintf("\"%s\"", route.Api.Methods[0])
|
||||
}
|
||||
|
||||
endpointsMap[strings.TrimPrefix(route.Name, "Handle")] = getEndpointKey(route.Name, groupName)
|
||||
|
||||
writeLine(f, fmt.Sprintf("\t\t\tkey: \"%s\",", getEndpointKey(route.Name, groupName)))
|
||||
|
||||
writeLine(f, fmt.Sprintf("\t\t\tmethods: [%s],", methodStr)) // methods: ['GET'],
|
||||
|
||||
writeLine(f, fmt.Sprintf("\t\t\tendpoint: \"%s\",", route.Api.Endpoint)) // path: '/api/v1/anilist/collection',
|
||||
|
||||
writeLine(f, "\t\t},") // },
|
||||
}
|
||||
|
||||
writeLine(f, "\t},") // },
|
||||
}
|
||||
|
||||
f.WriteString("} satisfies ApiEndpoints\n\n")
|
||||
|
||||
referenceGoStructs := make([]string, 0)
|
||||
for _, filename := range filenames {
|
||||
routes := groupedByFile[filename]
|
||||
if len(routes) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, route := range groupedByFile[filename] {
|
||||
if route.Api == nil || len(route.Api.Methods) == 0 {
|
||||
continue
|
||||
}
|
||||
if len(route.Api.Params) == 0 && len(route.Api.BodyFields) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, param := range route.Api.BodyFields {
|
||||
if param.UsedStructType != "" {
|
||||
referenceGoStructs = append(referenceGoStructs, param.UsedStructType)
|
||||
}
|
||||
}
|
||||
for _, param := range route.Api.Params {
|
||||
if param.UsedStructType != "" {
|
||||
referenceGoStructs = append(referenceGoStructs, param.UsedStructType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
referenceGoStructs = lo.Uniq(referenceGoStructs)
|
||||
|
||||
typeF.WriteString("// This code was generated by codegen/main.go. DO NOT EDIT.\n\n")
|
||||
|
||||
//
|
||||
// Imports
|
||||
//
|
||||
importedTypes := make([]string, 0)
|
||||
//
|
||||
for _, structName := range referenceGoStructs {
|
||||
parts := strings.Split(structName, ".")
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
var goStruct *GoStruct
|
||||
for _, s := range structs {
|
||||
if s.Name == parts[1] && s.Package == parts[0] {
|
||||
goStruct = s
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if goStruct == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
importedTypes = append(importedTypes, goStruct.FormattedName)
|
||||
}
|
||||
|
||||
for _, otherStrctName := range additionalStructNamesForEndpoints {
|
||||
importedTypes = append(importedTypes, stringGoTypeToTypescriptType(otherStrctName))
|
||||
}
|
||||
//
|
||||
slices.SortStableFunc(importedTypes, func(i, j string) int {
|
||||
return strings.Compare(i, j)
|
||||
})
|
||||
typeF.WriteString("import type {\n")
|
||||
for _, typeName := range importedTypes {
|
||||
typeF.WriteString(fmt.Sprintf(" %s,\n", typeName))
|
||||
}
|
||||
typeF.WriteString("} from \"@/api/generated/types.ts\"\n\n")
|
||||
|
||||
//
|
||||
// Types
|
||||
//
|
||||
|
||||
for _, filename := range filenames {
|
||||
routes := groupedByFile[filename]
|
||||
if len(routes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
typeF.WriteString("//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n")
|
||||
typeF.WriteString(fmt.Sprintf("// %s\n", strings.TrimSuffix(filename, ".go")))
|
||||
typeF.WriteString("//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n\n")
|
||||
|
||||
for _, route := range groupedByFile[filename] {
|
||||
if route.Api == nil || len(route.Api.Methods) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(route.Api.Params) == 0 && len(route.Api.BodyFields) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
typeF.WriteString("/**\n")
|
||||
typeF.WriteString(fmt.Sprintf(" * - Filepath: %s\n", filepath.ToSlash(strings.TrimPrefix(route.Filepath, "..\\"))))
|
||||
typeF.WriteString(fmt.Sprintf(" * - Filename: %s\n", route.Filename))
|
||||
typeF.WriteString(fmt.Sprintf(" * - Endpoint: %s\n", route.Api.Endpoint))
|
||||
if len(route.Api.Summary) > 0 {
|
||||
typeF.WriteString(fmt.Sprintf(" * @description\n"))
|
||||
typeF.WriteString(fmt.Sprintf(" * Route %s\n", strings.TrimSpace(route.Api.Summary)))
|
||||
}
|
||||
typeF.WriteString(" */\n")
|
||||
typeF.WriteString(fmt.Sprintf("export type %s_Variables = {\n", strings.TrimPrefix(route.Name, "Handle"))) // export type EditAnimeEntry_Variables = {
|
||||
|
||||
addedBodyFields := false
|
||||
for _, param := range route.Api.BodyFields {
|
||||
writeParamField(typeF, route, param) // mediaId: number;
|
||||
if param.UsedStructType != "" {
|
||||
referenceGoStructs = append(referenceGoStructs, param.UsedStructType)
|
||||
}
|
||||
addedBodyFields = true
|
||||
}
|
||||
|
||||
if !addedBodyFields {
|
||||
for _, param := range route.Api.Params {
|
||||
writeParamField(typeF, route, param) // mediaId: number;
|
||||
if param.UsedStructType != "" {
|
||||
referenceGoStructs = append(referenceGoStructs, param.UsedStructType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeLine(typeF, "}\n")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
generateHooksFile(hooksF, groupedByFile, filenames)
|
||||
|
||||
generateEventFile(eventDir, endpointsMap)
|
||||
|
||||
return referenceGoStructs
|
||||
}
|
||||
|
||||
func generateHooksFile(f *os.File, groupedHandlers map[string][]*RouteHandler, filenames []string) {
|
||||
|
||||
queryTemplate := `// export function use{handlerName}({props}) {
|
||||
// return useServerQuery{<}{TData}{TVar}{>}({
|
||||
// endpoint: API_ENDPOINTS.{groupName}.{handlerName}.endpoint{endpointSuffix},
|
||||
// method: API_ENDPOINTS.{groupName}.{handlerName}.methods[%d],
|
||||
// queryKey: [API_ENDPOINTS.{groupName}.{handlerName}.key],
|
||||
// enabled: true,
|
||||
// })
|
||||
// }
|
||||
|
||||
`
|
||||
mutationTemplate := `// export function use{handlerName}({props}) {
|
||||
// return useServerMutation{<}{TData}{TVar}{>}({
|
||||
// endpoint: API_ENDPOINTS.{groupName}.{handlerName}.endpoint{endpointSuffix},
|
||||
// method: API_ENDPOINTS.{groupName}.{handlerName}.methods[%d],
|
||||
// mutationKey: [API_ENDPOINTS.{groupName}.{handlerName}.key],
|
||||
// onSuccess: async () => {
|
||||
//
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
|
||||
`
|
||||
|
||||
tmpGroupTmpls := make(map[string][]string)
|
||||
|
||||
for _, filename := range filenames {
|
||||
routes := groupedHandlers[filename]
|
||||
if len(routes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if lo.EveryBy(routes, func(route *RouteHandler) bool {
|
||||
return route.Api == nil || len(route.Api.Methods) == 0
|
||||
}) {
|
||||
continue
|
||||
}
|
||||
|
||||
f.WriteString("//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n")
|
||||
f.WriteString(fmt.Sprintf("// %s\n", strings.TrimSuffix(filename, ".go")))
|
||||
f.WriteString("//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n\n")
|
||||
|
||||
tmpls := make([]string, 0)
|
||||
for _, route := range groupedHandlers[filename] {
|
||||
if route.Api == nil || len(route.Api.Methods) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for i, method := range route.Api.Methods {
|
||||
|
||||
tmpl := ""
|
||||
|
||||
if method == "GET" {
|
||||
getTemplate := strings.ReplaceAll(queryTemplate, "{handlerName}", strings.TrimPrefix(route.Name, "Handle"))
|
||||
getTemplate = strings.ReplaceAll(getTemplate, "{groupName}", strings.ToUpper(strings.TrimSuffix(filename, ".go")))
|
||||
getTemplate = strings.ReplaceAll(getTemplate, "{method}", "GET")
|
||||
tmpl = getTemplate
|
||||
}
|
||||
|
||||
if method == "POST" || method == "PATCH" || method == "PUT" || method == "DELETE" {
|
||||
mutTemplate := strings.ReplaceAll(mutationTemplate, "{handlerName}", strings.TrimPrefix(route.Name, "Handle"))
|
||||
mutTemplate = strings.ReplaceAll(mutTemplate, "{groupName}", strings.ToUpper(strings.TrimSuffix(filename, ".go")))
|
||||
mutTemplate = strings.ReplaceAll(mutTemplate, "{method}", method)
|
||||
tmpl = mutTemplate
|
||||
}
|
||||
|
||||
tmpl = strings.ReplaceAll(tmpl, "%d", strconv.Itoa(i))
|
||||
|
||||
if len(route.Api.ReturnTypescriptType) == 0 {
|
||||
tmpl = strings.ReplaceAll(tmpl, "{<}", "")
|
||||
tmpl = strings.ReplaceAll(tmpl, "{TData}", "")
|
||||
tmpl = strings.ReplaceAll(tmpl, "{TVar}", "")
|
||||
tmpl = strings.ReplaceAll(tmpl, "{>}", "")
|
||||
} else {
|
||||
tmpl = strings.ReplaceAll(tmpl, "{<}", "<")
|
||||
tmpl = strings.ReplaceAll(tmpl, "{TData}", route.Api.ReturnTypescriptType)
|
||||
tmpl = strings.ReplaceAll(tmpl, "{>}", ">")
|
||||
}
|
||||
|
||||
if len(route.Api.Params) == 0 {
|
||||
tmpl = strings.ReplaceAll(tmpl, "{endpointSuffix}", "")
|
||||
tmpl = strings.ReplaceAll(tmpl, "{props}", "")
|
||||
} else {
|
||||
props := ""
|
||||
for _, param := range route.Api.Params {
|
||||
props += fmt.Sprintf(`%s: %s, `, param.JsonName, param.TypescriptType)
|
||||
}
|
||||
tmpl = strings.ReplaceAll(tmpl, "{props}", props[:len(props)-2])
|
||||
endpointSuffix := ""
|
||||
for _, param := range route.Api.Params {
|
||||
endpointSuffix += fmt.Sprintf(`.replace("{%s}", String(%s))`, param.JsonName, param.JsonName)
|
||||
}
|
||||
tmpl = strings.ReplaceAll(tmpl, "{endpointSuffix}", endpointSuffix)
|
||||
}
|
||||
|
||||
if len(route.Api.BodyFields) == 0 {
|
||||
tmpl = strings.ReplaceAll(tmpl, "{TVar}", "")
|
||||
} else {
|
||||
tmpl = strings.ReplaceAll(tmpl, "{TVar}", fmt.Sprintf(", %s", strings.TrimPrefix(route.Name, "Handle")+"_Variables"))
|
||||
}
|
||||
|
||||
tmpls = append(tmpls, tmpl)
|
||||
f.WriteString(tmpl)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
tmpGroupTmpls[strings.TrimSuffix(filename, ".go")] = tmpls
|
||||
}
|
||||
|
||||
//for filename, tmpls := range tmpGroupTmpls {
|
||||
// hooksF, err := os.Create(filepath.Join("../seanime-web/src/api/hooks", filename+".hooks.ts"))
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
// defer hooksF.Close()
|
||||
//
|
||||
// for _, tmpl := range tmpls {
|
||||
// hooksF.WriteString(tmpl)
|
||||
// }
|
||||
//}
|
||||
|
||||
}
|
||||
|
||||
func generateEventFile(eventDir string, endpointsMap map[string]string) {
|
||||
fp := filepath.Join(eventDir, goEndpointsFileName)
|
||||
file, err := os.Create(fp)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
file.WriteString("// This code was generated by codegen/main.go. DO NOT EDIT.\n")
|
||||
file.WriteString("package events\n\n")
|
||||
|
||||
// file.WriteString(fmt"var Endpoint = map[string]string{\n")
|
||||
|
||||
endpoints := []string{}
|
||||
for endpoint := range endpointsMap {
|
||||
endpoints = append(endpoints, endpoint)
|
||||
}
|
||||
slices.SortStableFunc(endpoints, func(i, j string) int {
|
||||
return strings.Compare(i, j)
|
||||
})
|
||||
|
||||
goFmtSpacing := ""
|
||||
|
||||
file.WriteString("const (\n")
|
||||
for _, endpoint := range endpoints {
|
||||
file.WriteString(fmt.Sprintf(" %sEndpoint%s= \"%s\"\n", endpoint, goFmtSpacing, endpointsMap[endpoint]))
|
||||
}
|
||||
file.WriteString(")\n")
|
||||
|
||||
cmd := exec.Command("gofmt", "-w", fp)
|
||||
cmd.Run()
|
||||
|
||||
}
|
||||
|
||||
func writeParamField(f *os.File, handler *RouteHandler, param *RouteHandlerParam) {
|
||||
if len(param.Descriptions) > 0 {
|
||||
writeLine(f, "\t/**")
|
||||
for _, cmt := range param.Descriptions {
|
||||
writeLine(f, fmt.Sprintf("\t * %s", strings.TrimSpace(cmt)))
|
||||
}
|
||||
writeLine(f, "\t */")
|
||||
}
|
||||
fieldSuffix := ""
|
||||
if !param.Required {
|
||||
fieldSuffix = "?"
|
||||
}
|
||||
writeLine(f, fmt.Sprintf("\t%s%s: %s", param.JsonName, fieldSuffix, param.TypescriptType))
|
||||
}
|
||||
|
||||
func getEndpointKey(s string, groupName string) string {
|
||||
s = strings.TrimPrefix(s, "Handle")
|
||||
var result string
|
||||
for i, v := range s {
|
||||
if i > 0 && v >= 'A' && v <= 'Z' {
|
||||
result += "-"
|
||||
}
|
||||
|
||||
result += string(v)
|
||||
}
|
||||
result = strings.ToLower(result)
|
||||
if strings.Contains(result, "t-v-d-b") {
|
||||
result = strings.Replace(result, "t-v-d-b", "tvdb", 1)
|
||||
}
|
||||
if strings.Contains(result, "m-a-l") {
|
||||
result = strings.Replace(result, "m-a-l", "mal", 1)
|
||||
}
|
||||
return strings.ReplaceAll(groupName, "_", "-") + "-" + result
|
||||
}
|
||||
|
||||
func writeLine(file *os.File, template string) {
|
||||
template = strings.ReplaceAll(template, "\t", space)
|
||||
file.WriteString(fmt.Sprintf(template + "\n"))
|
||||
}
|
||||
334
seanime-2.9.10/codegen/internal/generate_types.go
Normal file
334
seanime-2.9.10/codegen/internal/generate_types.go
Normal file
@@ -0,0 +1,334 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
const (
|
||||
typescriptFileName = "types.ts"
|
||||
)
|
||||
|
||||
// Structs that are not directly referenced by the API routes but are needed for the Typescript file.
|
||||
var additionalStructNames = []string{
|
||||
"torrentstream.TorrentLoadingStatus",
|
||||
"torrentstream.TorrentStatus",
|
||||
"debrid_client.StreamState",
|
||||
"extension_repo.TrayPluginExtensionItem",
|
||||
"vendor_habari.Metadata",
|
||||
"nativeplayer.PlaybackInfo",
|
||||
"nativeplayer.ServerEvent",
|
||||
"nativeplayer.ClientEvent",
|
||||
"mkvparser.SubtitleEvent",
|
||||
"nakama.NakamaStatus",
|
||||
}
|
||||
|
||||
// GenerateTypescriptFile generates a Typescript file containing the types for the API routes parameters and responses based on the Docs struct.
|
||||
func GenerateTypescriptFile(docsFilePath string, publicStructsFilePath string, outDir string, goStructStrs []string) {
|
||||
|
||||
handlers := LoadHandlers(docsFilePath)
|
||||
|
||||
goStructs := LoadPublicStructs(publicStructsFilePath)
|
||||
|
||||
// e.g. map["models.User"]*GoStruct
|
||||
goStructsMap := make(map[string]*GoStruct)
|
||||
|
||||
for _, goStruct := range goStructs {
|
||||
goStructsMap[goStruct.Package+"."+goStruct.Name] = goStruct
|
||||
}
|
||||
|
||||
// Expand the structs with embedded structs
|
||||
for _, goStruct := range goStructs {
|
||||
for _, embeddedStructType := range goStruct.EmbeddedStructTypes {
|
||||
if embeddedStructType != "" {
|
||||
if usedStruct, ok := goStructsMap[embeddedStructType]; ok {
|
||||
for _, usedField := range usedStruct.Fields {
|
||||
goStruct.Fields = append(goStruct.Fields, usedField)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create the typescript file
|
||||
_ = os.MkdirAll(outDir, os.ModePerm)
|
||||
file, err := os.Create(filepath.Join(outDir, typescriptFileName))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Write the typescript file
|
||||
file.WriteString("// This code was generated by codegen/main.go. DO NOT EDIT.\n\n")
|
||||
|
||||
// Get all the returned structs from the routes
|
||||
// e.g. @returns models.User
|
||||
structStrMap := make(map[string]int)
|
||||
for _, str := range goStructStrs {
|
||||
if _, ok := structStrMap[str]; ok {
|
||||
structStrMap[str]++
|
||||
} else {
|
||||
structStrMap[str] = 1
|
||||
}
|
||||
}
|
||||
for _, handler := range handlers {
|
||||
if handler.Api != nil {
|
||||
switch handler.Api.ReturnTypescriptType {
|
||||
case "null", "string", "number", "boolean":
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := structStrMap[handler.Api.ReturnGoType]; ok {
|
||||
structStrMap[handler.Api.ReturnGoType]++
|
||||
} else {
|
||||
structStrMap[handler.Api.ReturnGoType] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Isolate the structs that are returned more than once
|
||||
sharedStructStrs := make([]string, 0)
|
||||
otherStructStrs := make([]string, 0)
|
||||
|
||||
for k, v := range structStrMap {
|
||||
if v > 1 {
|
||||
sharedStructStrs = append(sharedStructStrs, k)
|
||||
} else {
|
||||
otherStructStrs = append(otherStructStrs, k)
|
||||
}
|
||||
}
|
||||
|
||||
// Now that we have the returned structs, store them in slices
|
||||
sharedStructs := make([]*GoStruct, 0)
|
||||
otherStructs := make([]*GoStruct, 0)
|
||||
|
||||
for _, structStr := range sharedStructStrs {
|
||||
// e.g. "models.User"
|
||||
structStrParts := strings.Split(structStr, ".")
|
||||
if len(structStrParts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find the struct
|
||||
goStruct, ok := goStructsMap[structStr]
|
||||
if ok {
|
||||
sharedStructs = append(sharedStructs, goStruct)
|
||||
}
|
||||
|
||||
}
|
||||
for _, structStr := range otherStructStrs {
|
||||
// e.g. "models.User"
|
||||
structStrParts := strings.Split(structStr, ".")
|
||||
if len(structStrParts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find the struct
|
||||
goStruct, ok := goStructsMap[structStr]
|
||||
if ok {
|
||||
otherStructs = append(otherStructs, goStruct)
|
||||
}
|
||||
}
|
||||
|
||||
// Add additional structs to otherStructs
|
||||
for _, structName := range additionalStructNames {
|
||||
if goStruct, ok := goStructsMap[structName]; ok {
|
||||
otherStructs = append(otherStructs, goStruct)
|
||||
}
|
||||
}
|
||||
|
||||
//-------------------------
|
||||
|
||||
referencedStructs, ok := getReferencedStructsRecursively(sharedStructs, otherStructs, goStructsMap)
|
||||
if !ok {
|
||||
panic("Failed to get referenced structs")
|
||||
}
|
||||
|
||||
// Keep track of written Typescript types
|
||||
// This is to avoid name collisions
|
||||
writtenTypes := make(map[string]*GoStruct)
|
||||
|
||||
// Group the structs by package
|
||||
structsByPackage := make(map[string][]*GoStruct)
|
||||
for _, goStruct := range referencedStructs {
|
||||
if _, ok := structsByPackage[goStruct.Package]; !ok {
|
||||
structsByPackage[goStruct.Package] = make([]*GoStruct, 0)
|
||||
}
|
||||
structsByPackage[goStruct.Package] = append(structsByPackage[goStruct.Package], goStruct)
|
||||
}
|
||||
|
||||
packages := make([]string, 0)
|
||||
for k := range structsByPackage {
|
||||
packages = append(packages, k)
|
||||
}
|
||||
|
||||
slices.SortStableFunc(packages, func(i, j string) int {
|
||||
return cmp.Compare(i, j)
|
||||
})
|
||||
|
||||
file.WriteString("export type Nullish<T> = T | null | undefined\n\n")
|
||||
|
||||
for _, pkg := range packages {
|
||||
|
||||
file.WriteString("//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n")
|
||||
file.WriteString(fmt.Sprintf("// %s\n", strings.ReplaceAll(cases.Title(language.English, cases.Compact).String(strings.ReplaceAll(pkg, "_", " ")), " ", "")))
|
||||
file.WriteString("//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n\n")
|
||||
|
||||
structs := structsByPackage[pkg]
|
||||
slices.SortStableFunc(structs, func(i, j *GoStruct) int {
|
||||
return cmp.Compare(i.FormattedName, j.FormattedName)
|
||||
})
|
||||
|
||||
// Write the shared structs first
|
||||
for _, goStruct := range structs {
|
||||
|
||||
writeTypescriptType(file, goStruct, writtenTypes)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//for _, goStruct := range referencedStructs {
|
||||
//
|
||||
// writeTypescriptType(file, goStruct, writtenTypes)
|
||||
//
|
||||
//}
|
||||
|
||||
}
|
||||
|
||||
// getReferencedStructsRecursively returns a map of GoStructs that are referenced by the fields of sharedStructs and otherStructs.
|
||||
func getReferencedStructsRecursively(sharedStructs, otherStructs []*GoStruct, goStructsMap map[string]*GoStruct) (map[string]*GoStruct, bool) {
|
||||
allStructs := make(map[string]*GoStruct)
|
||||
for _, sharedStruct := range sharedStructs {
|
||||
allStructs[sharedStruct.Package+"."+sharedStruct.Name] = sharedStruct
|
||||
}
|
||||
for _, otherStruct := range otherStructs {
|
||||
allStructs[otherStruct.Package+"."+otherStruct.Name] = otherStruct
|
||||
}
|
||||
|
||||
// Keep track of the structs that have been visited
|
||||
|
||||
referencedStructs := make(map[string]*GoStruct)
|
||||
|
||||
for _, strct := range allStructs {
|
||||
getReferencedStructs(strct, referencedStructs, goStructsMap)
|
||||
}
|
||||
|
||||
return referencedStructs, true
|
||||
}
|
||||
|
||||
func getReferencedStructs(goStruct *GoStruct, referencedStructs map[string]*GoStruct, goStructsMap map[string]*GoStruct) {
|
||||
if _, ok := referencedStructs[goStruct.Package+"."+goStruct.Name]; ok {
|
||||
return
|
||||
}
|
||||
referencedStructs[goStruct.Package+"."+goStruct.Name] = goStruct
|
||||
for _, field := range goStruct.Fields {
|
||||
if field.UsedStructType != "" {
|
||||
if usedStruct, ok := goStructsMap[field.UsedStructType]; ok {
|
||||
getReferencedStructs(usedStruct, referencedStructs, goStructsMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
if goStruct.AliasOf != nil {
|
||||
if usedStruct, ok := goStructsMap[goStruct.AliasOf.UsedStructType]; ok {
|
||||
getReferencedStructs(usedStruct, referencedStructs, goStructsMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeTypescriptType(f *os.File, goStruct *GoStruct, writtenTypes map[string]*GoStruct) {
|
||||
f.WriteString("/**\n")
|
||||
f.WriteString(fmt.Sprintf(" * - Filepath: %s\n", strings.TrimPrefix(goStruct.Filepath, "../")))
|
||||
f.WriteString(fmt.Sprintf(" * - Filename: %s\n", goStruct.Filename))
|
||||
f.WriteString(fmt.Sprintf(" * - Package: %s\n", goStruct.Package))
|
||||
if len(goStruct.Comments) > 0 {
|
||||
f.WriteString(fmt.Sprintf(" * @description\n"))
|
||||
for _, cmt := range goStruct.Comments {
|
||||
f.WriteString(fmt.Sprintf(" * %s\n", strings.TrimSpace(cmt)))
|
||||
}
|
||||
}
|
||||
f.WriteString(" */\n")
|
||||
|
||||
if len(goStruct.Fields) > 0 {
|
||||
f.WriteString(fmt.Sprintf("export type %s = {\n", goStruct.FormattedName))
|
||||
for _, field := range goStruct.Fields {
|
||||
if field.JsonName == "" {
|
||||
continue
|
||||
}
|
||||
fieldNameSuffix := ""
|
||||
if !field.Required {
|
||||
fieldNameSuffix = "?"
|
||||
}
|
||||
|
||||
if len(field.Comments) > 0 {
|
||||
f.WriteString(fmt.Sprintf(" /**\n"))
|
||||
for _, cmt := range field.Comments {
|
||||
f.WriteString(fmt.Sprintf(" * %s\n", strings.TrimSpace(cmt)))
|
||||
}
|
||||
f.WriteString(fmt.Sprintf(" */\n"))
|
||||
}
|
||||
|
||||
typeText := field.TypescriptType
|
||||
//if !field.Required {
|
||||
// switch typeText {
|
||||
// case "string", "number", "boolean":
|
||||
// default:
|
||||
// typeText = "Nullish<" + typeText + ">"
|
||||
// }
|
||||
//}
|
||||
|
||||
f.WriteString(fmt.Sprintf(" %s%s: %s\n", field.JsonName, fieldNameSuffix, typeText))
|
||||
}
|
||||
f.WriteString("}\n\n")
|
||||
}
|
||||
|
||||
if goStruct.AliasOf != nil {
|
||||
if goStruct.AliasOf.DeclaredValues != nil && len(goStruct.AliasOf.DeclaredValues) > 0 {
|
||||
union := ""
|
||||
if len(goStruct.AliasOf.DeclaredValues) > 5 {
|
||||
union = strings.Join(goStruct.AliasOf.DeclaredValues, " |\n ")
|
||||
} else {
|
||||
union = strings.Join(goStruct.AliasOf.DeclaredValues, " | ")
|
||||
}
|
||||
f.WriteString(fmt.Sprintf("export type %s = %s\n\n", goStruct.FormattedName, union))
|
||||
} else {
|
||||
f.WriteString(fmt.Sprintf("export type %s = %s\n\n", goStruct.FormattedName, goStruct.AliasOf.TypescriptType))
|
||||
}
|
||||
}
|
||||
|
||||
// Add the struct to the written types
|
||||
writtenTypes[goStruct.Package+"."+goStruct.Name] = goStruct
|
||||
}
|
||||
|
||||
func getUnformattedGoType(goType string) string {
|
||||
if strings.HasPrefix(goType, "[]") {
|
||||
return getUnformattedGoType(goType[2:])
|
||||
}
|
||||
|
||||
if strings.HasPrefix(goType, "*") {
|
||||
return getUnformattedGoType(goType[1:])
|
||||
}
|
||||
|
||||
if strings.HasPrefix(goType, "map[") {
|
||||
s := strings.TrimPrefix(goType, "map[")
|
||||
value := ""
|
||||
for i, c := range s {
|
||||
if c == ']' {
|
||||
value = s[i+1:]
|
||||
break
|
||||
}
|
||||
}
|
||||
return getUnformattedGoType(value)
|
||||
}
|
||||
|
||||
return goType
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
34
seanime-2.9.10/codegen/internal/loaders.go
Normal file
34
seanime-2.9.10/codegen/internal/loaders.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
)
|
||||
|
||||
func LoadHandlers(path string) []*RouteHandler {
|
||||
var handlers []*RouteHandler
|
||||
docsContent, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = json.Unmarshal(docsContent, &handlers)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return handlers
|
||||
}
|
||||
|
||||
func LoadPublicStructs(path string) []*GoStruct {
|
||||
var goStructs []*GoStruct
|
||||
structsContent, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(structsContent, &goStructs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return goStructs
|
||||
}
|
||||
Reference in New Issue
Block a user