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
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{
Name: t.String(),
Kind: TypeKindStruct,
@ -186,6 +191,9 @@ func extractStructType(t reflect.Type, schema *APISchema) *APIType {
}
// Extract fields
embeddedStructs := make([]string, 0)
regularFields := make([]APIField, 0)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
@ -194,17 +202,36 @@ func extractStructType(t reflect.Type, schema *APISchema) *APIType {
continue
}
apiField := buildAPIField(field)
apiType.Fields = append(apiType.Fields, apiField)
// Check if this is an embedded struct (anonymous field)
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
if nestedType := extractAPIType(field.Type, schema); nestedType != nil {
if nestedType.Kind == TypeKindStruct {
schema.Types[nestedType.Name] = *nestedType
// 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
}
}
}
}
// 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
}
@ -310,7 +337,7 @@ func getAllReferencedStructs(schema *APISchema) []APIType {
// Also add all structs from the complete schema that might be referenced
for name, apiType := range schema.Types {
if apiType.Kind == TypeKindStruct {
if apiType.Kind == TypeKindStruct || apiType.Kind == TypeKindExtension {
allStructs[name] = apiType
}
}

View File

@ -18,6 +18,8 @@ const (
TypeKindArray TypeKind = "array"
// TypeKindPointer represents a pointer type
TypeKindPointer TypeKind = "pointer"
// TypeKindExtension represents a struct that extends another struct
TypeKindExtension TypeKind = "extension"
)
// APIType represents a type used in the JSON-RPC API
@ -26,6 +28,7 @@ type APIType struct {
Package string `json:"package"`
Kind TypeKind `json:"kind"`
Fields []APIField `json:"fields,omitempty"`
Extends string `json:"extends,omitempty"`
IsPointer bool `json:"is_pointer"`
IsSlice bool `json:"is_slice"`
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 },
"pad": func(s string, width int) string { return padString(s, width) },
"padComment": func(fieldName, fieldType string) string { return padComment(fieldName, fieldType) },
"isOptionalType": func(goType string) bool { return isOptionalType(goType) },
}
// Parse the main template
@ -65,6 +66,11 @@ func padComment(fieldName, fieldType string) string {
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
func cleanTypeName(typeName string) string {
// Remove package prefixes
@ -105,6 +111,12 @@ func parseTypeRecursively(goType string) string {
return "string"
case "usbgadget.ByteSlice":
return "number[]"
case "null.String":
return "string"
case "null.Bool":
return "boolean"
case "null.Int":
return "number"
case "interface {}":
return "any"
case "time.Duration":
@ -237,12 +249,15 @@ func hasParameters(handler APIHandler) bool {
// typescriptTemplate is the main template for generating TypeScript definitions
const typescriptTemplate = `// Code generated by generate_typings.go. DO NOT EDIT.
{{range $struct := getAllStructs}}
{{if eq $struct.Kind "extension"}}
export interface {{cleanTypeName $struct.Name}} extends {{cleanTypeName $struct.Extends}} {
}
{{else}}
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}}
// 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}};
@ -276,11 +291,6 @@ export interface 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
export class JsonRpcClient {
constructor(private send: (method: string, params: unknown, callback?: (resp: JsonRpcResponse) => void) => void) {}