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

466 lines
14 KiB
Go

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