node build fixed

This commit is contained in:
ra_ma
2025-09-20 14:08:38 +01:00
parent c6ebbe069d
commit 3d298fa434
1516 changed files with 535727 additions and 2 deletions

View 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"`
}

View 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
}

View 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, "_", " ")), " ", "")
}

View 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")
}
}

View 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
}

View 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)
}

View 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"))
}

View 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
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

View 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
}