diff --git a/scripts/jsonrpc_typings/schema.go b/scripts/jsonrpc_typings/schema.go index 5d602ce3..600bb620 100644 --- a/scripts/jsonrpc_typings/schema.go +++ b/scripts/jsonrpc_typings/schema.go @@ -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 } } diff --git a/scripts/jsonrpc_typings/types.go b/scripts/jsonrpc_typings/types.go index ea64d781..1242f97d 100644 --- a/scripts/jsonrpc_typings/types.go +++ b/scripts/jsonrpc_typings/types.go @@ -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"` diff --git a/scripts/jsonrpc_typings/typescript.go b/scripts/jsonrpc_typings/typescript.go index 8c986c57..32b5b22b 100644 --- a/scripts/jsonrpc_typings/typescript.go +++ b/scripts/jsonrpc_typings/typescript.go @@ -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) {}