fix: handle nullable properly

This commit is contained in:
Siyuan Miao 2025-09-11 01:57:46 +02:00
parent f8c2a95381
commit 3e2df4e651
3 changed files with 55 additions and 15 deletions

View File

@ -174,6 +174,11 @@ func extractMapType(t reflect.Type, schema *APISchema) *APIType {
// extractStructType handles struct types // extractStructType handles struct types
func extractStructType(t reflect.Type, schema *APISchema) *APIType { func extractStructType(t reflect.Type, schema *APISchema) *APIType {
// Skip null.* structs as they are handled as optional properties
if strings.HasPrefix(t.String(), "null.") {
return nil
}
apiType := &APIType{ apiType := &APIType{
Name: t.String(), Name: t.String(),
Kind: TypeKindStruct, Kind: TypeKindStruct,
@ -186,6 +191,9 @@ func extractStructType(t reflect.Type, schema *APISchema) *APIType {
} }
// Extract fields // Extract fields
embeddedStructs := make([]string, 0)
regularFields := make([]APIField, 0)
for i := 0; i < t.NumField(); i++ { for i := 0; i < t.NumField(); i++ {
field := t.Field(i) field := t.Field(i)
@ -194,17 +202,36 @@ func extractStructType(t reflect.Type, schema *APISchema) *APIType {
continue continue
} }
apiField := buildAPIField(field) // Check if this is an embedded struct (anonymous field)
apiType.Fields = append(apiType.Fields, apiField) if field.Anonymous && field.Type.Kind() == reflect.Struct {
embeddedStructs = append(embeddedStructs, field.Type.String())
// Recursively extract nested struct types from embedded structs
if nestedType := extractAPIType(field.Type, schema); nestedType != nil {
if nestedType.Kind == TypeKindStruct {
schema.Types[nestedType.Name] = *nestedType
}
}
} else {
apiField := buildAPIField(field)
regularFields = append(regularFields, apiField)
// Recursively extract nested struct types from field types // Recursively extract nested struct types from field types
if nestedType := extractAPIType(field.Type, schema); nestedType != nil { if nestedType := extractAPIType(field.Type, schema); nestedType != nil {
if nestedType.Kind == TypeKindStruct { if nestedType.Kind == TypeKindStruct {
schema.Types[nestedType.Name] = *nestedType schema.Types[nestedType.Name] = *nestedType
}
} }
} }
} }
// If we have exactly one embedded struct and no regular fields, mark it as an extension
if len(embeddedStructs) == 1 && len(regularFields) == 0 {
apiType.Kind = TypeKindExtension
apiType.Extends = embeddedStructs[0]
} else {
apiType.Fields = regularFields
}
return apiType return apiType
} }
@ -310,7 +337,7 @@ func getAllReferencedStructs(schema *APISchema) []APIType {
// Also add all structs from the complete schema that might be referenced // Also add all structs from the complete schema that might be referenced
for name, apiType := range schema.Types { for name, apiType := range schema.Types {
if apiType.Kind == TypeKindStruct { if apiType.Kind == TypeKindStruct || apiType.Kind == TypeKindExtension {
allStructs[name] = apiType allStructs[name] = apiType
} }
} }

View File

@ -18,6 +18,8 @@ const (
TypeKindArray TypeKind = "array" TypeKindArray TypeKind = "array"
// TypeKindPointer represents a pointer type // TypeKindPointer represents a pointer type
TypeKindPointer TypeKind = "pointer" TypeKindPointer TypeKind = "pointer"
// TypeKindExtension represents a struct that extends another struct
TypeKindExtension TypeKind = "extension"
) )
// APIType represents a type used in the JSON-RPC API // APIType represents a type used in the JSON-RPC API
@ -26,6 +28,7 @@ type APIType struct {
Package string `json:"package"` Package string `json:"package"`
Kind TypeKind `json:"kind"` Kind TypeKind `json:"kind"`
Fields []APIField `json:"fields,omitempty"` Fields []APIField `json:"fields,omitempty"`
Extends string `json:"extends,omitempty"`
IsPointer bool `json:"is_pointer"` IsPointer bool `json:"is_pointer"`
IsSlice bool `json:"is_slice"` IsSlice bool `json:"is_slice"`
IsMap bool `json:"is_map"` IsMap bool `json:"is_map"`

View File

@ -24,6 +24,7 @@ func generateTypeScriptTypings(schema *APISchema, searchPath string) string {
"sub": func(a, b int) int { return a - b }, "sub": func(a, b int) int { return a - b },
"pad": func(s string, width int) string { return padString(s, width) }, "pad": func(s string, width int) string { return padString(s, width) },
"padComment": func(fieldName, fieldType string) string { return padComment(fieldName, fieldType) }, "padComment": func(fieldName, fieldType string) string { return padComment(fieldName, fieldType) },
"isOptionalType": func(goType string) bool { return isOptionalType(goType) },
} }
// Parse the main template // Parse the main template
@ -65,6 +66,11 @@ func padComment(fieldName, fieldType string) string {
return strings.Repeat(" ", targetColumn-declarationLength) return strings.Repeat(" ", targetColumn-declarationLength)
} }
// isOptionalType determines if a Go type should be rendered as an optional TypeScript property
func isOptionalType(goType string) bool {
return goType == "null.String" || goType == "null.Bool" || goType == "null.Int"
}
// cleanTypeName cleans up Go type names for TypeScript // cleanTypeName cleans up Go type names for TypeScript
func cleanTypeName(typeName string) string { func cleanTypeName(typeName string) string {
// Remove package prefixes // Remove package prefixes
@ -105,6 +111,12 @@ func parseTypeRecursively(goType string) string {
return "string" return "string"
case "usbgadget.ByteSlice": case "usbgadget.ByteSlice":
return "number[]" return "number[]"
case "null.String":
return "string"
case "null.Bool":
return "boolean"
case "null.Int":
return "number"
case "interface {}": case "interface {}":
return "any" return "any"
case "time.Duration": case "time.Duration":
@ -237,12 +249,15 @@ func hasParameters(handler APIHandler) bool {
// typescriptTemplate is the main template for generating TypeScript definitions // typescriptTemplate is the main template for generating TypeScript definitions
const typescriptTemplate = `// Code generated by generate_typings.go. DO NOT EDIT. const typescriptTemplate = `// Code generated by generate_typings.go. DO NOT EDIT.
{{range $struct := getAllStructs}} {{range $struct := getAllStructs}}
{{if eq $struct.Kind "extension"}}
export interface {{cleanTypeName $struct.Name}} extends {{cleanTypeName $struct.Extends}} {
}
{{else}}
export interface {{cleanTypeName $struct.Name}} { export interface {{cleanTypeName $struct.Name}} {
{{range $field := $struct.Fields}} {{$field.JSONName}}: {{goToTypeScriptType $field.Type}};{{padComment $field.JSONName (goToTypeScriptType $field.Type)}}// {{$field.Name}} {{$field.Type}} {{range $field := $struct.Fields}} {{$field.JSONName}}{{if isOptionalType $field.Type}}?{{end}}: {{goToTypeScriptType $field.Type}};{{padComment $field.JSONName (goToTypeScriptType $field.Type)}}// {{$field.Name}} {{$field.Type}}
{{end}}} {{end}}}
{{end}} {{end}}
{{end}}
// String aliases with constants // String aliases with constants
{{range $alias := getStringAliasInfo}} {{range $alias := getStringAliasInfo}}
export type {{$alias.Name}} = {{range $i, $const := $alias.Constants}}"{{$const}}"{{if lt $i (sub (len $alias.Constants) 1)}} | {{end}}{{end}}; export type {{$alias.Name}} = {{range $i, $const := $alias.Constants}}"{{$const}}"{{if lt $i (sub (len $alias.Constants) 1)}} | {{end}}{{end}};
@ -276,11 +291,6 @@ export interface JsonRpcErrorResponse {
export type JsonRpcResponse = JsonRpcSuccessResponse | JsonRpcErrorResponse; 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 // RPC Functions
export class JsonRpcClient { export class JsonRpcClient {
constructor(private send: (method: string, params: unknown, callback?: (resp: JsonRpcResponse) => void) => void) {} constructor(private send: (method: string, params: unknown, callback?: (resp: JsonRpcResponse) => void) => void) {}