waveterm/pkg/gogen/gogen.go
Copilot df24959e23
Add native 2+ arg RPC support and wire a concrete TestMultiArgCommand through server, generated clients, and CLI (#2963)
This PR extends WSH RPC command signatures to support `ctx + 2+ typed
args` while preserving existing `ctx` and `ctx + 1 arg` behavior. It
also adds a concrete `TestMultiArgCommand` end-to-end so the generated
Go/TS client surfaces can be inspected and exercised from CLI.

- **RPC wire + dispatch model**
- Added `wshrpc.MultiArg` (`args []any`) as the over-the-wire envelope
for 2+ arg commands.
- Extended RPC metadata to track all command arg types
(`CommandDataTypes`) and exposed a helper for normalized access.
  - Updated server adapter unmarshalling to:
    - decode `MultiArg` for 2+ arg commands,
    - validate arg count,
- re-unmarshal each arg into its declared type before invoking typed
handlers.
  - Kept single-arg commands on the existing non-`MultiArg` path.

- **Code generation (Go + TS)**
- Go codegen now emits multi-parameter wrappers for 2+ arg methods and
packs payload as `wshrpc.MultiArg`.
- TS codegen now emits multi-parameter API methods and packs payload as
`{ args: [...] }`.
  - 0/1-arg generation remains unchanged to avoid wire/API churn.

- **Concrete command added for validation**
  - Added to `WshRpcInterface`:
- `TestMultiArgCommand(ctx context.Context, arg1 string, arg2 int, arg3
bool) (string, error)`
- Implemented in `wshserver` with deterministic formatted return output
including source + all args.
- Updated `wsh test` command to call `TestMultiArgCommand` and print the
returned string.

- **Focused coverage**
- Added/updated targeted tests around RPC metadata and Go/TS multi-arg
codegen behavior, including command declaration for `testmultiarg`.

Example generated call shape:

```go
func TestMultiArgCommand(w *wshutil.WshRpc, arg1 string, arg2 int, arg3 bool, opts *wshrpc.RpcOpts) (string, error) {
    return sendRpcRequestCallHelper[string](
        w,
        "testmultiarg",
        wshrpc.MultiArg{Args: []any{arg1, arg2, arg3}},
        opts,
    )
}
```

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sawka <2722291+sawka@users.noreply.github.com>
Co-authored-by: sawka <mike@commandline.dev>
2026-03-02 12:29:04 -08:00

131 lines
4.1 KiB
Go

// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package gogen
import (
"fmt"
"reflect"
"strings"
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
)
func GenerateBoilerplate(buf *strings.Builder, pkgName string, imports []string) {
buf.WriteString("// Copyright 2026, Command Line Inc.\n")
buf.WriteString("// SPDX-License-Identifier: Apache-2.0\n")
buf.WriteString("\n// Generated Code. DO NOT EDIT.\n\n")
buf.WriteString(fmt.Sprintf("package %s\n\n", pkgName))
if len(imports) > 0 {
buf.WriteString("import (\n")
for _, imp := range imports {
buf.WriteString(fmt.Sprintf("\t%q\n", imp))
}
buf.WriteString(")\n\n")
}
}
func getBeforeColonPart(s string) string {
if colonIdx := strings.Index(s, ":"); colonIdx != -1 {
return s[:colonIdx]
}
return s
}
func GenerateMetaMapConsts(buf *strings.Builder, constPrefix string, rtype reflect.Type, embedded bool) {
if !embedded {
buf.WriteString("const (\n")
} else {
buf.WriteString("\n")
}
var lastBeforeColon = ""
isFirst := true
for idx := 0; idx < rtype.NumField(); idx++ {
field := rtype.Field(idx)
if field.PkgPath != "" {
continue
}
if field.Anonymous {
var embeddedBuf strings.Builder
GenerateMetaMapConsts(&embeddedBuf, constPrefix, field.Type, true)
buf.WriteString(embeddedBuf.String())
continue
}
fieldName := field.Name
jsonTag := utilfn.GetJsonTag(field)
if jsonTag == "" {
jsonTag = fieldName
}
beforeColon := getBeforeColonPart(jsonTag)
if beforeColon != lastBeforeColon {
if !isFirst {
buf.WriteString("\n")
}
lastBeforeColon = beforeColon
}
cname := constPrefix + fieldName
buf.WriteString(fmt.Sprintf("\t%-40s = %q\n", cname, jsonTag))
isFirst = false
}
if !embedded {
buf.WriteString(")\n")
}
}
func GenMethod_Call(buf *strings.Builder, methodDecl *wshrpc.WshRpcMethodDecl) {
fmt.Fprintf(buf, "// command %q, wshserver.%s\n", methodDecl.Command, methodDecl.MethodName)
dataType, dataVarName := getWshMethodDataParamsAndExpr(methodDecl)
returnType := "error"
respName := "_"
tParamVal := "any"
if methodDecl.DefaultResponseDataType != nil {
returnType = "(" + methodDecl.DefaultResponseDataType.String() + ", error)"
respName = "resp"
tParamVal = methodDecl.DefaultResponseDataType.String()
}
fmt.Fprintf(buf, "func %s(w *wshutil.WshRpc%s, opts *wshrpc.RpcOpts) %s {\n", methodDecl.MethodName, dataType, returnType)
fmt.Fprintf(buf, "\t%s, err := sendRpcRequestCallHelper[%s](w, %q, %s, opts)\n", respName, tParamVal, methodDecl.Command, dataVarName)
if methodDecl.DefaultResponseDataType != nil {
fmt.Fprintf(buf, "\treturn resp, err\n")
} else {
fmt.Fprintf(buf, "\treturn err\n")
}
fmt.Fprintf(buf, "}\n\n")
}
func GenMethod_ResponseStream(buf *strings.Builder, methodDecl *wshrpc.WshRpcMethodDecl) {
fmt.Fprintf(buf, "// command %q, wshserver.%s\n", methodDecl.Command, methodDecl.MethodName)
dataType, dataVarName := getWshMethodDataParamsAndExpr(methodDecl)
respType := "any"
if methodDecl.DefaultResponseDataType != nil {
respType = methodDecl.DefaultResponseDataType.String()
}
fmt.Fprintf(buf, "func %s(w *wshutil.WshRpc%s, opts *wshrpc.RpcOpts) chan wshrpc.RespOrErrorUnion[%s] {\n", methodDecl.MethodName, dataType, respType)
fmt.Fprintf(buf, "\treturn sendRpcRequestResponseStreamHelper[%s](w, %q, %s, opts)\n", respType, methodDecl.Command, dataVarName)
fmt.Fprintf(buf, "}\n\n")
}
func getWshMethodDataParamsAndExpr(methodDecl *wshrpc.WshRpcMethodDecl) (string, string) {
dataTypes := methodDecl.GetCommandDataTypes()
if len(dataTypes) == 0 {
return "", "nil"
}
if len(dataTypes) == 1 {
return ", data " + dataTypes[0].String(), "data"
}
var paramBuilder strings.Builder
var argBuilder strings.Builder
for idx, dataType := range dataTypes {
argName := fmt.Sprintf("arg%d", idx+1)
paramBuilder.WriteString(", ")
paramBuilder.WriteString(argName)
paramBuilder.WriteString(" ")
paramBuilder.WriteString(dataType.String())
if idx > 0 {
argBuilder.WriteString(", ")
}
argBuilder.WriteString(argName)
}
return paramBuilder.String(), fmt.Sprintf("wshrpc.MultiArg{Args: []any{%s}}", argBuilder.String())
}