feat: generate ts typings from golang code

This commit is contained in:
Siyuan Miao 2025-09-11 01:40:42 +02:00
parent c8dd84c6b7
commit f8c2a95381
6 changed files with 1193 additions and 0 deletions

View File

@ -1049,6 +1049,24 @@ func rpcSetLocalLoopbackOnly(enabled bool) error {
return nil return nil
} }
// JSONRPCHandler represents a JSON-RPC handler
type JSONRPCHandler struct {
Type reflect.Type
Params []string
}
// GetJSONRPCHandlers returns the JSON-RPC handlers
func GetJSONRPCHandlers() map[string]JSONRPCHandler {
ret := make(map[string]JSONRPCHandler)
for name, handler := range rpcHandlers {
ret[name] = JSONRPCHandler{
Type: reflect.ValueOf(handler.Func).Type(),
Params: handler.Params,
}
}
return ret
}
var rpcHandlers = map[string]RPCHandler{ var rpcHandlers = map[string]RPCHandler{
"ping": {Func: rpcPing}, "ping": {Func: rpcPing},
"reboot": {Func: rpcReboot, Params: []string{"force"}}, "reboot": {Func: rpcReboot, Params: []string{"force"}},

View File

@ -0,0 +1,334 @@
package main
import (
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"strings"
"github.com/rs/zerolog/log"
)
// getStringAliasInfoWithReflection uses reflection to automatically detect constants
// This approach tries to find constants by examining the actual values
func getStringAliasInfoWithReflection(searchPath string) []StringAliasInfo {
log.Debug().Str("searchPath", searchPath).Msg("Detecting string aliases and constants in single pass")
// Detect both string aliases and their constants in a single file system walk
result := detectStringAliasesWithConstants(searchPath)
// If reflection didn't work, throw an error
if len(result) == 0 {
log.Fatal().Msg("No string aliases with constants could be detected. Make sure the types are defined with constants in Go files.")
}
log.Debug().Int("detected", len(result)).Msg("String alias detection completed")
return result
}
// detectStringAliasesWithConstants detects both string aliases and their constants in a single file system walk
func detectStringAliasesWithConstants(searchPath string) []StringAliasInfo {
var result []StringAliasInfo
// Walk the specified directory to find Go files
err := filepath.Walk(searchPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Skip directories and non-Go files
if info.IsDir() || !strings.HasSuffix(path, ".go") {
return nil
}
// Skip test files and our own tool files
if strings.Contains(path, "_test.go") || strings.Contains(path, "scripts/jsonrpc_typings") {
return nil
}
// Parse the file to find both string aliases and their constants
aliases := findStringAliasesWithConstantsInFile(path)
result = append(result, aliases...)
return nil
})
if err != nil {
log.Fatal().Err(err).Msg("Error walking directory for string alias detection")
}
// Remove duplicates based on type name
uniqueAliases := make([]StringAliasInfo, 0)
seen := make(map[string]bool)
for _, alias := range result {
if !seen[alias.Name] {
seen[alias.Name] = true
uniqueAliases = append(uniqueAliases, alias)
}
}
return uniqueAliases
}
// findStringAliasesWithConstantsInFile finds both string aliases and their constants in a single Go file
func findStringAliasesWithConstantsInFile(filePath string) []StringAliasInfo {
var result []StringAliasInfo
// Parse the Go file
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
if err != nil {
log.Debug().Err(err).Str("file", filePath).Msg("Failed to parse file")
return result
}
// First pass: collect all string alias type names
stringAliases := make(map[string]bool)
ast.Inspect(node, func(n ast.Node) bool {
genDecl, ok := n.(*ast.GenDecl)
if !ok || genDecl.Tok != token.TYPE {
return true
}
for _, spec := range genDecl.Specs {
typeSpec, ok := spec.(*ast.TypeSpec)
if !ok {
continue
}
// Check if this is a string alias (type Name string)
if ident, ok := typeSpec.Type.(*ast.Ident); ok && ident.Name == "string" {
stringAliases[typeSpec.Name.Name] = true
}
}
return true
})
// Second pass: find constants for the string aliases we found
ast.Inspect(node, func(n ast.Node) bool {
genDecl, ok := n.(*ast.GenDecl)
if !ok || genDecl.Tok != token.CONST {
return true
}
// Process each constant specification in the declaration
for _, spec := range genDecl.Specs {
valueSpec, ok := spec.(*ast.ValueSpec)
if !ok {
continue
}
// Check if this constant is typed with one of our string aliases
if valueSpec.Type == nil {
continue
}
ident, ok := valueSpec.Type.(*ast.Ident)
if !ok {
continue
}
typeName := ident.Name
// Check if this type is one of our string aliases
if _, ok := stringAliases[typeName]; !ok {
continue
}
// Extract string literal values
for _, value := range valueSpec.Values {
basicLit, ok := value.(*ast.BasicLit)
if !ok || basicLit.Kind != token.STRING {
continue
}
// Remove quotes from string literal
constantValue := strings.Trim(basicLit.Value, "\"")
// Find or create the StringAliasInfo for this type
var aliasInfo *StringAliasInfo
for i := range result {
if result[i].Name == typeName {
aliasInfo = &result[i]
break
}
}
if aliasInfo == nil {
result = append(result, StringAliasInfo{
Name: typeName,
Constants: []string{},
})
aliasInfo = &result[len(result)-1]
}
aliasInfo.Constants = append(aliasInfo.Constants, constantValue)
}
}
return true
})
return result
}
// batchDetectConstantsForTypes efficiently detects constants for multiple types in a single file system walk
func batchDetectConstantsForTypes(typeNames []string, searchPath string) map[string][]string {
result := make(map[string][]string)
// Initialize result map
for _, typeName := range typeNames {
result[typeName] = []string{}
}
// Walk the specified directory to find Go files
err := filepath.Walk(searchPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Skip directories and non-Go files
if info.IsDir() || !strings.HasSuffix(path, ".go") {
return nil
}
// Skip test files and our own tool files
if strings.Contains(path, "_test.go") || strings.Contains(path, "scripts/jsonrpc_typings") {
return nil
}
// Check if this file contains any of the types we're looking for
fileContainsAnyType := false
for _, typeName := range typeNames {
if fileContainsType(path, typeName) {
fileContainsAnyType = true
break
}
}
if !fileContainsAnyType {
return nil
}
log.Debug().Str("file", path).Strs("types", typeNames).Msg("Parsing file for constants")
// Parse constants for all types from this file
fileConstants := batchParseConstantsFromFile(path, typeNames)
// Merge results
for typeName, constants := range fileConstants {
if len(constants) > 0 {
result[typeName] = constants
log.Debug().Str("type", typeName).Strs("constants", constants).Str("file", path).Msg("Found constants")
}
}
return nil
})
if err != nil {
log.Fatal().Err(err).Msg("Error searching for constants")
}
return result
}
// batchParseConstantsFromFile parses constants for multiple types from a single Go file
func batchParseConstantsFromFile(filePath string, typeNames []string) map[string][]string {
result := make(map[string][]string)
// Initialize result map
for _, typeName := range typeNames {
result[typeName] = []string{}
}
// Parse the Go file
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
if err != nil {
log.Debug().Err(err).Str("file", filePath).Msg("Failed to parse file")
return result
}
// Walk the AST to find constant declarations
ast.Inspect(node, func(n ast.Node) bool {
// Look for GenDecl nodes (const declarations)
genDecl, ok := n.(*ast.GenDecl)
if !ok || genDecl.Tok != token.CONST {
return true
}
// Process each constant specification in the declaration
for _, spec := range genDecl.Specs {
valueSpec, ok := spec.(*ast.ValueSpec)
if !ok {
continue
}
// Check if this constant is typed with one of our target types
if valueSpec.Type != nil {
if ident, ok := valueSpec.Type.(*ast.Ident); ok {
typeName := ident.Name
// Check if this type is one we're looking for
if contains(typeNames, typeName) {
// Extract string literal values
for _, value := range valueSpec.Values {
if basicLit, ok := value.(*ast.BasicLit); ok && basicLit.Kind == token.STRING {
// Remove quotes from string literal
constantValue := strings.Trim(basicLit.Value, "\"")
result[typeName] = append(result[typeName], constantValue)
}
}
}
}
}
}
return true
})
return result
}
// contains checks if a slice contains a string
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
// fileContainsType checks if a Go file contains a type definition for the given type name
func fileContainsType(filePath, typeName string) bool {
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
if err != nil {
return false
}
// Walk the AST to find type definitions
found := false
ast.Inspect(node, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.GenDecl:
if x.Tok == token.TYPE {
for _, spec := range x.Specs {
if typeSpec, ok := spec.(*ast.TypeSpec); ok {
if typeSpec.Name.Name == typeName {
found = true
return false // Stop searching
}
}
}
}
}
return !found // Continue searching if not found yet
})
return found
}

View File

@ -0,0 +1,45 @@
package main
import (
"flag"
"os"
"strings"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
// Parse command line flags
logLevel := flag.String("log-level", "info", "Log level (trace, debug, info, warn, error, fatal, panic)")
searchPath := flag.String("search-path", ".", "Path to search for Go files containing type definitions")
outputPath := flag.String("output", "jsonrpc.ts", "Output path for the generated TypeScript file")
flag.Parse()
// Set log level
level, err := zerolog.ParseLevel(strings.ToLower(*logLevel))
if err != nil {
log.Fatal().Err(err).Str("level", *logLevel).Msg("Invalid log level")
}
zerolog.SetGlobalLevel(level)
// Configure zerolog for pretty console output
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
// Create API schema
log.Info().Str("search-path", *searchPath).Msg("Creating API schema from JSON-RPC handlers")
schema := NewAPISchema(*searchPath)
// Generate TypeScript typings
log.Info().Msg("Generating TypeScript typings")
typings := generateTypeScriptTypings(schema, *searchPath)
// Write to output file
log.Info().Str("file", *outputPath).Msg("Writing TypeScript definitions to file")
err = os.WriteFile(*outputPath, []byte(typings), 0644)
if err != nil {
log.Fatal().Err(err).Str("file", *outputPath).Msg("Failed to write TypeScript definitions")
}
log.Info().Str("file", *outputPath).Msg("TypeScript typings generated successfully")
}

View File

@ -0,0 +1,406 @@
package main
import (
"fmt"
"reflect"
"sort"
"strings"
"github.com/jetkvm/kvm"
"github.com/rs/zerolog/log"
)
// NewAPISchema creates a new API schema from the JSON-RPC handlers
func NewAPISchema(searchPath string) *APISchema {
schema := &APISchema{
Handlers: make(map[string]APIHandler),
Types: make(map[string]APIType),
}
handlers := kvm.GetJSONRPCHandlers()
log.Info().Int("count", len(handlers)).Msg("Processing JSON-RPC handlers")
for name, handler := range handlers {
log.Debug().Str("handler", name).Msg("Building API handler")
apiHandler := buildAPIHandler(name, handler, schema)
schema.Handlers[name] = apiHandler
}
schema.HandlerCount = len(schema.Handlers)
schema.TypeCount = len(schema.Types)
log.Info().
Int("handlers", schema.HandlerCount).
Int("types", schema.TypeCount).
Msg("API schema created successfully")
return schema
}
// buildAPIHandler constructs an APIHandler from a JSON-RPC handler
func buildAPIHandler(name string, handler kvm.JSONRPCHandler, schema *APISchema) APIHandler {
apiHandler := APIHandler{
Name: name,
FunctionType: handler.Type.String(),
ParameterNames: handler.Params,
Parameters: make([]APIParameter, 0, handler.Type.NumIn()),
ReturnValues: make([]APIReturnValue, 0, handler.Type.NumOut()),
}
// Process parameters
for i := 0; i < handler.Type.NumIn(); i++ {
paramType := handler.Type.In(i)
paramName := getParameterName(i, handler.Params)
apiParam := APIParameter{
Name: paramName,
Type: paramType.String(),
}
if apiType := extractAPIType(paramType, schema); apiType != nil {
apiParam.APIType = apiType
schema.Types[apiType.Name] = *apiType
}
apiHandler.Parameters = append(apiHandler.Parameters, apiParam)
}
// Process return values
for i := 0; i < handler.Type.NumOut(); i++ {
returnType := handler.Type.Out(i)
apiReturn := APIReturnValue{
Index: i,
Type: returnType.String(),
}
if apiType := extractAPIType(returnType, schema); apiType != nil {
apiReturn.APIType = apiType
schema.Types[apiType.Name] = *apiType
}
apiHandler.ReturnValues = append(apiHandler.ReturnValues, apiReturn)
}
return apiHandler
}
// getParameterName safely retrieves a parameter name by index
func getParameterName(index int, paramNames []string) string {
if index < len(paramNames) {
return paramNames[index]
}
return ""
}
// extractAPIType extracts API type information from a reflect.Type
// It recursively finds and adds nested struct types to the schema
func extractAPIType(t reflect.Type, schema *APISchema) *APIType {
if t == nil {
return nil
}
switch t.Kind() {
case reflect.Ptr:
return extractPointerType(t, schema)
case reflect.Slice:
return extractSliceType(t, schema)
case reflect.Array:
return extractArrayType(t, schema)
case reflect.Map:
return extractMapType(t, schema)
case reflect.Struct:
return extractStructType(t, schema)
case reflect.Interface:
return extractInterfaceType(t)
default:
return extractBasicType(t)
}
}
// extractPointerType handles pointer types
func extractPointerType(t reflect.Type, schema *APISchema) *APIType {
elemType := extractAPIType(t.Elem(), schema)
if elemType == nil {
return nil
}
elemType.IsPointer = true
elemType.Name = "*" + elemType.Name
return elemType
}
// extractSliceType handles slice types
func extractSliceType(t reflect.Type, schema *APISchema) *APIType {
elemType := extractAPIType(t.Elem(), schema)
if elemType == nil {
return nil
}
elemType.IsSlice = true
elemType.SliceType = elemType.Name
elemType.Name = "[]" + elemType.Name
return elemType
}
// extractArrayType handles array types
func extractArrayType(t reflect.Type, schema *APISchema) *APIType {
elemType := extractAPIType(t.Elem(), schema)
if elemType == nil {
return nil
}
elemType.Name = fmt.Sprintf("[%d]%s", t.Len(), elemType.Name)
return elemType
}
// extractMapType handles map types
func extractMapType(t reflect.Type, schema *APISchema) *APIType {
keyType := extractAPIType(t.Key(), schema)
valueType := extractAPIType(t.Elem(), schema)
if keyType == nil || valueType == nil {
return nil
}
return &APIType{
Name: fmt.Sprintf("map[%s]%s", keyType.Name, valueType.Name),
Kind: TypeKindMap,
IsMap: true,
MapKeyType: keyType.Name,
MapValueType: valueType.Name,
}
}
// extractStructType handles struct types
func extractStructType(t reflect.Type, schema *APISchema) *APIType {
apiType := &APIType{
Name: t.String(),
Kind: TypeKindStruct,
Fields: make([]APIField, 0, t.NumField()),
}
// Extract package name
if pkgPath := t.PkgPath(); pkgPath != "" {
apiType.Package = extractPackageName(pkgPath)
}
// Extract fields
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
// Skip unexported fields
if !field.IsExported() {
continue
}
apiField := buildAPIField(field)
apiType.Fields = append(apiType.Fields, apiField)
// Recursively extract nested struct types from field types
if nestedType := extractAPIType(field.Type, schema); nestedType != nil {
if nestedType.Kind == TypeKindStruct {
schema.Types[nestedType.Name] = *nestedType
}
}
}
return apiType
}
// extractInterfaceType handles interface types
func extractInterfaceType(t reflect.Type) *APIType {
return &APIType{
Name: t.String(),
Kind: TypeKindInterface,
}
}
// extractBasicType handles basic Go types
func extractBasicType(t reflect.Type) *APIType {
if isBasicType(t.String()) {
return &APIType{
Name: t.String(),
Kind: TypeKindBasic,
}
}
return nil
}
// buildAPIField constructs an APIField from a reflect.StructField
func buildAPIField(field reflect.StructField) APIField {
apiField := APIField{
Name: field.Name,
JSONName: field.Name, // Default to field name
Type: field.Type.String(),
IsExported: field.IsExported(),
Tag: string(field.Tag),
}
// Parse JSON tag
if jsonTag := field.Tag.Get("json"); jsonTag != "" {
if jsonName := parseJSONTag(jsonTag); jsonName != "" {
apiField.JSONName = jsonName
}
}
return apiField
}
// parseJSONTag extracts the JSON field name from a JSON tag
func parseJSONTag(jsonTag string) string {
parts := strings.Split(jsonTag, ",")
if len(parts) > 0 && parts[0] != "" && parts[0] != "-" {
return parts[0]
}
return ""
}
// extractPackageName extracts the package name from a package path
func extractPackageName(pkgPath string) string {
parts := strings.Split(pkgPath, "/")
if len(parts) > 0 {
return parts[len(parts)-1]
}
return ""
}
// GetStructTypes returns all struct types from the schema
func (s *APISchema) GetStructTypes() []APIType {
structs := make([]APIType, 0)
for _, apiType := range s.Types {
if apiType.Kind == TypeKindStruct {
structs = append(structs, apiType)
}
}
return structs
}
// getSortedHandlers returns handlers sorted by name
func getSortedHandlers(schema *APISchema) []APIHandler {
var handlers []APIHandler
for _, handler := range schema.Handlers {
handlers = append(handlers, handler)
}
sort.Slice(handlers, func(i, j int) bool {
return handlers[i].Name < handlers[j].Name
})
return handlers
}
// getSortedMethods returns method names sorted alphabetically
func getSortedMethods(schema *APISchema) []string {
var methods []string
for name := range schema.Handlers {
methods = append(methods, name)
}
sort.Strings(methods)
return methods
}
// getAllReferencedStructs recursively finds all structs referenced in the API
func getAllReferencedStructs(schema *APISchema) []APIType {
// Start with all structs found in handlers
allStructs := make(map[string]APIType)
// Add all structs from handlers
for _, apiType := range schema.GetStructTypes() {
allStructs[apiType.Name] = apiType
}
// Also add all structs from the complete schema that might be referenced
for name, apiType := range schema.Types {
if apiType.Kind == TypeKindStruct {
allStructs[name] = apiType
}
}
// Recursively find all referenced structs
changed := true
for changed {
changed = false
for _, apiType := range allStructs {
referencedStructs := findReferencedStructs(apiType, schema)
for _, refStruct := range referencedStructs {
if _, exists := allStructs[refStruct.Name]; !exists {
allStructs[refStruct.Name] = refStruct
changed = true
}
}
}
}
// Convert map to slice
var result []APIType
for _, apiType := range allStructs {
result = append(result, apiType)
}
return result
}
// findReferencedStructs finds structs referenced in a given API type
func findReferencedStructs(apiType APIType, schema *APISchema) []APIType {
var referenced []APIType
for _, field := range apiType.Fields {
if isStructType(field.Type) {
structName := extractStructName(field.Type)
if structType, exists := schema.Types[structName]; exists {
referenced = append(referenced, structType)
}
}
}
return referenced
}
// isStructType checks if a type string represents a struct
func isStructType(typeStr string) bool {
// Check if it's a custom type (not basic Go types)
return !isBasicType(typeStr) &&
!strings.HasPrefix(typeStr, "[]") &&
!strings.HasPrefix(typeStr, "map[") &&
!strings.HasPrefix(typeStr, "*") &&
!strings.HasPrefix(typeStr, "[")
}
// extractStructName extracts the struct name from a type string
func extractStructName(typeStr string) string {
// Remove array/slice prefixes
if strings.HasPrefix(typeStr, "[]") {
return typeStr[2:]
}
if strings.HasPrefix(typeStr, "*") {
return typeStr[1:]
}
return typeStr
}
// isBasicType checks if a type is a basic Go type
func isBasicType(typeName string) bool {
basicTypes := map[string]bool{
"bool": true,
"string": true,
"int": true,
"int8": true,
"int16": true,
"int32": true,
"int64": true,
"uint": true,
"uint8": true,
"uint16": true,
"uint32": true,
"uint64": true,
"uintptr": true,
"float32": true,
"float64": true,
"complex64": true,
"complex128": true,
"byte": true,
"rune": true,
"error": true,
}
return basicTypes[typeName]
}

View File

@ -0,0 +1,81 @@
package main
// TypeKind represents the kind of API type
type TypeKind string
const (
// TypeKindStruct represents a struct type
TypeKindStruct TypeKind = "struct"
// TypeKindInterface represents an interface type
TypeKindInterface TypeKind = "interface"
// TypeKindBasic represents a basic Go type
TypeKindBasic TypeKind = "basic"
// TypeKindMap represents a map type
TypeKindMap TypeKind = "map"
// TypeKindSlice represents a slice type
TypeKindSlice TypeKind = "slice"
// TypeKindArray represents an array type
TypeKindArray TypeKind = "array"
// TypeKindPointer represents a pointer type
TypeKindPointer TypeKind = "pointer"
)
// APIType represents a type used in the JSON-RPC API
type APIType struct {
Name string `json:"name"`
Package string `json:"package"`
Kind TypeKind `json:"kind"`
Fields []APIField `json:"fields,omitempty"`
IsPointer bool `json:"is_pointer"`
IsSlice bool `json:"is_slice"`
IsMap bool `json:"is_map"`
MapKeyType string `json:"map_key_type,omitempty"`
MapValueType string `json:"map_value_type,omitempty"`
SliceType string `json:"slice_type,omitempty"`
}
// APIField represents a field in a struct
type APIField struct {
Name string `json:"name"`
JSONName string `json:"json_name"`
Type string `json:"type"`
IsExported bool `json:"is_exported"`
Tag string `json:"tag"`
}
// APIParameter represents a parameter in a JSON-RPC handler
type APIParameter struct {
Name string `json:"name"`
Type string `json:"type"`
APIType *APIType `json:"api_type,omitempty"`
}
// APIReturnValue represents a return value from a JSON-RPC handler
type APIReturnValue struct {
Index int `json:"index"`
Type string `json:"type"`
APIType *APIType `json:"api_type,omitempty"`
}
// APIHandler represents a complete JSON-RPC handler
type APIHandler struct {
Name string `json:"name"`
FunctionType string `json:"function_type"`
Parameters []APIParameter `json:"parameters"`
ReturnValues []APIReturnValue `json:"return_values"`
ParameterNames []string `json:"parameter_names"`
}
// APISchema represents the complete JSON-RPC API schema
type APISchema struct {
Handlers map[string]APIHandler `json:"handlers"`
Types map[string]APIType `json:"types"`
TypeCount int `json:"type_count"`
HandlerCount int `json:"handler_count"`
}
// StringAliasInfo represents information about a string alias and its constants
type StringAliasInfo struct {
Name string
Constants []string
}

View File

@ -0,0 +1,309 @@
package main
import (
"fmt"
"strings"
"text/template"
"github.com/rs/zerolog/log"
)
// generateTypeScriptTypings generates complete TypeScript definitions
func generateTypeScriptTypings(schema *APISchema, searchPath string) string {
// Create template functions
funcMap := template.FuncMap{
"cleanTypeName": cleanTypeName,
"goToTypeScriptType": goToTypeScriptType,
"getAllStructs": func() []APIType { return getAllReferencedStructs(schema) },
"getSortedHandlers": func() []APIHandler { return getSortedHandlers(schema) },
"getSortedMethods": func() []string { return getSortedMethods(schema) },
"getReturnType": getReturnType,
"getParameterList": getParameterList,
"hasParameters": hasParameters,
"getStringAliasInfo": func() []StringAliasInfo { return getStringAliasInfoWithReflection(searchPath) },
"sub": func(a, b int) int { return a - b },
"pad": func(s string, width int) string { return padString(s, width) },
"padComment": func(fieldName, fieldType string) string { return padComment(fieldName, fieldType) },
}
// Parse the main template
tmpl, err := template.New("typescript").Funcs(funcMap).Parse(typescriptTemplate)
if err != nil {
log.Fatal().Err(err).Msg("Failed to parse TypeScript template")
}
// Execute template
var output strings.Builder
err = tmpl.Execute(&output, schema)
if err != nil {
log.Fatal().Err(err).Msg("Failed to execute TypeScript template")
}
return output.String()
}
// padString pads a string to the specified width
func padString(s string, width int) string {
if len(s) >= width {
return s
}
return s + strings.Repeat(" ", width-len(s))
}
// padComment calculates the proper padding for field comments
func padComment(fieldName, fieldType string) string {
// Calculate the length of the field declaration part
// Format: " fieldName: fieldType;"
declarationLength := 2 + len(fieldName) + 2 + len(fieldType) + 1 // " " + fieldName + ": " + fieldType + ";"
// Target alignment at column 40
targetColumn := 40
if declarationLength >= targetColumn {
return " " // Just one space if already past target
}
return strings.Repeat(" ", targetColumn-declarationLength)
}
// cleanTypeName cleans up Go type names for TypeScript
func cleanTypeName(typeName string) string {
// Remove package prefixes
if strings.Contains(typeName, ".") {
parts := strings.Split(typeName, ".")
return parts[len(parts)-1]
}
return typeName
}
// goToTypeScriptType converts Go types to TypeScript types with recursive parsing
func goToTypeScriptType(goType string) string {
return parseTypeRecursively(goType)
}
// parseTypeRecursively parses Go types recursively to handle complex nested types
func parseTypeRecursively(goType string) string {
// Remove any leading/trailing whitespace
goType = strings.TrimSpace(goType)
// Handle basic types first
switch goType {
case "bool":
return "boolean"
case "string":
return "string"
case "int", "int8", "int16", "int32", "int64":
return "number"
case "uint", "uint8", "uint16", "uint32", "uint64":
return "number"
case "float32", "float64":
return "number"
case "byte":
return "number"
case "rune":
return "string"
case "error":
return "string"
case "usbgadget.ByteSlice":
return "number[]"
case "interface {}":
return "any"
case "time.Duration":
return "number"
case "time.Time":
return "string"
case "net.IP":
return "string"
case "net.IPNet":
return "string"
case "net.HardwareAddr":
return "string"
}
// Handle pointer types
if strings.HasPrefix(goType, "*") {
innerType := parseTypeRecursively(goType[1:])
return innerType + " | null"
}
// Handle slice types
if strings.HasPrefix(goType, "[]") {
elementType := parseTypeRecursively(goType[2:])
return elementType + "[]"
}
// Handle map types with proper bracket matching
if strings.HasPrefix(goType, "map[") {
return parseMapType(goType)
}
// Handle any remaining interface {} in complex types
if strings.Contains(goType, "interface {}") {
return strings.ReplaceAll(goType, "interface {}", "any")
}
// Check if this is a string alias (type name != underlying type)
if isStringAlias(goType) {
return cleanTypeName(goType)
}
// Return cleaned custom type name
return cleanTypeName(goType)
}
// parseMapType parses map types with proper bracket matching
func parseMapType(goType string) string {
if !strings.HasPrefix(goType, "map[") {
return goType
}
// Find the key type and value type
start := 4 // After "map["
bracketCount := 0
keyEnd := -1
// Find the end of the key type by looking for the first ']' at bracket level 0
for i := start; i < len(goType); i++ {
char := goType[i]
if char == '[' {
bracketCount++
} else if char == ']' {
if bracketCount == 0 {
keyEnd = i
break
}
bracketCount--
}
}
if keyEnd == -1 || keyEnd >= len(goType)-1 {
return goType // Invalid map type
}
keyType := goType[start:keyEnd]
valueType := goType[keyEnd+1:]
// Parse key and value types recursively
tsKeyType := parseTypeRecursively(keyType)
tsValueType := parseTypeRecursively(valueType)
return fmt.Sprintf("Record<%s, %s>", tsKeyType, tsValueType)
}
// isStringAlias checks if a type is a string alias
func isStringAlias(typeName string) bool {
// Known string aliases in the codebase
stringAliases := map[string]bool{
"VirtualMediaMode": true,
"VirtualMediaSource": true,
}
return stringAliases[typeName]
}
// getReturnType returns the TypeScript return type for a handler
func getReturnType(handler APIHandler) string {
if len(handler.ReturnValues) == 0 {
return "void"
} else if len(handler.ReturnValues) == 1 {
return goToTypeScriptType(handler.ReturnValues[0].Type)
} else {
// Multiple return values - use tuple type
var returnTypes []string
for _, retVal := range handler.ReturnValues {
returnTypes = append(returnTypes, goToTypeScriptType(retVal.Type))
}
return fmt.Sprintf("[%s]", strings.Join(returnTypes, ", "))
}
}
// getParameterList returns the TypeScript parameter list for a handler
func getParameterList(handler APIHandler) string {
if len(handler.Parameters) == 0 {
return ""
}
var paramList []string
for _, param := range handler.Parameters {
tsType := goToTypeScriptType(param.Type)
paramList = append(paramList, fmt.Sprintf("%s: %s", param.Name, tsType))
}
return strings.Join(paramList, ", ")
}
// hasParameters returns true if the handler has parameters
func hasParameters(handler APIHandler) bool {
return len(handler.Parameters) > 0
}
// typescriptTemplate is the main template for generating TypeScript definitions
const typescriptTemplate = `// Code generated by generate_typings.go. DO NOT EDIT.
{{range $struct := getAllStructs}}
export interface {{cleanTypeName $struct.Name}} {
{{range $field := $struct.Fields}} {{$field.JSONName}}: {{goToTypeScriptType $field.Type}};{{padComment $field.JSONName (goToTypeScriptType $field.Type)}}// {{$field.Name}} {{$field.Type}}
{{end}}}
{{end}}
// String aliases with constants
{{range $alias := getStringAliasInfo}}
export type {{$alias.Name}} = {{range $i, $const := $alias.Constants}}"{{$const}}"{{if lt $i (sub (len $alias.Constants) 1)}} | {{end}}{{end}};
{{end}}
// JSON-RPC Types
export interface JsonRpcRequest {
jsonrpc: "2.0";
method: string;
params?: unknown;
id: number | string;
}
export interface JsonRpcError {
code: number;
data?: string;
message: string;
}
export interface JsonRpcSuccessResponse {
jsonrpc: "2.0";
result: unknown;
id: number | string;
}
export interface JsonRpcErrorResponse {
jsonrpc: "2.0";
error: JsonRpcError;
id: number | string;
}
export type JsonRpcResponse = JsonRpcSuccessResponse | JsonRpcErrorResponse;
// Handler method types (generated from actual handlers)
export type JsonRpcMethod =
{{range $i, $method := getSortedMethods}}{{if $i}} | {{else}} | {{end}}"{{$method}}"
{{end}};
// RPC Functions
export class JsonRpcClient {
constructor(private send: (method: string, params: unknown, callback?: (resp: JsonRpcResponse) => void) => void) {}
private async sendAsync<T>(method: string, params?: unknown): Promise<T> {
return new Promise<T>((resolve, reject) => {
this.send(method, params, (response: JsonRpcResponse) => {
if ('error' in response) {
reject(new Error('RPC error: ' + response.error.message));
return;
}
resolve(response.result as T);
});
});
}
{{range $handler := getSortedHandlers}}
async {{$handler.Name}}({{getParameterList $handler}}) {
{{if eq (len $handler.Parameters) 0}} return this.sendAsync<{{getReturnType $handler}}>('{{$handler.Name}}');
{{else}} return this.sendAsync<{{getReturnType $handler}}>('{{$handler.Name}}', {
{{range $param := $handler.Parameters}} {{$param.Name}},
{{end}} });
{{end}} }
{{end}}}
`