466 lines
14 KiB
Go
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"))
|
|
}
|