From 3e5935340505dc70ac4686cbc4d556fcbb60a8df Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 20 May 2026 10:52:54 -0700 Subject: [PATCH 1/8] Add system Nexus WIT generation --- Makefile | 44 ++++++- cmd/gen-system-nexus-wit/main.go | 198 +++++++++++++++++++++++++++++++ nexus/temporal-system.wit | 77 ++++++++++++ 3 files changed, 315 insertions(+), 4 deletions(-) create mode 100644 cmd/gen-system-nexus-wit/main.go create mode 100644 nexus/temporal-system.wit diff --git a/Makefile b/Makefile index ea42652e9..9044d1726 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ ci-build: install proto http-api-docs install: grpc-install api-linter-install buf-install # Run all linters and compile proto files. -proto: grpc http-api-docs nexus-rpc-yaml +proto: grpc http-api-docs nexus-rpc-yaml system-nexus-wit ######################################################################## ##### Variables ###### @@ -17,7 +17,9 @@ GOPATH := $(shell go env GOPATH) endif GOBIN := $(if $(shell go env GOBIN),$(shell go env GOBIN),$(GOPATH)/bin) -PATH := $(GOBIN):$(PATH) +CARGO_HOME ?= $(HOME)/.cargo +CARGO_BIN := $(CARGO_HOME)/bin +PATH := $(GOBIN):$(CARGO_BIN):$(PATH) STAMPDIR := .stamp COLOR := "\e[1;36m%s\e[0m\n" @@ -33,8 +35,15 @@ PROTO_PATHS = paths=source_relative:$(PROTO_OUT) OAPI_OUT := openapi OAPI3_PATH := .components.schemas.Payload +SYSTEM_NEXUS_OUT := nexus +SYSTEM_NEXUS_WIT := $(SYSTEM_NEXUS_OUT)/temporal-system.wit +SYSTEM_NEXUS_DESCRIPTOR := $(PROTO_OUT)/system-nexus/temporal_api.bin +SYSTEM_NEXUS_SERVICE_PROTO_FILES := $(shell find temporal/api -name "service.proto" | sort) +GO_BUILD_CACHE ?= $(abspath $(PROTO_OUT)/go-build-cache) +NEXUS_API_GEN ?= nexus-api-gen + $(PROTO_OUT): - mkdir $(PROTO_OUT) + mkdir -p $(PROTO_OUT) ##### Compile proto files for go ##### grpc: buf-lint api-linter buf-breaking clean go-grpc fix-path @@ -106,7 +115,7 @@ api-linter: @api-linter --set-exit-status $(PROTO_IMPORTS) --config $(PROTO_ROOT)/api-linter.yaml --output-format json $(PROTO_FILES) | gojq -r 'map(select(.problems != []) | . as $$file | .problems[] | {rule: .rule_doc_uri, location: "\($$file.file_path):\(.location.start_position.line_number)"}) | group_by(.rule) | .[] | .[0].rule + ":\n" + (map("\t" + .location) | join("\n"))' $(STAMPDIR): - mkdir $@ + mkdir -p $@ $(STAMPDIR)/buf-mod-prune: $(STAMPDIR) buf.yaml printf $(COLOR) "Pruning buf module" @@ -137,6 +146,33 @@ nexus-rpc-yaml-install: printf $(COLOR) "Build and install protoc-gen-nexus-rpc-yaml..." @cd cmd/protoc-gen-nexus-rpc-yaml && go install . +##### Compile system Nexus WIT files ##### +system-nexus-wit: $(SYSTEM_NEXUS_WIT) + +$(SYSTEM_NEXUS_DESCRIPTOR): $(PROTO_FILES) | $(PROTO_OUT) + printf $(COLOR) "Build Temporal API descriptor set for system Nexus WIT..." + mkdir -p $(@D) + protoc -I $(PROTO_ROOT) \ + --include_imports \ + --include_source_info \ + --descriptor_set_out=$@ \ + $(SYSTEM_NEXUS_SERVICE_PROTO_FILES) + +$(SYSTEM_NEXUS_WIT): $(SYSTEM_NEXUS_DESCRIPTOR) cmd/gen-system-nexus-wit/main.go $(SYSTEM_NEXUS_SERVICE_PROTO_FILES) | $(STAMPDIR)/nexus-api-gen-install + printf $(COLOR) "Generate system Nexus WIT..." + GOCACHE=$(GO_BUILD_CACHE) go run cmd/gen-system-nexus-wit/main.go \ + --descriptors $(SYSTEM_NEXUS_DESCRIPTOR) \ + --nexus-api-gen $(NEXUS_API_GEN) \ + --output $@ \ + $(SYSTEM_NEXUS_SERVICE_PROTO_FILES) + +$(STAMPDIR)/nexus-api-gen-install: | $(STAMPDIR) + printf $(COLOR) "Install nexus-api-gen if missing..." + command -v $(NEXUS_API_GEN) >/dev/null || CARGO_NET_GIT_FETCH_WITH_CLI=true cargo install --git https://github.com/temporalio/nexus-api-gen + touch $@ + +nexus-api-gen-install: $(STAMPDIR)/nexus-api-gen-install + ##### Clean ##### clean: printf $(COLOR) "Delete generated go files..." diff --git a/cmd/gen-system-nexus-wit/main.go b/cmd/gen-system-nexus-wit/main.go new file mode 100644 index 000000000..865b5f67b --- /dev/null +++ b/cmd/gen-system-nexus-wit/main.go @@ -0,0 +1,198 @@ +package main + +import ( + "flag" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" +) + +var ( + exposedTagRE = regexp.MustCompile(`\btags\s*=\s*"exposed"`) + packageRE = regexp.MustCompile(`^\s*package\s+([A-Za-z0-9_.]+)\s*;`) + rpcRE = regexp.MustCompile(`^\s*rpc\s+([A-Za-z0-9_]+)\s*\(`) + serviceRE = regexp.MustCompile(`^\s*service\s+([A-Za-z0-9_]+)\s*\{`) +) + +func main() { + var descriptors string + var nexusAPIGen string + var output string + flag.StringVar(&descriptors, "descriptors", "", "protobuf descriptor set") + flag.StringVar(&nexusAPIGen, "nexus-api-gen", "nexus-api-gen", "path to nexus-api-gen") + flag.StringVar(&output, "output", "", "output WIT file") + flag.Parse() + + if descriptors == "" || output == "" || flag.NArg() == 0 { + fmt.Fprintf(os.Stderr, "usage: gen_system_nexus_wit --descriptors DESCRIPTORS [--nexus-api-gen PATH] --output OUTPUT PROTO...\n") + os.Exit(2) + } + + rpcs, err := exposedRPCs(flag.Args()) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + if len(rpcs) == 0 { + fmt.Fprintln(os.Stderr, "no proto RPCs are marked as exposed Nexus operations") + os.Exit(1) + } + + tempDir, err := os.MkdirTemp("", "system-nexus-wit-*") + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + defer os.RemoveAll(tempDir) + + tempOutput := filepath.Join(tempDir, "system-nexus.wit") + var input string + if _, err := os.Stat(output); err == nil { + if err := copyFile(output, tempOutput); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + input = tempOutput + } else if !os.IsNotExist(err) { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + for _, rpc := range rpcs { + if err := runAddRPC(nexusAPIGen, descriptors, rpc, tempOutput, input); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + input = tempOutput + } + + if err := os.MkdirAll(filepath.Dir(output), 0o755); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + if err := copyFile(tempOutput, output); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func exposedRPCs(paths []string) ([]string, error) { + var rpcs []string + for _, path := range paths { + content, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + var packageName string + var serviceName string + var serviceDepth int + var rpcName string + var rpcDepth int + var rpcIsExposed bool + + for _, rawLine := range strings.Split(string(content), "\n") { + line := stripLineComment(rawLine) + + if packageName == "" { + if match := packageRE.FindStringSubmatch(line); match != nil { + packageName = match[1] + } + } + + if serviceName == "" { + if match := serviceRE.FindStringSubmatch(line); match != nil { + serviceName = match[1] + serviceDepth = braceDelta(line) + } + continue + } + + if rpcName == "" { + if match := rpcRE.FindStringSubmatch(line); match != nil { + rpcName = match[1] + rpcDepth = braceDelta(line) + rpcIsExposed = exposedTagRE.MatchString(line) + if rpcDepth <= 0 { + if rpcIsExposed { + rpcs = append(rpcs, qualifiedRPCName(path, packageName, serviceName, rpcName)) + } + rpcName = "" + rpcDepth = 0 + rpcIsExposed = false + } + continue + } + + serviceDepth += braceDelta(line) + if serviceDepth <= 0 { + serviceName = "" + serviceDepth = 0 + } + continue + } + + rpcIsExposed = rpcIsExposed || exposedTagRE.MatchString(line) + rpcDepth += braceDelta(line) + if rpcDepth <= 0 { + if rpcIsExposed { + rpcs = append(rpcs, qualifiedRPCName(path, packageName, serviceName, rpcName)) + } + rpcName = "" + rpcDepth = 0 + rpcIsExposed = false + } + } + } + return rpcs, nil +} + +func qualifiedRPCName(path string, packageName string, serviceName string, rpcName string) string { + if packageName == "" || serviceName == "" { + fmt.Fprintf(os.Stderr, "%s: exposed RPC has no package or service\n", path) + os.Exit(1) + } + return packageName + "." + serviceName + "." + rpcName +} + +func runAddRPC(nexusAPIGen string, descriptors string, rpc string, output string, input string) error { + args := []string{ + "add-rpc", + "--descriptors", descriptors, + "--rpc", rpc, + "--output", output, + } + if input != "" { + args = append(args, "--input", input) + } + + command := exec.Command(nexusAPIGen, args...) + command.Stdout = os.Stdout + command.Stderr = os.Stderr + if err := command.Run(); err != nil { + return fmt.Errorf("%s %s: %w", nexusAPIGen, strings.Join(args, " "), err) + } + return nil +} + +func copyFile(source string, destination string) error { + content, err := os.ReadFile(source) + if err != nil { + return err + } + return os.WriteFile(destination, content, 0o644) +} + +func stripLineComment(line string) string { + if before, _, ok := strings.Cut(line, "//"); ok { + return before + } + return line +} + +func braceDelta(line string) int { + return strings.Count(line, "{") - strings.Count(line, "}") +} diff --git a/nexus/temporal-system.wit b/nexus/temporal-system.wit new file mode 100644 index 000000000..aa7e12fce --- /dev/null +++ b/nexus/temporal-system.wit @@ -0,0 +1,77 @@ +package temporal:nexus@1.0.0; + +world system { + export workflow-service; +} + +/// @nexus.endpoint "temporal-system" +interface workflow-service { + use nexus:temporal-types/model@1.0.0.{ + duration, + memo, + payloads, + placeholder, + priority, + retry-policy, + search-attributes, + signal-function, + task-queue, + user-metadata, + versioning-override, + workflow-function, + workflow-id-conflict-policy, + workflow-id-reuse-policy, + }; + + /// @nexus.proto "temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest" + record signal-with-start-workflow-execution-request { + /// @nexus.proto-field "workflow_type" + workflow: workflow-function, + workflow-id: string, + task-queue: task-queue, + /// @nexus.proto-field "signal_name" + signal: signal-function, + workflow-execution-timeout: option, + workflow-run-timeout: option, + workflow-task-timeout: option, + identity: option, + request-id: option, + workflow-id-reuse-policy: option, + workflow-id-conflict-policy: option, + retry-policy: option, + cron-schedule: option, + memo: option, + search-attributes: option, + priority: option, + versioning-override: option, + workflow-start-delay: option, + user-metadata: option, + /// @nexus.source python="workflow_namespace" typescript="workflowNamespace" + namespace: string, + /// @nexus.omit + control: placeholder, + /// @nexus.omit + header: placeholder, + /// @nexus.omit + links: placeholder, + /// @nexus.omit + time-skipping-config: placeholder, + } + + /// @nexus.proto "temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse" + record signal-with-start-workflow-execution-response { + run-id: option, + started: option, + /// @nexus.omit + signal-link: placeholder, + } + + /// @nexus.output-transform + /// python-type="workflow.ExternalWorkflowHandle[typing.Any]" + /// python="workflow.get_external_workflow_handle(request.workflow_id, run_id=result.run_id)" + /// typescript-type="workflow.ExternalWorkflowHandle" + /// typescript="workflow.getExternalWorkflowHandle(request.workflowId, result.runId ?? undefined)" + signal-with-start-workflow-execution: func( + request: signal-with-start-workflow-execution-request, + ) -> signal-with-start-workflow-execution-response; +} From c2d52d06d234622f03193c3aa2bf82f520a943f4 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 20 May 2026 12:57:46 -0700 Subject: [PATCH 2/8] Expose system Nexus operation metadata --- README.md | 1 + nexus/temporal-system.wit | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 788613ae8..b31d2f70d 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Install as git submodule to the project. ## Contribution Make your change to the temporal/proto files, and run `make` to update the openapi definitions. +Rust is also required because `make` installs and runs `nexus-api-gen` when regenerating system Nexus WIT files. ## Breaking changes diff --git a/nexus/temporal-system.wit b/nexus/temporal-system.wit index aa7e12fce..1acb3f7e0 100644 --- a/nexus/temporal-system.wit +++ b/nexus/temporal-system.wit @@ -71,7 +71,8 @@ interface workflow-service { /// python="workflow.get_external_workflow_handle(request.workflow_id, run_id=result.run_id)" /// typescript-type="workflow.ExternalWorkflowHandle" /// typescript="workflow.getExternalWorkflowHandle(request.workflowId, result.runId ?? undefined)" - signal-with-start-workflow-execution: func( + /// @nexus.operation name="SignalWithStartWorkflowExecution" + signal-with-start-workflow: func( request: signal-with-start-workflow-execution-request, ) -> signal-with-start-workflow-execution-response; } From 5229707452c2cf426eb530bc15878fd3b0581f02 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 20 May 2026 13:13:25 -0700 Subject: [PATCH 3/8] Generate system Nexus WIT with protoc plugin --- Makefile | 41 +-- cmd/gen-system-nexus-wit/main.go | 198 -------------- cmd/protoc-gen-nexus-rpc-yaml/generator.go | 272 ------------------- cmd/protoc-gen-nexus-rpc-yaml/go.mod | 12 - cmd/protoc-gen-nexus-rpc-yaml/go.sum | 10 - cmd/protoc-gen-nexus-rpc-yaml/main.go | 13 - cmd/protoc-gen-system-nexus-wit/generator.go | 168 ++++++++++++ cmd/protoc-gen-system-nexus-wit/go.mod | 8 + cmd/protoc-gen-system-nexus-wit/go.sum | 8 + cmd/protoc-gen-system-nexus-wit/main.go | 11 + nexus/temporal-proto-models-nexusrpc.yaml | 19 -- nexus/temporal-system.wit | 2 +- 12 files changed, 206 insertions(+), 556 deletions(-) delete mode 100644 cmd/gen-system-nexus-wit/main.go delete mode 100644 cmd/protoc-gen-nexus-rpc-yaml/generator.go delete mode 100644 cmd/protoc-gen-nexus-rpc-yaml/go.mod delete mode 100644 cmd/protoc-gen-nexus-rpc-yaml/go.sum delete mode 100644 cmd/protoc-gen-nexus-rpc-yaml/main.go create mode 100644 cmd/protoc-gen-system-nexus-wit/generator.go create mode 100644 cmd/protoc-gen-system-nexus-wit/go.mod create mode 100644 cmd/protoc-gen-system-nexus-wit/go.sum create mode 100644 cmd/protoc-gen-system-nexus-wit/main.go delete mode 100644 nexus/temporal-proto-models-nexusrpc.yaml diff --git a/Makefile b/Makefile index 9044d1726..f98caab7d 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ ci-build: install proto http-api-docs install: grpc-install api-linter-install buf-install # Run all linters and compile proto files. -proto: grpc http-api-docs nexus-rpc-yaml system-nexus-wit +proto: grpc http-api-docs system-nexus-wit ######################################################################## ##### Variables ###### @@ -37,7 +37,6 @@ OAPI3_PATH := .components.schemas.Payload SYSTEM_NEXUS_OUT := nexus SYSTEM_NEXUS_WIT := $(SYSTEM_NEXUS_OUT)/temporal-system.wit -SYSTEM_NEXUS_DESCRIPTOR := $(PROTO_OUT)/system-nexus/temporal_api.bin SYSTEM_NEXUS_SERVICE_PROTO_FILES := $(shell find temporal/api -name "service.proto" | sort) GO_BUILD_CACHE ?= $(abspath $(PROTO_OUT)/go-build-cache) NEXUS_API_GEN ?= nexus-api-gen @@ -130,41 +129,21 @@ buf-breaking: @printf $(COLOR) "Run buf breaking changes check against master branch..." @(cd $(PROTO_ROOT) && buf breaking --against 'https://github.com/temporalio/api.git#branch=master') -nexus-rpc-yaml: nexus-rpc-yaml-install - printf $(COLOR) "Generate nexus/temporal-proto-models-nexusrpc.yaml..." - mkdir -p nexus - protoc -I $(PROTO_ROOT) \ - --nexus-rpc-yaml_opt=nexus-rpc_langs_out=nexus/temporal-proto-models-nexusrpc.yaml \ - --nexus-rpc-yaml_opt=python_package_prefix=temporalio.api \ - --nexus-rpc-yaml_opt=typescript_package_prefix=@temporalio/api \ - --nexus-rpc-yaml_opt=include_operation_tags=exposed \ - --nexus-rpc-yaml_out=. \ - temporal/api/workflowservice/v1/* \ - temporal/api/operatorservice/v1/* - -nexus-rpc-yaml-install: - printf $(COLOR) "Build and install protoc-gen-nexus-rpc-yaml..." - @cd cmd/protoc-gen-nexus-rpc-yaml && go install . - ##### Compile system Nexus WIT files ##### system-nexus-wit: $(SYSTEM_NEXUS_WIT) -$(SYSTEM_NEXUS_DESCRIPTOR): $(PROTO_FILES) | $(PROTO_OUT) - printf $(COLOR) "Build Temporal API descriptor set for system Nexus WIT..." - mkdir -p $(@D) +$(SYSTEM_NEXUS_WIT): $(PROTO_FILES) cmd/protoc-gen-system-nexus-wit/main.go cmd/protoc-gen-system-nexus-wit/generator.go | system-nexus-wit-install $(STAMPDIR)/nexus-api-gen-install + printf $(COLOR) "Generate system Nexus WIT..." + mkdir -p $(SYSTEM_NEXUS_OUT) protoc -I $(PROTO_ROOT) \ - --include_imports \ - --include_source_info \ - --descriptor_set_out=$@ \ + --system-nexus-wit_opt=output=$(SYSTEM_NEXUS_WIT) \ + --system-nexus-wit_opt=nexus_api_gen=$(NEXUS_API_GEN) \ + --system-nexus-wit_out=. \ $(SYSTEM_NEXUS_SERVICE_PROTO_FILES) -$(SYSTEM_NEXUS_WIT): $(SYSTEM_NEXUS_DESCRIPTOR) cmd/gen-system-nexus-wit/main.go $(SYSTEM_NEXUS_SERVICE_PROTO_FILES) | $(STAMPDIR)/nexus-api-gen-install - printf $(COLOR) "Generate system Nexus WIT..." - GOCACHE=$(GO_BUILD_CACHE) go run cmd/gen-system-nexus-wit/main.go \ - --descriptors $(SYSTEM_NEXUS_DESCRIPTOR) \ - --nexus-api-gen $(NEXUS_API_GEN) \ - --output $@ \ - $(SYSTEM_NEXUS_SERVICE_PROTO_FILES) +system-nexus-wit-install: | $(PROTO_OUT) + printf $(COLOR) "Build and install protoc-gen-system-nexus-wit..." + @cd cmd/protoc-gen-system-nexus-wit && GOCACHE=$(GO_BUILD_CACHE) go install . $(STAMPDIR)/nexus-api-gen-install: | $(STAMPDIR) printf $(COLOR) "Install nexus-api-gen if missing..." diff --git a/cmd/gen-system-nexus-wit/main.go b/cmd/gen-system-nexus-wit/main.go deleted file mode 100644 index 865b5f67b..000000000 --- a/cmd/gen-system-nexus-wit/main.go +++ /dev/null @@ -1,198 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "os" - "os/exec" - "path/filepath" - "regexp" - "strings" -) - -var ( - exposedTagRE = regexp.MustCompile(`\btags\s*=\s*"exposed"`) - packageRE = regexp.MustCompile(`^\s*package\s+([A-Za-z0-9_.]+)\s*;`) - rpcRE = regexp.MustCompile(`^\s*rpc\s+([A-Za-z0-9_]+)\s*\(`) - serviceRE = regexp.MustCompile(`^\s*service\s+([A-Za-z0-9_]+)\s*\{`) -) - -func main() { - var descriptors string - var nexusAPIGen string - var output string - flag.StringVar(&descriptors, "descriptors", "", "protobuf descriptor set") - flag.StringVar(&nexusAPIGen, "nexus-api-gen", "nexus-api-gen", "path to nexus-api-gen") - flag.StringVar(&output, "output", "", "output WIT file") - flag.Parse() - - if descriptors == "" || output == "" || flag.NArg() == 0 { - fmt.Fprintf(os.Stderr, "usage: gen_system_nexus_wit --descriptors DESCRIPTORS [--nexus-api-gen PATH] --output OUTPUT PROTO...\n") - os.Exit(2) - } - - rpcs, err := exposedRPCs(flag.Args()) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - if len(rpcs) == 0 { - fmt.Fprintln(os.Stderr, "no proto RPCs are marked as exposed Nexus operations") - os.Exit(1) - } - - tempDir, err := os.MkdirTemp("", "system-nexus-wit-*") - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - defer os.RemoveAll(tempDir) - - tempOutput := filepath.Join(tempDir, "system-nexus.wit") - var input string - if _, err := os.Stat(output); err == nil { - if err := copyFile(output, tempOutput); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - input = tempOutput - } else if !os.IsNotExist(err) { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - - for _, rpc := range rpcs { - if err := runAddRPC(nexusAPIGen, descriptors, rpc, tempOutput, input); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - input = tempOutput - } - - if err := os.MkdirAll(filepath.Dir(output), 0o755); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - if err := copyFile(tempOutput, output); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} - -func exposedRPCs(paths []string) ([]string, error) { - var rpcs []string - for _, path := range paths { - content, err := os.ReadFile(path) - if err != nil { - return nil, err - } - - var packageName string - var serviceName string - var serviceDepth int - var rpcName string - var rpcDepth int - var rpcIsExposed bool - - for _, rawLine := range strings.Split(string(content), "\n") { - line := stripLineComment(rawLine) - - if packageName == "" { - if match := packageRE.FindStringSubmatch(line); match != nil { - packageName = match[1] - } - } - - if serviceName == "" { - if match := serviceRE.FindStringSubmatch(line); match != nil { - serviceName = match[1] - serviceDepth = braceDelta(line) - } - continue - } - - if rpcName == "" { - if match := rpcRE.FindStringSubmatch(line); match != nil { - rpcName = match[1] - rpcDepth = braceDelta(line) - rpcIsExposed = exposedTagRE.MatchString(line) - if rpcDepth <= 0 { - if rpcIsExposed { - rpcs = append(rpcs, qualifiedRPCName(path, packageName, serviceName, rpcName)) - } - rpcName = "" - rpcDepth = 0 - rpcIsExposed = false - } - continue - } - - serviceDepth += braceDelta(line) - if serviceDepth <= 0 { - serviceName = "" - serviceDepth = 0 - } - continue - } - - rpcIsExposed = rpcIsExposed || exposedTagRE.MatchString(line) - rpcDepth += braceDelta(line) - if rpcDepth <= 0 { - if rpcIsExposed { - rpcs = append(rpcs, qualifiedRPCName(path, packageName, serviceName, rpcName)) - } - rpcName = "" - rpcDepth = 0 - rpcIsExposed = false - } - } - } - return rpcs, nil -} - -func qualifiedRPCName(path string, packageName string, serviceName string, rpcName string) string { - if packageName == "" || serviceName == "" { - fmt.Fprintf(os.Stderr, "%s: exposed RPC has no package or service\n", path) - os.Exit(1) - } - return packageName + "." + serviceName + "." + rpcName -} - -func runAddRPC(nexusAPIGen string, descriptors string, rpc string, output string, input string) error { - args := []string{ - "add-rpc", - "--descriptors", descriptors, - "--rpc", rpc, - "--output", output, - } - if input != "" { - args = append(args, "--input", input) - } - - command := exec.Command(nexusAPIGen, args...) - command.Stdout = os.Stdout - command.Stderr = os.Stderr - if err := command.Run(); err != nil { - return fmt.Errorf("%s %s: %w", nexusAPIGen, strings.Join(args, " "), err) - } - return nil -} - -func copyFile(source string, destination string) error { - content, err := os.ReadFile(source) - if err != nil { - return err - } - return os.WriteFile(destination, content, 0o644) -} - -func stripLineComment(line string) string { - if before, _, ok := strings.Cut(line, "//"); ok { - return before - } - return line -} - -func braceDelta(line string) int { - return strings.Count(line, "{") - strings.Count(line, "}") -} diff --git a/cmd/protoc-gen-nexus-rpc-yaml/generator.go b/cmd/protoc-gen-nexus-rpc-yaml/generator.go deleted file mode 100644 index 2e4a939c8..000000000 --- a/cmd/protoc-gen-nexus-rpc-yaml/generator.go +++ /dev/null @@ -1,272 +0,0 @@ -package main - -import ( - "fmt" - "slices" - "sort" - "strings" - - nexusannotationsv1 "github.com/nexus-rpc/nexus-proto-annotations/go/nexusannotations/v1" - "google.golang.org/protobuf/compiler/protogen" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/types/descriptorpb" - "gopkg.in/yaml.v3" -) - -// params holds the parsed protoc plugin options. -// Passed via --nexus-rpc-yaml_opt=key=value (multiple opts are comma-joined by protoc). -// -// - nexus-rpc_langs_out: optional. Output path for the langs YAML. -// If empty, nothing is written. -// Example: "nexus/temporal-proto-models-nexusrpc.yaml" -// -// - python_package_prefix: optional. Dot-separated package prefix for $pythonRef. -// The last two path segments of the go_package ({service}/v{n}) are appended. -// Example: "temporalio.api" → "temporalio.api.workflowservice.v1.TypeName" -// If empty, $pythonRef is omitted. -// -// - typescript_package_prefix: optional. Scoped package prefix for $typescriptRef. -// The last two path segments of the go_package ({service}/v{n}) are appended. -// Example: "@temporalio/api" → "@temporalio/api/workflowservice/v1.TypeName" -// If empty, $typescriptRef is omitted. -// -// - include_operation_tags: optional, repeatable. Only include operations whose tags -// contain at least one of these values. If empty, all annotated operations are included -// (subject to exclude_operation_tags). Specify multiple times for multiple tags. -// Example: include_operation_tags=exposed -// -// - exclude_operation_tags: optional, repeatable. Exclude operations whose tags contain -// any of these values. Applied after include_operation_tags. -// Example: exclude_operation_tags=internal -type params struct { - nexusRpcLangsOut string - pythonPackagePrefix string - typescriptPackagePrefix string - includeOperationTags []string - excludeOperationTags []string -} - -// parseParams parses the comma-separated key=value parameter string provided by protoc. -func parseParams(raw string) (params, error) { - var p params - if raw == "" { - return p, nil - } - for kv := range strings.SplitSeq(raw, ",") { - key, value, ok := strings.Cut(kv, "=") - if !ok { - return p, fmt.Errorf("invalid parameter %q: expected key=value", kv) - } - switch key { - case "nexus-rpc_langs_out": - p.nexusRpcLangsOut = value - case "python_package_prefix": - p.pythonPackagePrefix = value - case "typescript_package_prefix": - p.typescriptPackagePrefix = value - case "include_operation_tags": - p.includeOperationTags = append(p.includeOperationTags, value) - case "exclude_operation_tags": - p.excludeOperationTags = append(p.excludeOperationTags, value) - default: - return p, fmt.Errorf("unknown parameter %q", key) - } - } - return p, nil -} - -// shouldIncludeOperation returns true if the method's nexus operation tags pass -// the include/exclude filters. Mirrors the logic from protoc-gen-go-nexus: -// 1. Method must have the nexus operation extension set. -// 2. If includeOperationTags is non-empty, at least one of the method's tags must match. -// 3. If excludeOperationTags is non-empty, none of the method's tags may match. -func shouldIncludeOperation(p params, m *protogen.Method) bool { - opts, ok := m.Desc.Options().(*descriptorpb.MethodOptions) - if !ok || opts == nil { - return false - } - if !proto.HasExtension(opts, nexusannotationsv1.E_Operation) { - return false - } - tags := proto.GetExtension(opts, nexusannotationsv1.E_Operation).(*nexusannotationsv1.OperationOptions).GetTags() - if len(p.includeOperationTags) > 0 && !slices.ContainsFunc(p.includeOperationTags, func(t string) bool { - return slices.Contains(tags, t) - }) { - return false - } - return !slices.ContainsFunc(p.excludeOperationTags, func(t string) bool { - return slices.Contains(tags, t) - }) -} - -func generate(gen *protogen.Plugin) error { - p, err := parseParams(gen.Request.GetParameter()) - if err != nil { - return err - } - - langsDoc := newDoc() - hasOps := false - - for _, f := range gen.Files { - if !f.Generate { - continue - } - for _, svc := range f.Services { - for _, m := range svc.Methods { - if !shouldIncludeOperation(p, m) { - continue - } - svcName := string(svc.Desc.Name()) - methodName := string(m.Desc.Name()) - hasOps = true - addOperation(langsDoc, svcName, methodName, - langRefs(p, f.Desc, m.Input.Desc), - langRefs(p, f.Desc, m.Output.Desc), - ) - } - } - } - - if !hasOps { - return nil - } - if p.nexusRpcLangsOut != "" { - return writeFile(gen, p.nexusRpcLangsOut, langsDoc) - } - return nil -} - -// langRefs builds the map of language-specific type refs for a message. -// -// Go, Java, dotnet, and Ruby refs are derived from proto file-level package options. -// Python and TypeScript refs require the corresponding prefix params to be set; if -// empty they are omitted. Both use the last two path segments of go_package -// ({service}/v{n}), dropping any intermediate grouping directory. -func langRefs(p params, file protoreflect.FileDescriptor, msg protoreflect.MessageDescriptor) map[string]string { - opts, ok := file.Options().(*descriptorpb.FileOptions) - if !ok || opts == nil { - return nil - } - name := string(msg.Name()) - refs := make(map[string]string) - - if pkg := opts.GetGoPackage(); pkg != "" { - // strip the ";alias" suffix (e.g. "go.temporal.io/api/workflowservice/v1;workflowservice") - pkg = strings.SplitN(pkg, ";", 2)[0] - refs["$goRef"] = pkg + "." + name - - segments := strings.Split(pkg, "/") - if len(segments) >= 2 { - tail := segments[len(segments)-2] + "/" + segments[len(segments)-1] - if p.pythonPackagePrefix != "" { - dotTail := strings.ReplaceAll(tail, "/", ".") - refs["$pythonRef"] = p.pythonPackagePrefix + "." + dotTail + "." + name - } - if p.typescriptPackagePrefix != "" { - refs["$typescriptRef"] = p.typescriptPackagePrefix + "/" + tail + "." + name - } - } - } - if pkg := opts.GetJavaPackage(); pkg != "" { - refs["$javaRef"] = pkg + "." + name - } - if pkg := opts.GetRubyPackage(); pkg != "" { - refs["$rubyRef"] = pkg + "::" + name - } - if pkg := opts.GetCsharpNamespace(); pkg != "" { - refs["$dotnetRef"] = pkg + "." + name - } - if len(refs) == 0 { - return nil - } - return refs -} - -// newDoc creates a yaml.Node document with the "nexusrpc: 1.0.0" header -// and an empty "services" mapping node. -func newDoc() *yaml.Node { - doc := &yaml.Node{Kind: yaml.DocumentNode} - root := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} - doc.Content = []*yaml.Node{root} - root.Content = append(root.Content, - scalarNode("nexusrpc"), - scalarNode("1.0.0"), - scalarNode("services"), - &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}, - ) - return doc -} - -// servicesNode returns the "services" mapping node from a doc created by newDoc. -func servicesNode(doc *yaml.Node) *yaml.Node { - root := doc.Content[0] - for i := 0; i < len(root.Content)-1; i += 2 { - if root.Content[i].Value == "services" { - return root.Content[i+1] - } - } - panic("services node not found") -} - -// addOperation inserts a service → operation → {input, output} entry into doc. -// Services and operations are inserted in the order first encountered. -func addOperation(doc *yaml.Node, svcName, methodName string, input, output map[string]string) { - svcs := servicesNode(doc) - - var svcOps *yaml.Node - for i := 0; i < len(svcs.Content)-1; i += 2 { - if svcs.Content[i].Value == svcName { - svcMap := svcs.Content[i+1] - for j := 0; j < len(svcMap.Content)-1; j += 2 { - if svcMap.Content[j].Value == "operations" { - svcOps = svcMap.Content[j+1] - } - } - } - } - if svcOps == nil { - svcMap := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} - svcOps = &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} - svcMap.Content = append(svcMap.Content, scalarNode("operations"), svcOps) - svcs.Content = append(svcs.Content, scalarNode(svcName), svcMap) - } - - opNode := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} - if len(input) > 0 { - opNode.Content = append(opNode.Content, scalarNode("input"), mapNode(input)) - } - if len(output) > 0 { - opNode.Content = append(opNode.Content, scalarNode("output"), mapNode(output)) - } - svcOps.Content = append(svcOps.Content, scalarNode(methodName), opNode) -} - -// mapNode serializes a map[string]string as a yaml mapping node with keys in sorted order. -func mapNode(m map[string]string) *yaml.Node { - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - node := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} - for _, k := range keys { - node.Content = append(node.Content, scalarNode(k), scalarNode(m[k])) - } - return node -} - -func scalarNode(value string) *yaml.Node { - return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: value} -} - -func writeFile(gen *protogen.Plugin, name string, doc *yaml.Node) error { - f := gen.NewGeneratedFile(name, "") - enc := yaml.NewEncoder(f) - enc.SetIndent(2) - if err := enc.Encode(doc); err != nil { - return err - } - return enc.Close() -} diff --git a/cmd/protoc-gen-nexus-rpc-yaml/go.mod b/cmd/protoc-gen-nexus-rpc-yaml/go.mod deleted file mode 100644 index 863c771a6..000000000 --- a/cmd/protoc-gen-nexus-rpc-yaml/go.mod +++ /dev/null @@ -1,12 +0,0 @@ -module github.com/temporalio/api/cmd/protoc-gen-nexus-rpc-yaml - -go 1.25.4 - -require ( - google.golang.org/protobuf v1.36.1 - gopkg.in/yaml.v3 v3.0.1 -) - -require github.com/nexus-rpc/nexus-proto-annotations v0.0.0-20260330194009-e558d6edaf84 - -require github.com/google/go-cmp v0.6.0 // indirect diff --git a/cmd/protoc-gen-nexus-rpc-yaml/go.sum b/cmd/protoc-gen-nexus-rpc-yaml/go.sum deleted file mode 100644 index cbc5252ff..000000000 --- a/cmd/protoc-gen-nexus-rpc-yaml/go.sum +++ /dev/null @@ -1,10 +0,0 @@ -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/nexus-rpc/nexus-proto-annotations v0.0.0-20260330194009-e558d6edaf84 h1:SWHt3Coj0VvF0Km1A0wlY+IjnHKsjQLgO29io84r3wY= -github.com/nexus-rpc/nexus-proto-annotations v0.0.0-20260330194009-e558d6edaf84/go.mod h1:n3UjF1bPCW8llR8tHvbxJ+27yPWrhpo8w/Yg1IOuY0Y= -google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cmd/protoc-gen-nexus-rpc-yaml/main.go b/cmd/protoc-gen-nexus-rpc-yaml/main.go deleted file mode 100644 index 31cdca92f..000000000 --- a/cmd/protoc-gen-nexus-rpc-yaml/main.go +++ /dev/null @@ -1,13 +0,0 @@ -// protoc-gen-nexus-rpc-yaml is a protoc plugin that generates nexus/temporal-proto-models-nexusrpc.yaml -// from proto service methods annotated with option (nexusannotations.v1.operation).tags = "exposed". -package main - -import ( - "google.golang.org/protobuf/compiler/protogen" -) - -func main() { - protogen.Options{}.Run(func(gen *protogen.Plugin) error { - return generate(gen) - }) -} diff --git a/cmd/protoc-gen-system-nexus-wit/generator.go b/cmd/protoc-gen-system-nexus-wit/generator.go new file mode 100644 index 000000000..0e7207266 --- /dev/null +++ b/cmd/protoc-gen-system-nexus-wit/generator.go @@ -0,0 +1,168 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "slices" + "strings" + + nexusannotationsv1 "github.com/nexus-rpc/nexus-proto-annotations/go/nexusannotations/v1" + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/descriptorpb" +) + +type params struct { + nexusAPIGen string + output string + input string +} + +func parseParams(raw string) (params, error) { + p := params{ + nexusAPIGen: "nexus-api-gen", + } + if raw == "" { + return p, nil + } + for kv := range strings.SplitSeq(raw, ",") { + key, value, ok := strings.Cut(kv, "=") + if !ok { + return p, fmt.Errorf("invalid parameter %q: expected key=value", kv) + } + switch key { + case "nexus_api_gen": + p.nexusAPIGen = value + case "output": + p.output = value + case "input": + p.input = value + default: + return p, fmt.Errorf("unknown parameter %q", key) + } + } + return p, nil +} + +func generate(gen *protogen.Plugin) error { + p, err := parseParams(gen.Request.GetParameter()) + if err != nil { + return err + } + if p.output == "" { + return fmt.Errorf("missing required output parameter") + } + if p.input == "" { + p.input = p.output + } + + rpcs := exposedRPCs(gen) + if len(rpcs) == 0 { + return fmt.Errorf("no proto RPCs are marked as exposed Nexus operations") + } + + tempDir, err := os.MkdirTemp("", "system-nexus-wit-*") + if err != nil { + return err + } + defer os.RemoveAll(tempDir) + + descriptorPath := filepath.Join(tempDir, "temporal_api.bin") + if err := writeDescriptorSet(gen, descriptorPath); err != nil { + return err + } + + tempOutput := filepath.Join(tempDir, "system-nexus.wit") + input := "" + if _, err := os.Stat(p.input); err == nil { + if err := copyFile(p.input, tempOutput); err != nil { + return err + } + input = tempOutput + } else if !os.IsNotExist(err) { + return err + } + + for _, rpc := range rpcs { + if err := runAddRPC(p.nexusAPIGen, descriptorPath, rpc, tempOutput, input); err != nil { + return err + } + input = tempOutput + } + + content, err := os.ReadFile(tempOutput) + if err != nil { + return err + } + _, err = gen.NewGeneratedFile(p.output, "").Write(content) + return err +} + +func exposedRPCs(gen *protogen.Plugin) []string { + var rpcs []string + for _, f := range gen.Files { + if !f.Generate { + continue + } + for _, svc := range f.Services { + for _, m := range svc.Methods { + if isExposedOperation(m) { + rpcs = append(rpcs, string(m.Desc.FullName())) + } + } + } + } + return rpcs +} + +func isExposedOperation(m *protogen.Method) bool { + opts, ok := m.Desc.Options().(*descriptorpb.MethodOptions) + if !ok || opts == nil { + return false + } + if !proto.HasExtension(opts, nexusannotationsv1.E_Operation) { + return false + } + tags := proto.GetExtension(opts, nexusannotationsv1.E_Operation).(*nexusannotationsv1.OperationOptions).GetTags() + return slices.Contains(tags, "exposed") +} + +func writeDescriptorSet(gen *protogen.Plugin, descriptorPath string) error { + data, err := proto.Marshal(&descriptorpb.FileDescriptorSet{ + File: gen.Request.GetProtoFile(), + }) + if err != nil { + return err + } + return os.WriteFile(descriptorPath, data, 0o644) +} + +func runAddRPC(nexusAPIGen string, descriptors string, rpc string, output string, input string) error { + args := []string{ + "add-rpc", + "--descriptors", descriptors, + "--rpc", rpc, + "--output", output, + } + if input != "" { + args = append(args, "--input", input) + } + + command := exec.Command(nexusAPIGen, args...) + command.Stdout = os.Stdout + command.Stderr = os.Stderr + if err := command.Run(); err != nil { + return fmt.Errorf("%s %s: %w", nexusAPIGen, strings.Join(args, " "), err) + } + return nil +} + +func copyFile(source string, destination string) error { + content, err := os.ReadFile(source) + if err != nil { + return err + } + return os.WriteFile(destination, content, 0o644) +} diff --git a/cmd/protoc-gen-system-nexus-wit/go.mod b/cmd/protoc-gen-system-nexus-wit/go.mod new file mode 100644 index 000000000..222e32187 --- /dev/null +++ b/cmd/protoc-gen-system-nexus-wit/go.mod @@ -0,0 +1,8 @@ +module github.com/temporalio/api/cmd/protoc-gen-system-nexus-wit + +go 1.25.4 + +require ( + github.com/nexus-rpc/nexus-proto-annotations v0.0.0-20260330194009-e558d6edaf84 + google.golang.org/protobuf v1.36.1 +) diff --git a/cmd/protoc-gen-system-nexus-wit/go.sum b/cmd/protoc-gen-system-nexus-wit/go.sum new file mode 100644 index 000000000..c96f6a1c5 --- /dev/null +++ b/cmd/protoc-gen-system-nexus-wit/go.sum @@ -0,0 +1,8 @@ +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/nexus-rpc/nexus-proto-annotations v0.0.0-20260330194009-e558d6edaf84 h1:SWHt3Coj0VvF0Km1A0wlY+IjnHKsjQLgO29io84r3wY= +github.com/nexus-rpc/nexus-proto-annotations v0.0.0-20260330194009-e558d6edaf84/go.mod h1:n3UjF1bPCW8llR8tHvbxJ+27yPWrhpo8w/Yg1IOuY0Y= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= diff --git a/cmd/protoc-gen-system-nexus-wit/main.go b/cmd/protoc-gen-system-nexus-wit/main.go new file mode 100644 index 000000000..0e9fe3182 --- /dev/null +++ b/cmd/protoc-gen-system-nexus-wit/main.go @@ -0,0 +1,11 @@ +// protoc-gen-system-nexus-wit generates nexus/temporal-system.wit from proto service +// methods annotated with option (nexusannotations.v1.operation).tags = "exposed". +package main + +import "google.golang.org/protobuf/compiler/protogen" + +func main() { + protogen.Options{}.Run(func(gen *protogen.Plugin) error { + return generate(gen) + }) +} diff --git a/nexus/temporal-proto-models-nexusrpc.yaml b/nexus/temporal-proto-models-nexusrpc.yaml deleted file mode 100644 index e0761fd15..000000000 --- a/nexus/temporal-proto-models-nexusrpc.yaml +++ /dev/null @@ -1,19 +0,0 @@ -nexusrpc: 1.0.0 -services: - WorkflowService: - operations: - SignalWithStartWorkflowExecution: - input: - $dotnetRef: Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionRequest - $goRef: go.temporal.io/api/workflowservice/v1.SignalWithStartWorkflowExecutionRequest - $javaRef: io.temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest - $pythonRef: temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest - $rubyRef: Temporalio::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionRequest - $typescriptRef: '@temporalio/api/workflowservice/v1.SignalWithStartWorkflowExecutionRequest' - output: - $dotnetRef: Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionResponse - $goRef: go.temporal.io/api/workflowservice/v1.SignalWithStartWorkflowExecutionResponse - $javaRef: io.temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse - $pythonRef: temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse - $rubyRef: Temporalio::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionResponse - $typescriptRef: '@temporalio/api/workflowservice/v1.SignalWithStartWorkflowExecutionResponse' diff --git a/nexus/temporal-system.wit b/nexus/temporal-system.wit index 1acb3f7e0..29021b42a 100644 --- a/nexus/temporal-system.wit +++ b/nexus/temporal-system.wit @@ -4,7 +4,7 @@ world system { export workflow-service; } -/// @nexus.endpoint "temporal-system" +/// @nexus.endpoint "__temporal_system" interface workflow-service { use nexus:temporal-types/model@1.0.0.{ duration, From bdb13b28a2238e58f7aa2b4c1dff17d5e45840e9 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 20 May 2026 13:21:05 -0700 Subject: [PATCH 4/8] Simplify system Nexus WIT make target --- Makefile | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/Makefile b/Makefile index f98caab7d..4f06eaf12 100644 --- a/Makefile +++ b/Makefile @@ -35,14 +35,12 @@ PROTO_PATHS = paths=source_relative:$(PROTO_OUT) OAPI_OUT := openapi OAPI3_PATH := .components.schemas.Payload -SYSTEM_NEXUS_OUT := nexus -SYSTEM_NEXUS_WIT := $(SYSTEM_NEXUS_OUT)/temporal-system.wit +SYSTEM_NEXUS_WIT := nexus/temporal-system.wit SYSTEM_NEXUS_SERVICE_PROTO_FILES := $(shell find temporal/api -name "service.proto" | sort) -GO_BUILD_CACHE ?= $(abspath $(PROTO_OUT)/go-build-cache) NEXUS_API_GEN ?= nexus-api-gen $(PROTO_OUT): - mkdir -p $(PROTO_OUT) + mkdir $(PROTO_OUT) ##### Compile proto files for go ##### grpc: buf-lint api-linter buf-breaking clean go-grpc fix-path @@ -114,7 +112,7 @@ api-linter: @api-linter --set-exit-status $(PROTO_IMPORTS) --config $(PROTO_ROOT)/api-linter.yaml --output-format json $(PROTO_FILES) | gojq -r 'map(select(.problems != []) | . as $$file | .problems[] | {rule: .rule_doc_uri, location: "\($$file.file_path):\(.location.start_position.line_number)"}) | group_by(.rule) | .[] | .[0].rule + ":\n" + (map("\t" + .location) | join("\n"))' $(STAMPDIR): - mkdir -p $@ + mkdir $@ $(STAMPDIR)/buf-mod-prune: $(STAMPDIR) buf.yaml printf $(COLOR) "Pruning buf module" @@ -130,27 +128,21 @@ buf-breaking: @(cd $(PROTO_ROOT) && buf breaking --against 'https://github.com/temporalio/api.git#branch=master') ##### Compile system Nexus WIT files ##### -system-nexus-wit: $(SYSTEM_NEXUS_WIT) - -$(SYSTEM_NEXUS_WIT): $(PROTO_FILES) cmd/protoc-gen-system-nexus-wit/main.go cmd/protoc-gen-system-nexus-wit/generator.go | system-nexus-wit-install $(STAMPDIR)/nexus-api-gen-install +system-nexus-wit: system-nexus-wit-install nexus-api-gen-install printf $(COLOR) "Generate system Nexus WIT..." - mkdir -p $(SYSTEM_NEXUS_OUT) protoc -I $(PROTO_ROOT) \ --system-nexus-wit_opt=output=$(SYSTEM_NEXUS_WIT) \ --system-nexus-wit_opt=nexus_api_gen=$(NEXUS_API_GEN) \ --system-nexus-wit_out=. \ $(SYSTEM_NEXUS_SERVICE_PROTO_FILES) -system-nexus-wit-install: | $(PROTO_OUT) +system-nexus-wit-install: printf $(COLOR) "Build and install protoc-gen-system-nexus-wit..." - @cd cmd/protoc-gen-system-nexus-wit && GOCACHE=$(GO_BUILD_CACHE) go install . + @cd cmd/protoc-gen-system-nexus-wit && go install . -$(STAMPDIR)/nexus-api-gen-install: | $(STAMPDIR) +nexus-api-gen-install: printf $(COLOR) "Install nexus-api-gen if missing..." command -v $(NEXUS_API_GEN) >/dev/null || CARGO_NET_GIT_FETCH_WITH_CLI=true cargo install --git https://github.com/temporalio/nexus-api-gen - touch $@ - -nexus-api-gen-install: $(STAMPDIR)/nexus-api-gen-install ##### Clean ##### clean: From 2dd27ca64b1f4febfeec361bc579cb8841666469 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 20 May 2026 13:28:01 -0700 Subject: [PATCH 5/8] Document system Nexus WIT plugin parameters --- cmd/protoc-gen-system-nexus-wit/generator.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cmd/protoc-gen-system-nexus-wit/generator.go b/cmd/protoc-gen-system-nexus-wit/generator.go index 0e7207266..31fdfbb43 100644 --- a/cmd/protoc-gen-system-nexus-wit/generator.go +++ b/cmd/protoc-gen-system-nexus-wit/generator.go @@ -20,6 +20,17 @@ type params struct { input string } +// parseParams parses the comma-separated key=value parameter string provided by protoc. +// +// - output: required. Path to the WIT file to generate, relative to the +// --system-nexus-wit_out directory. Example: "nexus/temporal-system.wit". +// +// - nexus_api_gen: optional. Path to the nexus-api-gen binary. Defaults to +// "nexus-api-gen". +// +// - input: optional. Existing WIT file to update. Defaults to output, so +// existing handwritten annotations and type refinements are preserved when +// regenerating in place. func parseParams(raw string) (params, error) { p := params{ nexusAPIGen: "nexus-api-gen", From ffa051e452976eeba3704481d1ee2b872df05968 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 20 May 2026 13:50:26 -0700 Subject: [PATCH 6/8] Shorten signal-with-start WIT names --- nexus/temporal-system.wit | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nexus/temporal-system.wit b/nexus/temporal-system.wit index 29021b42a..e410c64d7 100644 --- a/nexus/temporal-system.wit +++ b/nexus/temporal-system.wit @@ -24,7 +24,7 @@ interface workflow-service { }; /// @nexus.proto "temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest" - record signal-with-start-workflow-execution-request { + record signal-with-start-workflow-request { /// @nexus.proto-field "workflow_type" workflow: workflow-function, workflow-id: string, @@ -59,7 +59,7 @@ interface workflow-service { } /// @nexus.proto "temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse" - record signal-with-start-workflow-execution-response { + record signal-with-start-workflow-response { run-id: option, started: option, /// @nexus.omit @@ -73,6 +73,6 @@ interface workflow-service { /// typescript="workflow.getExternalWorkflowHandle(request.workflowId, result.runId ?? undefined)" /// @nexus.operation name="SignalWithStartWorkflowExecution" signal-with-start-workflow: func( - request: signal-with-start-workflow-execution-request, - ) -> signal-with-start-workflow-execution-response; + request: signal-with-start-workflow-request, + ) -> signal-with-start-workflow-response; } From abd072dc213e05ced42774dd806f01fab298fec9 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Tue, 26 May 2026 08:13:01 -0700 Subject: [PATCH 7/8] Fix service name --- nexus/temporal-system.wit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nexus/temporal-system.wit b/nexus/temporal-system.wit index aa7e12fce..d17c76fc8 100644 --- a/nexus/temporal-system.wit +++ b/nexus/temporal-system.wit @@ -4,7 +4,7 @@ world system { export workflow-service; } -/// @nexus.endpoint "temporal-system" +/// @nexus.endpoint "__temporal_system" interface workflow-service { use nexus:temporal-types/model@1.0.0.{ duration, From 47a5e622d5981a7e0a2ecbf7f1444dcde11137f5 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Tue, 26 May 2026 08:17:53 -0700 Subject: [PATCH 8/8] Restore Nexus RPC YAML generation --- Makefile | 18 +- cmd/protoc-gen-nexus-rpc-yaml/generator.go | 272 +++++++++++++++++++++ cmd/protoc-gen-nexus-rpc-yaml/go.mod | 12 + cmd/protoc-gen-nexus-rpc-yaml/go.sum | 10 + cmd/protoc-gen-nexus-rpc-yaml/main.go | 13 + nexus/temporal-proto-models-nexusrpc.yaml | 19 ++ 6 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 cmd/protoc-gen-nexus-rpc-yaml/generator.go create mode 100644 cmd/protoc-gen-nexus-rpc-yaml/go.mod create mode 100644 cmd/protoc-gen-nexus-rpc-yaml/go.sum create mode 100644 cmd/protoc-gen-nexus-rpc-yaml/main.go create mode 100644 nexus/temporal-proto-models-nexusrpc.yaml diff --git a/Makefile b/Makefile index 4f06eaf12..8d230e741 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ ci-build: install proto http-api-docs install: grpc-install api-linter-install buf-install # Run all linters and compile proto files. -proto: grpc http-api-docs system-nexus-wit +proto: grpc http-api-docs nexus-rpc-yaml system-nexus-wit ######################################################################## ##### Variables ###### @@ -127,6 +127,22 @@ buf-breaking: @printf $(COLOR) "Run buf breaking changes check against master branch..." @(cd $(PROTO_ROOT) && buf breaking --against 'https://github.com/temporalio/api.git#branch=master') +nexus-rpc-yaml: nexus-rpc-yaml-install + printf $(COLOR) "Generate nexus/temporal-proto-models-nexusrpc.yaml..." + mkdir -p nexus + protoc -I $(PROTO_ROOT) \ + --nexus-rpc-yaml_opt=nexus-rpc_langs_out=nexus/temporal-proto-models-nexusrpc.yaml \ + --nexus-rpc-yaml_opt=python_package_prefix=temporalio.api \ + --nexus-rpc-yaml_opt=typescript_package_prefix=@temporalio/api \ + --nexus-rpc-yaml_opt=include_operation_tags=exposed \ + --nexus-rpc-yaml_out=. \ + temporal/api/workflowservice/v1/* \ + temporal/api/operatorservice/v1/* + +nexus-rpc-yaml-install: + printf $(COLOR) "Build and install protoc-gen-nexus-rpc-yaml..." + @cd cmd/protoc-gen-nexus-rpc-yaml && go install . + ##### Compile system Nexus WIT files ##### system-nexus-wit: system-nexus-wit-install nexus-api-gen-install printf $(COLOR) "Generate system Nexus WIT..." diff --git a/cmd/protoc-gen-nexus-rpc-yaml/generator.go b/cmd/protoc-gen-nexus-rpc-yaml/generator.go new file mode 100644 index 000000000..2e4a939c8 --- /dev/null +++ b/cmd/protoc-gen-nexus-rpc-yaml/generator.go @@ -0,0 +1,272 @@ +package main + +import ( + "fmt" + "slices" + "sort" + "strings" + + nexusannotationsv1 "github.com/nexus-rpc/nexus-proto-annotations/go/nexusannotations/v1" + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/descriptorpb" + "gopkg.in/yaml.v3" +) + +// params holds the parsed protoc plugin options. +// Passed via --nexus-rpc-yaml_opt=key=value (multiple opts are comma-joined by protoc). +// +// - nexus-rpc_langs_out: optional. Output path for the langs YAML. +// If empty, nothing is written. +// Example: "nexus/temporal-proto-models-nexusrpc.yaml" +// +// - python_package_prefix: optional. Dot-separated package prefix for $pythonRef. +// The last two path segments of the go_package ({service}/v{n}) are appended. +// Example: "temporalio.api" → "temporalio.api.workflowservice.v1.TypeName" +// If empty, $pythonRef is omitted. +// +// - typescript_package_prefix: optional. Scoped package prefix for $typescriptRef. +// The last two path segments of the go_package ({service}/v{n}) are appended. +// Example: "@temporalio/api" → "@temporalio/api/workflowservice/v1.TypeName" +// If empty, $typescriptRef is omitted. +// +// - include_operation_tags: optional, repeatable. Only include operations whose tags +// contain at least one of these values. If empty, all annotated operations are included +// (subject to exclude_operation_tags). Specify multiple times for multiple tags. +// Example: include_operation_tags=exposed +// +// - exclude_operation_tags: optional, repeatable. Exclude operations whose tags contain +// any of these values. Applied after include_operation_tags. +// Example: exclude_operation_tags=internal +type params struct { + nexusRpcLangsOut string + pythonPackagePrefix string + typescriptPackagePrefix string + includeOperationTags []string + excludeOperationTags []string +} + +// parseParams parses the comma-separated key=value parameter string provided by protoc. +func parseParams(raw string) (params, error) { + var p params + if raw == "" { + return p, nil + } + for kv := range strings.SplitSeq(raw, ",") { + key, value, ok := strings.Cut(kv, "=") + if !ok { + return p, fmt.Errorf("invalid parameter %q: expected key=value", kv) + } + switch key { + case "nexus-rpc_langs_out": + p.nexusRpcLangsOut = value + case "python_package_prefix": + p.pythonPackagePrefix = value + case "typescript_package_prefix": + p.typescriptPackagePrefix = value + case "include_operation_tags": + p.includeOperationTags = append(p.includeOperationTags, value) + case "exclude_operation_tags": + p.excludeOperationTags = append(p.excludeOperationTags, value) + default: + return p, fmt.Errorf("unknown parameter %q", key) + } + } + return p, nil +} + +// shouldIncludeOperation returns true if the method's nexus operation tags pass +// the include/exclude filters. Mirrors the logic from protoc-gen-go-nexus: +// 1. Method must have the nexus operation extension set. +// 2. If includeOperationTags is non-empty, at least one of the method's tags must match. +// 3. If excludeOperationTags is non-empty, none of the method's tags may match. +func shouldIncludeOperation(p params, m *protogen.Method) bool { + opts, ok := m.Desc.Options().(*descriptorpb.MethodOptions) + if !ok || opts == nil { + return false + } + if !proto.HasExtension(opts, nexusannotationsv1.E_Operation) { + return false + } + tags := proto.GetExtension(opts, nexusannotationsv1.E_Operation).(*nexusannotationsv1.OperationOptions).GetTags() + if len(p.includeOperationTags) > 0 && !slices.ContainsFunc(p.includeOperationTags, func(t string) bool { + return slices.Contains(tags, t) + }) { + return false + } + return !slices.ContainsFunc(p.excludeOperationTags, func(t string) bool { + return slices.Contains(tags, t) + }) +} + +func generate(gen *protogen.Plugin) error { + p, err := parseParams(gen.Request.GetParameter()) + if err != nil { + return err + } + + langsDoc := newDoc() + hasOps := false + + for _, f := range gen.Files { + if !f.Generate { + continue + } + for _, svc := range f.Services { + for _, m := range svc.Methods { + if !shouldIncludeOperation(p, m) { + continue + } + svcName := string(svc.Desc.Name()) + methodName := string(m.Desc.Name()) + hasOps = true + addOperation(langsDoc, svcName, methodName, + langRefs(p, f.Desc, m.Input.Desc), + langRefs(p, f.Desc, m.Output.Desc), + ) + } + } + } + + if !hasOps { + return nil + } + if p.nexusRpcLangsOut != "" { + return writeFile(gen, p.nexusRpcLangsOut, langsDoc) + } + return nil +} + +// langRefs builds the map of language-specific type refs for a message. +// +// Go, Java, dotnet, and Ruby refs are derived from proto file-level package options. +// Python and TypeScript refs require the corresponding prefix params to be set; if +// empty they are omitted. Both use the last two path segments of go_package +// ({service}/v{n}), dropping any intermediate grouping directory. +func langRefs(p params, file protoreflect.FileDescriptor, msg protoreflect.MessageDescriptor) map[string]string { + opts, ok := file.Options().(*descriptorpb.FileOptions) + if !ok || opts == nil { + return nil + } + name := string(msg.Name()) + refs := make(map[string]string) + + if pkg := opts.GetGoPackage(); pkg != "" { + // strip the ";alias" suffix (e.g. "go.temporal.io/api/workflowservice/v1;workflowservice") + pkg = strings.SplitN(pkg, ";", 2)[0] + refs["$goRef"] = pkg + "." + name + + segments := strings.Split(pkg, "/") + if len(segments) >= 2 { + tail := segments[len(segments)-2] + "/" + segments[len(segments)-1] + if p.pythonPackagePrefix != "" { + dotTail := strings.ReplaceAll(tail, "/", ".") + refs["$pythonRef"] = p.pythonPackagePrefix + "." + dotTail + "." + name + } + if p.typescriptPackagePrefix != "" { + refs["$typescriptRef"] = p.typescriptPackagePrefix + "/" + tail + "." + name + } + } + } + if pkg := opts.GetJavaPackage(); pkg != "" { + refs["$javaRef"] = pkg + "." + name + } + if pkg := opts.GetRubyPackage(); pkg != "" { + refs["$rubyRef"] = pkg + "::" + name + } + if pkg := opts.GetCsharpNamespace(); pkg != "" { + refs["$dotnetRef"] = pkg + "." + name + } + if len(refs) == 0 { + return nil + } + return refs +} + +// newDoc creates a yaml.Node document with the "nexusrpc: 1.0.0" header +// and an empty "services" mapping node. +func newDoc() *yaml.Node { + doc := &yaml.Node{Kind: yaml.DocumentNode} + root := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + doc.Content = []*yaml.Node{root} + root.Content = append(root.Content, + scalarNode("nexusrpc"), + scalarNode("1.0.0"), + scalarNode("services"), + &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}, + ) + return doc +} + +// servicesNode returns the "services" mapping node from a doc created by newDoc. +func servicesNode(doc *yaml.Node) *yaml.Node { + root := doc.Content[0] + for i := 0; i < len(root.Content)-1; i += 2 { + if root.Content[i].Value == "services" { + return root.Content[i+1] + } + } + panic("services node not found") +} + +// addOperation inserts a service → operation → {input, output} entry into doc. +// Services and operations are inserted in the order first encountered. +func addOperation(doc *yaml.Node, svcName, methodName string, input, output map[string]string) { + svcs := servicesNode(doc) + + var svcOps *yaml.Node + for i := 0; i < len(svcs.Content)-1; i += 2 { + if svcs.Content[i].Value == svcName { + svcMap := svcs.Content[i+1] + for j := 0; j < len(svcMap.Content)-1; j += 2 { + if svcMap.Content[j].Value == "operations" { + svcOps = svcMap.Content[j+1] + } + } + } + } + if svcOps == nil { + svcMap := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + svcOps = &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + svcMap.Content = append(svcMap.Content, scalarNode("operations"), svcOps) + svcs.Content = append(svcs.Content, scalarNode(svcName), svcMap) + } + + opNode := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + if len(input) > 0 { + opNode.Content = append(opNode.Content, scalarNode("input"), mapNode(input)) + } + if len(output) > 0 { + opNode.Content = append(opNode.Content, scalarNode("output"), mapNode(output)) + } + svcOps.Content = append(svcOps.Content, scalarNode(methodName), opNode) +} + +// mapNode serializes a map[string]string as a yaml mapping node with keys in sorted order. +func mapNode(m map[string]string) *yaml.Node { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + node := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + for _, k := range keys { + node.Content = append(node.Content, scalarNode(k), scalarNode(m[k])) + } + return node +} + +func scalarNode(value string) *yaml.Node { + return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: value} +} + +func writeFile(gen *protogen.Plugin, name string, doc *yaml.Node) error { + f := gen.NewGeneratedFile(name, "") + enc := yaml.NewEncoder(f) + enc.SetIndent(2) + if err := enc.Encode(doc); err != nil { + return err + } + return enc.Close() +} diff --git a/cmd/protoc-gen-nexus-rpc-yaml/go.mod b/cmd/protoc-gen-nexus-rpc-yaml/go.mod new file mode 100644 index 000000000..863c771a6 --- /dev/null +++ b/cmd/protoc-gen-nexus-rpc-yaml/go.mod @@ -0,0 +1,12 @@ +module github.com/temporalio/api/cmd/protoc-gen-nexus-rpc-yaml + +go 1.25.4 + +require ( + google.golang.org/protobuf v1.36.1 + gopkg.in/yaml.v3 v3.0.1 +) + +require github.com/nexus-rpc/nexus-proto-annotations v0.0.0-20260330194009-e558d6edaf84 + +require github.com/google/go-cmp v0.6.0 // indirect diff --git a/cmd/protoc-gen-nexus-rpc-yaml/go.sum b/cmd/protoc-gen-nexus-rpc-yaml/go.sum new file mode 100644 index 000000000..cbc5252ff --- /dev/null +++ b/cmd/protoc-gen-nexus-rpc-yaml/go.sum @@ -0,0 +1,10 @@ +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/nexus-rpc/nexus-proto-annotations v0.0.0-20260330194009-e558d6edaf84 h1:SWHt3Coj0VvF0Km1A0wlY+IjnHKsjQLgO29io84r3wY= +github.com/nexus-rpc/nexus-proto-annotations v0.0.0-20260330194009-e558d6edaf84/go.mod h1:n3UjF1bPCW8llR8tHvbxJ+27yPWrhpo8w/Yg1IOuY0Y= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cmd/protoc-gen-nexus-rpc-yaml/main.go b/cmd/protoc-gen-nexus-rpc-yaml/main.go new file mode 100644 index 000000000..31cdca92f --- /dev/null +++ b/cmd/protoc-gen-nexus-rpc-yaml/main.go @@ -0,0 +1,13 @@ +// protoc-gen-nexus-rpc-yaml is a protoc plugin that generates nexus/temporal-proto-models-nexusrpc.yaml +// from proto service methods annotated with option (nexusannotations.v1.operation).tags = "exposed". +package main + +import ( + "google.golang.org/protobuf/compiler/protogen" +) + +func main() { + protogen.Options{}.Run(func(gen *protogen.Plugin) error { + return generate(gen) + }) +} diff --git a/nexus/temporal-proto-models-nexusrpc.yaml b/nexus/temporal-proto-models-nexusrpc.yaml new file mode 100644 index 000000000..e0761fd15 --- /dev/null +++ b/nexus/temporal-proto-models-nexusrpc.yaml @@ -0,0 +1,19 @@ +nexusrpc: 1.0.0 +services: + WorkflowService: + operations: + SignalWithStartWorkflowExecution: + input: + $dotnetRef: Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionRequest + $goRef: go.temporal.io/api/workflowservice/v1.SignalWithStartWorkflowExecutionRequest + $javaRef: io.temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest + $pythonRef: temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest + $rubyRef: Temporalio::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionRequest + $typescriptRef: '@temporalio/api/workflowservice/v1.SignalWithStartWorkflowExecutionRequest' + output: + $dotnetRef: Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionResponse + $goRef: go.temporal.io/api/workflowservice/v1.SignalWithStartWorkflowExecutionResponse + $javaRef: io.temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse + $pythonRef: temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse + $rubyRef: Temporalio::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionResponse + $typescriptRef: '@temporalio/api/workflowservice/v1.SignalWithStartWorkflowExecutionResponse'