diff --git a/Makefile b/Makefile index b4b33bdb5..e88f9fca5 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 +proto: sync-nexus-annotations grpc http-api-docs nexus-rpc-yaml ######################################################################## ##### Variables ###### @@ -95,6 +95,11 @@ buf-install: printf $(COLOR) "Install/update buf..." go install github.com/bufbuild/buf/cmd/buf@v1.27.0 +##### Sync external proto dependencies ##### +sync-nexus-annotations: + printf $(COLOR) "Sync nexusannotations from buf.build/temporalio/nexus-annotations..." + buf export buf.build/temporalio/nexus-annotations --output . + ##### Linters ##### api-linter: printf $(COLOR) "Run api-linter..." @@ -116,6 +121,23 @@ 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-json-schema-models-nexusrpc.yaml and nexus/temporal-proto-models-nexusrpc.yaml..." + mkdir -p nexus + protoc -I $(PROTO_ROOT) \ + --nexus-rpc-yaml_opt=nexus-rpc_out=nexus/temporal-json-schema-models-nexusrpc.yaml \ + --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 . + ##### Clean ##### clean: printf $(COLOR) "Delete generated go files..." diff --git a/buf.lock b/buf.lock index fccbfb89d..f43352bf2 100644 --- a/buf.lock +++ b/buf.lock @@ -4,10 +4,15 @@ deps: - remote: buf.build owner: googleapis repository: googleapis - commit: 28151c0d0a1641bf938a7672c500e01d - digest: shake256:49215edf8ef57f7863004539deff8834cfb2195113f0b890dd1f67815d9353e28e668019165b9d872395871eeafcbab3ccfdb2b5f11734d3cca95be9e8d139de + commit: 004180b77378443887d3b55cabc00384 + digest: shake256:d26c7c2fd95f0873761af33ca4a0c0d92c8577122b6feb74eb3b0a57ebe47a98ab24a209a0e91945ac4c77204e9da0c2de0020b2cedc27bdbcdea6c431eec69b - remote: buf.build owner: grpc-ecosystem repository: grpc-gateway - commit: 048ae6ff94ca4476b3225904b1078fad - digest: shake256:e5250bf2d999516c02206d757502b902e406f35c099d0e869dc3e4f923f6870fe0805a9974c27df0695462937eae90cd4d9db90bb9a03489412560baa74a87b6 + commit: 6467306b4f624747aaf6266762ee7a1c + digest: shake256:833d648b99b9d2c18b6882ef41aaeb113e76fc38de20dda810c588d133846e6593b4da71b388bcd921b1c7ab41c7acf8f106663d7301ae9e82ceab22cf64b1b7 + - remote: buf.build + owner: temporalio + repository: nexus-annotations + commit: 599b78404fbe4e78b833d527a1d0da40 + digest: shake256:1f41ef11ccbf31d7318b0fe1915550ba6567c99dc94694d60b117fc1ffc756290ba9766c58b403986f079e2b861b42538e5f8cf0495f744cd390d223b81854ca diff --git a/buf.yaml b/buf.yaml index e984c1439..2f2fa5389 100644 --- a/buf.yaml +++ b/buf.yaml @@ -3,11 +3,14 @@ name: buf.build/temporalio/api deps: - buf.build/grpc-ecosystem/grpc-gateway - buf.build/googleapis/googleapis + - buf.build/temporalio/nexus-annotations build: excludes: # Buf won't accept a local dependency on the google protos but we need them # to run api-linter, so just tell buf it ignore it - google + # Same for nexusannotations - local copy for api-linter, BSR dep for buf + - nexusannotations breaking: use: - WIRE_JSON @@ -18,3 +21,4 @@ lint: - DEFAULT ignore: - google + - cmd 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..f44043e7f --- /dev/null +++ b/cmd/protoc-gen-nexus-rpc-yaml/generator.go @@ -0,0 +1,614 @@ +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). +// +// - openapi_ref_prefix: optional. When set, nexus-rpc_out emits a bare $ref to the OpenAPI +// schema rather than inlining schemas under components/schemas. +// Example: "../openapi/openapiv3.yaml#/types/" +// +// - nexus-rpc_out: optional. Output path for nexus-rpc.yaml (relative to --nexus-rpc-yaml_out dir). +// If empty, nexus-rpc.yaml is not written. +// Example: "nexus/nexus-rpc.yaml" +// +// - nexus-rpc_langs_out: optional. Output path for nexus-rpc.langs.yaml. +// If empty, nexus-rpc.langs.yaml is not written. +// Example: "nexus/nexus-rpc.langs.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 { + openAPIRefPrefix string + nexusRpcOut string + 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 "openapi_ref_prefix": + p.openAPIRefPrefix = value + case "nexus-rpc_out": + p.nexusRpcOut = value + 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 + } + + nexusDoc := newDoc() + langsDoc := newDoc() + hasOps := false + collector := newSchemaCollector() + + 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 + + if p.openAPIRefPrefix != "" { + addOperation(nexusDoc, svcName, methodName, + map[string]string{"$ref": p.openAPIRefPrefix + string(m.Input.Desc.Name())}, + map[string]string{"$ref": p.openAPIRefPrefix + string(m.Output.Desc.Name())}, + ) + } else { + inputName := msgSchemaName(m.Input.Desc) + outputName := msgSchemaName(m.Output.Desc) + collector.collect(m.Input.Desc) + collector.collect(m.Output.Desc) + if refs := protoRefMap(langRefs(p, f.Desc, m.Input.Desc)); refs != nil { + collector.protoRefs[inputName] = refs + } + if refs := protoRefMap(langRefs(p, f.Desc, m.Output.Desc)); refs != nil { + collector.protoRefs[outputName] = refs + } + addOperation(nexusDoc, svcName, methodName, + map[string]string{"$ref": "#/types/" + inputName}, + map[string]string{"$ref": "#/types/" + outputName}, + ) + } + + addOperation(langsDoc, svcName, methodName, + langRefs(p, f.Desc, m.Input.Desc), + langRefs(p, f.Desc, m.Output.Desc), + ) + } + } + } + + if !hasOps { + return nil + } + + if p.openAPIRefPrefix == "" { + addTypes(nexusDoc, collector) + } + + if p.nexusRpcOut != "" { + if err := writeFile(gen, p.nexusRpcOut, nexusDoc); err != nil { + return err + } + } + 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 +} + +// schemaCollector recursively collects JSON Schema nodes for proto message types. +type schemaCollector struct { + schemas map[string]*yaml.Node + visited map[string]bool + protoRefs map[string]map[string]string // schema name → {$goProtoRef: ..., $pythonProtoRef: ...} +} + +func newSchemaCollector() *schemaCollector { + return &schemaCollector{ + schemas: make(map[string]*yaml.Node), + visited: make(map[string]bool), + protoRefs: make(map[string]map[string]string), + } +} + +// protoRefMap converts a langRefs map (keys like $goRef, $pythonRef) to proto ref keys +// ($goProtoRef, $pythonProtoRef, …). Returns nil if refs is empty. +func protoRefMap(refs map[string]string) map[string]string { + if len(refs) == 0 { + return nil + } + result := make(map[string]string, len(refs)) + for k, v := range refs { + result[strings.TrimSuffix(k, "Ref")+"ProtoRef"] = v + } + return result +} + +// msgSchemaName converts a proto message full name to a JSON Schema component name. +// It strips the proto package prefix and retains at most the last 2 path segments, +// joining them with "_". This matches the OpenAPI spec naming convention: +// +// temporal.api.common.v1.Payload.ExternalPayloadDetails → Payload_ExternalPayloadDetails +// temporal.api.common.v1.Link.WorkflowEvent.EventReference → WorkflowEvent_EventReference +// temporal.api.common.v1.Link.WorkflowEvent → Link_WorkflowEvent +func msgSchemaName(msg protoreflect.MessageDescriptor) string { + fullName := string(msg.FullName()) + pkg := string(msg.ParentFile().Package()) + if pkg != "" { + fullName = strings.TrimPrefix(fullName, pkg+".") + } + parts := strings.Split(fullName, ".") + if len(parts) > 2 { + parts = parts[len(parts)-2:] + } + return strings.Join(parts, "_") +} + +// collect adds the JSON Schema for msg and all transitively referenced messages to c.schemas. +// Marking visited before processing fields prevents infinite loops from circular references. +func (c *schemaCollector) collect(msg protoreflect.MessageDescriptor) { + name := msgSchemaName(msg) + if c.visited[name] { + return + } + c.visited[name] = true + + objNode := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + objNode.Content = append(objNode.Content, scalarNode("type"), scalarNode("object")) + + fields := msg.Fields() + if fields.Len() > 0 { + propsNode := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + for i := 0; i < fields.Len(); i++ { + field := fields.Get(i) + propsNode.Content = append(propsNode.Content, + scalarNode(field.JSONName()), + c.fieldNode(field), + ) + } + objNode.Content = append(objNode.Content, scalarNode("properties"), propsNode) + } + + c.schemas[name] = objNode +} + +// fieldNode returns the JSON Schema node for a single proto field, handling map, +// repeated (list), and singular cardinalities. +func (c *schemaCollector) fieldNode(field protoreflect.FieldDescriptor) *yaml.Node { + if field.IsMap() { + // map → {type: object, additionalProperties: } + valueField := field.Message().Fields().ByName("value") + node := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + node.Content = append(node.Content, + scalarNode("type"), + scalarNode("object"), + scalarNode("additionalProperties"), + c.kindNode(valueField), + ) + return node + } + item := c.kindNode(field) + if field.IsList() { + // repeated T → {type: array, items: } + node := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + node.Content = append(node.Content, + scalarNode("type"), + scalarNode("array"), + scalarNode("items"), + item, + ) + return node + } + return item +} + +// kindNode returns the JSON Schema for the base (scalar/message/enum) type of a field, +// ignoring any repeated/map cardinality wrapping. +func (c *schemaCollector) kindNode(field protoreflect.FieldDescriptor) *yaml.Node { + switch field.Kind() { + case protoreflect.BoolKind: + return typeNode("boolean") + case protoreflect.StringKind: + return typeNode("string") + case protoreflect.BytesKind: + n := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + n.Content = append(n.Content, + scalarNode("type"), scalarNode("string"), + scalarNode("format"), scalarNode("byte"), + ) + return n + case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind: + n := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + n.Content = append(n.Content, + scalarNode("type"), scalarNode("integer"), + scalarNode("format"), scalarNode("int32"), + ) + return n + case protoreflect.Uint32Kind, protoreflect.Fixed32Kind: + n := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + n.Content = append(n.Content, + scalarNode("type"), scalarNode("integer"), + scalarNode("format"), scalarNode("uint32"), + ) + return n + case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind, + protoreflect.Uint64Kind, protoreflect.Fixed64Kind: + // protojson encodes 64-bit integers as decimal strings to avoid JS precision loss. + return typeNode("string") + case protoreflect.FloatKind: + n := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + n.Content = append(n.Content, + scalarNode("type"), scalarNode("number"), + scalarNode("format"), scalarNode("float"), + ) + return n + case protoreflect.DoubleKind: + n := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + n.Content = append(n.Content, + scalarNode("type"), scalarNode("number"), + scalarNode("format"), scalarNode("double"), + ) + return n + case protoreflect.EnumKind: + return enumNode(field.Enum()) + case protoreflect.MessageKind, protoreflect.GroupKind: + return c.msgRefNode(field.Message()) + } + return typeNode("object") +} + +// msgRefNode returns an inline schema for well-known proto types, or a $ref for all others. +// It also triggers recursive schema collection for non-well-known message types. +func (c *schemaCollector) msgRefNode(msg protoreflect.MessageDescriptor) *yaml.Node { + switch string(msg.FullName()) { + case "google.protobuf.Duration": + n := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + n.Content = append(n.Content, + scalarNode("type"), scalarNode("string"), + scalarNode("pattern"), scalarNode(`^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$`), + ) + return n + case "google.protobuf.Timestamp": + n := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + n.Content = append(n.Content, + scalarNode("type"), scalarNode("string"), + scalarNode("format"), scalarNode("date-time"), + ) + return n + case "google.protobuf.Empty": + return typeNode("object") + case "google.protobuf.Any", + "google.protobuf.Struct", + "google.protobuf.Value": + return typeNode("object") + case "google.protobuf.ListValue": + n := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + n.Content = append(n.Content, + scalarNode("type"), scalarNode("array"), + scalarNode("items"), typeNode("object"), + ) + return n + case "google.protobuf.BoolValue": + return typeNode("boolean") + case "google.protobuf.StringValue": + return typeNode("string") + case "google.protobuf.BytesValue": + n := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + n.Content = append(n.Content, + scalarNode("type"), scalarNode("string"), + scalarNode("format"), scalarNode("byte"), + ) + return n + case "google.protobuf.Int32Value", + "google.protobuf.UInt32Value": + return typeNode("integer") + case "google.protobuf.Int64Value", + "google.protobuf.UInt64Value": + return typeNode("string") + case "google.protobuf.FloatValue": + n := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + n.Content = append(n.Content, + scalarNode("type"), scalarNode("number"), + scalarNode("format"), scalarNode("float"), + ) + return n + case "google.protobuf.DoubleValue": + n := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + n.Content = append(n.Content, + scalarNode("type"), scalarNode("number"), + scalarNode("format"), scalarNode("double"), + ) + return n + } + name := msgSchemaName(msg) + c.collect(msg) + n := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + n.Content = append(n.Content, scalarNode("$ref"), scalarNode("#/types/"+name)) + return n +} + +// enumNode builds a JSON Schema enum node listing all enum value names. +func enumNode(e protoreflect.EnumDescriptor) *yaml.Node { + n := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + seqNode := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"} + values := e.Values() + for i := 0; i < values.Len(); i++ { + seqNode.Content = append(seqNode.Content, scalarNode(string(values.Get(i).Name()))) + } + n.Content = append(n.Content, + scalarNode("enum"), seqNode, + scalarNode("type"), scalarNode("string"), + scalarNode("format"), scalarNode("enum"), + ) + return n +} + +func typeNode(t string) *yaml.Node { + n := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + n.Content = append(n.Content, scalarNode("type"), scalarNode(t)) + return n +} + +// addTypes inserts a "types:" section into doc, positioned between the +// "nexusrpc" version header and the "services" block. Types are sorted +// alphabetically for deterministic output. +func addTypes(doc *yaml.Node, c *schemaCollector) { + root := doc.Content[0] + + names := make([]string, 0, len(c.schemas)) + for name := range c.schemas { + names = append(names, name) + } + sort.Strings(names) + + typesNode := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + for _, name := range names { + schema := c.schemas[name] + if refs, ok := c.protoRefs[name]; ok { + refKeys := make([]string, 0, len(refs)) + for k := range refs { + refKeys = append(refKeys, k) + } + sort.Strings(refKeys) + prefix := make([]*yaml.Node, 0, 2*len(refs)) + for _, k := range refKeys { + prefix = append(prefix, scalarNode(k), scalarNode(refs[k])) + } + schema.Content = append(prefix, schema.Content...) + } + typesNode.Content = append(typesNode.Content, scalarNode(name), schema) + } + + // root.Content layout: [nexusrpc, 1.0.0, services, {...}] + // After insert: [nexusrpc, 1.0.0, types, {...}, services, {...}] + newContent := make([]*yaml.Node, 0, len(root.Content)+2) + newContent = append(newContent, root.Content[:2]...) + newContent = append(newContent, scalarNode("types"), typesNode) + newContent = append(newContent, root.Content[2:]...) + root.Content = newContent +} + +// 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..23c3a8e7c --- /dev/null +++ b/cmd/protoc-gen-nexus-rpc-yaml/main.go @@ -0,0 +1,14 @@ +// protoc-gen-nexus-rpc-yaml is a protoc plugin that generates nexus/nexus-rpc.yaml +// and nexus/nexus-rpc.langs.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-json-schema-models-nexusrpc.yaml b/nexus/temporal-json-schema-models-nexusrpc.yaml new file mode 100644 index 000000000..924e448af --- /dev/null +++ b/nexus/temporal-json-schema-models-nexusrpc.yaml @@ -0,0 +1,445 @@ +nexusrpc: 1.0.0 +types: + Deployment: + type: object + properties: + seriesName: + type: string + buildId: + type: string + Header: + type: object + properties: + fields: + type: object + additionalProperties: + $ref: '#/types/Payload' + Link: + type: object + properties: + workflowEvent: + $ref: '#/types/Link_WorkflowEvent' + batchJob: + $ref: '#/types/Link_BatchJob' + activity: + $ref: '#/types/Link_Activity' + Link_Activity: + type: object + properties: + namespace: + type: string + activityId: + type: string + runId: + type: string + Link_BatchJob: + type: object + properties: + jobId: + type: string + Link_WorkflowEvent: + type: object + properties: + namespace: + type: string + workflowId: + type: string + runId: + type: string + eventRef: + $ref: '#/types/WorkflowEvent_EventReference' + requestIdRef: + $ref: '#/types/WorkflowEvent_RequestIdReference' + Memo: + type: object + properties: + fields: + type: object + additionalProperties: + $ref: '#/types/Payload' + Payload: + type: object + properties: + metadata: + type: object + additionalProperties: + type: string + format: byte + data: + type: string + format: byte + externalPayloads: + type: array + items: + $ref: '#/types/Payload_ExternalPayloadDetails' + Payload_ExternalPayloadDetails: + type: object + properties: + sizeBytes: + type: string + Payloads: + type: object + properties: + payloads: + type: array + items: + $ref: '#/types/Payload' + Priority: + type: object + properties: + priorityKey: + type: integer + format: int32 + fairnessKey: + type: string + fairnessWeight: + type: number + format: float + RetryPolicy: + type: object + properties: + initialInterval: + type: string + pattern: ^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$ + backoffCoefficient: + type: number + format: double + maximumInterval: + type: string + pattern: ^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$ + maximumAttempts: + type: integer + format: int32 + nonRetryableErrorTypes: + type: array + items: + type: string + SearchAttributes: + type: object + properties: + indexedFields: + type: object + additionalProperties: + $ref: '#/types/Payload' + SignalWithStartWorkflowExecutionRequest: + $dotnetProtoRef: Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionRequest + $goProtoRef: go.temporal.io/api/workflowservice/v1.SignalWithStartWorkflowExecutionRequest + $javaProtoRef: io.temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest + $pythonProtoRef: temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest + $rubyProtoRef: Temporalio::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionRequest + $typescriptProtoRef: '@temporalio/api/workflowservice/v1.SignalWithStartWorkflowExecutionRequest' + type: object + properties: + namespace: + type: string + workflowId: + type: string + workflowType: + $ref: '#/types/WorkflowType' + taskQueue: + $ref: '#/types/TaskQueue' + input: + $ref: '#/types/Payloads' + workflowExecutionTimeout: + type: string + pattern: ^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$ + workflowRunTimeout: + type: string + pattern: ^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$ + workflowTaskTimeout: + type: string + pattern: ^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$ + identity: + type: string + requestId: + type: string + workflowIdReusePolicy: + enum: + - WORKFLOW_ID_REUSE_POLICY_UNSPECIFIED + - WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE + - WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY + - WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE + - WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING + type: string + format: enum + workflowIdConflictPolicy: + enum: + - WORKFLOW_ID_CONFLICT_POLICY_UNSPECIFIED + - WORKFLOW_ID_CONFLICT_POLICY_FAIL + - WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING + - WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING + type: string + format: enum + signalName: + type: string + signalInput: + $ref: '#/types/Payloads' + control: + type: string + retryPolicy: + $ref: '#/types/RetryPolicy' + cronSchedule: + type: string + memo: + $ref: '#/types/Memo' + searchAttributes: + $ref: '#/types/SearchAttributes' + header: + $ref: '#/types/Header' + workflowStartDelay: + type: string + pattern: ^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$ + userMetadata: + $ref: '#/types/UserMetadata' + links: + type: array + items: + $ref: '#/types/Link' + versioningOverride: + $ref: '#/types/VersioningOverride' + priority: + $ref: '#/types/Priority' + timeSkippingConfig: + $ref: '#/types/TimeSkippingConfig' + SignalWithStartWorkflowExecutionResponse: + $dotnetProtoRef: Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionResponse + $goProtoRef: go.temporal.io/api/workflowservice/v1.SignalWithStartWorkflowExecutionResponse + $javaProtoRef: io.temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse + $pythonProtoRef: temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse + $rubyProtoRef: Temporalio::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionResponse + $typescriptProtoRef: '@temporalio/api/workflowservice/v1.SignalWithStartWorkflowExecutionResponse' + type: object + properties: + runId: + type: string + started: + type: boolean + signalLink: + $ref: '#/types/Link' + TaskQueue: + type: object + properties: + name: + type: string + kind: + enum: + - TASK_QUEUE_KIND_UNSPECIFIED + - TASK_QUEUE_KIND_NORMAL + - TASK_QUEUE_KIND_STICKY + - TASK_QUEUE_KIND_WORKER_COMMANDS + type: string + format: enum + normalName: + type: string + TimeSkippingConfig: + type: object + properties: + enabled: + type: boolean + disablePropagation: + type: boolean + maxSkippedDuration: + type: string + pattern: ^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$ + maxElapsedDuration: + type: string + pattern: ^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$ + maxTargetTime: + type: string + format: date-time + UserMetadata: + type: object + properties: + summary: + $ref: '#/types/Payload' + details: + $ref: '#/types/Payload' + VersioningOverride: + type: object + properties: + pinned: + $ref: '#/types/VersioningOverride_PinnedOverride' + autoUpgrade: + type: boolean + behavior: + enum: + - VERSIONING_BEHAVIOR_UNSPECIFIED + - VERSIONING_BEHAVIOR_PINNED + - VERSIONING_BEHAVIOR_AUTO_UPGRADE + type: string + format: enum + deployment: + $ref: '#/types/Deployment' + pinnedVersion: + type: string + VersioningOverride_PinnedOverride: + type: object + properties: + behavior: + enum: + - PINNED_OVERRIDE_BEHAVIOR_UNSPECIFIED + - PINNED_OVERRIDE_BEHAVIOR_PINNED + type: string + format: enum + version: + $ref: '#/types/WorkerDeploymentVersion' + WorkerDeploymentVersion: + type: object + properties: + buildId: + type: string + deploymentName: + type: string + WorkflowEvent_EventReference: + type: object + properties: + eventId: + type: string + eventType: + enum: + - EVENT_TYPE_UNSPECIFIED + - EVENT_TYPE_WORKFLOW_EXECUTION_STARTED + - EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED + - EVENT_TYPE_WORKFLOW_EXECUTION_FAILED + - EVENT_TYPE_WORKFLOW_EXECUTION_TIMED_OUT + - EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + - EVENT_TYPE_WORKFLOW_TASK_STARTED + - EVENT_TYPE_WORKFLOW_TASK_COMPLETED + - EVENT_TYPE_WORKFLOW_TASK_TIMED_OUT + - EVENT_TYPE_WORKFLOW_TASK_FAILED + - EVENT_TYPE_ACTIVITY_TASK_SCHEDULED + - EVENT_TYPE_ACTIVITY_TASK_STARTED + - EVENT_TYPE_ACTIVITY_TASK_COMPLETED + - EVENT_TYPE_ACTIVITY_TASK_FAILED + - EVENT_TYPE_ACTIVITY_TASK_TIMED_OUT + - EVENT_TYPE_ACTIVITY_TASK_CANCEL_REQUESTED + - EVENT_TYPE_ACTIVITY_TASK_CANCELED + - EVENT_TYPE_TIMER_STARTED + - EVENT_TYPE_TIMER_FIRED + - EVENT_TYPE_TIMER_CANCELED + - EVENT_TYPE_WORKFLOW_EXECUTION_CANCEL_REQUESTED + - EVENT_TYPE_WORKFLOW_EXECUTION_CANCELED + - EVENT_TYPE_REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED + - EVENT_TYPE_REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION_FAILED + - EVENT_TYPE_EXTERNAL_WORKFLOW_EXECUTION_CANCEL_REQUESTED + - EVENT_TYPE_MARKER_RECORDED + - EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED + - EVENT_TYPE_WORKFLOW_EXECUTION_TERMINATED + - EVENT_TYPE_WORKFLOW_EXECUTION_CONTINUED_AS_NEW + - EVENT_TYPE_START_CHILD_WORKFLOW_EXECUTION_INITIATED + - EVENT_TYPE_START_CHILD_WORKFLOW_EXECUTION_FAILED + - EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_STARTED + - EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_COMPLETED + - EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_FAILED + - EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_CANCELED + - EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_TIMED_OUT + - EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_TERMINATED + - EVENT_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED + - EVENT_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_FAILED + - EVENT_TYPE_EXTERNAL_WORKFLOW_EXECUTION_SIGNALED + - EVENT_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES + - EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_ADMITTED + - EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_ACCEPTED + - EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_REJECTED + - EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_COMPLETED + - EVENT_TYPE_WORKFLOW_PROPERTIES_MODIFIED_EXTERNALLY + - EVENT_TYPE_ACTIVITY_PROPERTIES_MODIFIED_EXTERNALLY + - EVENT_TYPE_WORKFLOW_PROPERTIES_MODIFIED + - EVENT_TYPE_NEXUS_OPERATION_SCHEDULED + - EVENT_TYPE_NEXUS_OPERATION_STARTED + - EVENT_TYPE_NEXUS_OPERATION_COMPLETED + - EVENT_TYPE_NEXUS_OPERATION_FAILED + - EVENT_TYPE_NEXUS_OPERATION_CANCELED + - EVENT_TYPE_NEXUS_OPERATION_TIMED_OUT + - EVENT_TYPE_NEXUS_OPERATION_CANCEL_REQUESTED + - EVENT_TYPE_WORKFLOW_EXECUTION_OPTIONS_UPDATED + - EVENT_TYPE_NEXUS_OPERATION_CANCEL_REQUEST_COMPLETED + - EVENT_TYPE_NEXUS_OPERATION_CANCEL_REQUEST_FAILED + - EVENT_TYPE_WORKFLOW_EXECUTION_PAUSED + - EVENT_TYPE_WORKFLOW_EXECUTION_UNPAUSED + - EVENT_TYPE_WORKFLOW_EXECUTION_TIME_SKIPPING_TRANSITIONED + type: string + format: enum + WorkflowEvent_RequestIdReference: + type: object + properties: + requestId: + type: string + eventType: + enum: + - EVENT_TYPE_UNSPECIFIED + - EVENT_TYPE_WORKFLOW_EXECUTION_STARTED + - EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED + - EVENT_TYPE_WORKFLOW_EXECUTION_FAILED + - EVENT_TYPE_WORKFLOW_EXECUTION_TIMED_OUT + - EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + - EVENT_TYPE_WORKFLOW_TASK_STARTED + - EVENT_TYPE_WORKFLOW_TASK_COMPLETED + - EVENT_TYPE_WORKFLOW_TASK_TIMED_OUT + - EVENT_TYPE_WORKFLOW_TASK_FAILED + - EVENT_TYPE_ACTIVITY_TASK_SCHEDULED + - EVENT_TYPE_ACTIVITY_TASK_STARTED + - EVENT_TYPE_ACTIVITY_TASK_COMPLETED + - EVENT_TYPE_ACTIVITY_TASK_FAILED + - EVENT_TYPE_ACTIVITY_TASK_TIMED_OUT + - EVENT_TYPE_ACTIVITY_TASK_CANCEL_REQUESTED + - EVENT_TYPE_ACTIVITY_TASK_CANCELED + - EVENT_TYPE_TIMER_STARTED + - EVENT_TYPE_TIMER_FIRED + - EVENT_TYPE_TIMER_CANCELED + - EVENT_TYPE_WORKFLOW_EXECUTION_CANCEL_REQUESTED + - EVENT_TYPE_WORKFLOW_EXECUTION_CANCELED + - EVENT_TYPE_REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED + - EVENT_TYPE_REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION_FAILED + - EVENT_TYPE_EXTERNAL_WORKFLOW_EXECUTION_CANCEL_REQUESTED + - EVENT_TYPE_MARKER_RECORDED + - EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED + - EVENT_TYPE_WORKFLOW_EXECUTION_TERMINATED + - EVENT_TYPE_WORKFLOW_EXECUTION_CONTINUED_AS_NEW + - EVENT_TYPE_START_CHILD_WORKFLOW_EXECUTION_INITIATED + - EVENT_TYPE_START_CHILD_WORKFLOW_EXECUTION_FAILED + - EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_STARTED + - EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_COMPLETED + - EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_FAILED + - EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_CANCELED + - EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_TIMED_OUT + - EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_TERMINATED + - EVENT_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED + - EVENT_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_FAILED + - EVENT_TYPE_EXTERNAL_WORKFLOW_EXECUTION_SIGNALED + - EVENT_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES + - EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_ADMITTED + - EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_ACCEPTED + - EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_REJECTED + - EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_COMPLETED + - EVENT_TYPE_WORKFLOW_PROPERTIES_MODIFIED_EXTERNALLY + - EVENT_TYPE_ACTIVITY_PROPERTIES_MODIFIED_EXTERNALLY + - EVENT_TYPE_WORKFLOW_PROPERTIES_MODIFIED + - EVENT_TYPE_NEXUS_OPERATION_SCHEDULED + - EVENT_TYPE_NEXUS_OPERATION_STARTED + - EVENT_TYPE_NEXUS_OPERATION_COMPLETED + - EVENT_TYPE_NEXUS_OPERATION_FAILED + - EVENT_TYPE_NEXUS_OPERATION_CANCELED + - EVENT_TYPE_NEXUS_OPERATION_TIMED_OUT + - EVENT_TYPE_NEXUS_OPERATION_CANCEL_REQUESTED + - EVENT_TYPE_WORKFLOW_EXECUTION_OPTIONS_UPDATED + - EVENT_TYPE_NEXUS_OPERATION_CANCEL_REQUEST_COMPLETED + - EVENT_TYPE_NEXUS_OPERATION_CANCEL_REQUEST_FAILED + - EVENT_TYPE_WORKFLOW_EXECUTION_PAUSED + - EVENT_TYPE_WORKFLOW_EXECUTION_UNPAUSED + - EVENT_TYPE_WORKFLOW_EXECUTION_TIME_SKIPPING_TRANSITIONED + type: string + format: enum + WorkflowType: + type: object + properties: + name: + type: string +services: + WorkflowService: + operations: + SignalWithStartWorkflowExecution: + input: + $ref: '#/types/SignalWithStartWorkflowExecutionRequest' + output: + $ref: '#/types/SignalWithStartWorkflowExecutionResponse' 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' diff --git a/nexusannotations/v1/options.proto b/nexusannotations/v1/options.proto new file mode 100644 index 000000000..e137896bb --- /dev/null +++ b/nexusannotations/v1/options.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +package nexusannotations.v1; + +import "google/protobuf/descriptor.proto"; + +option go_package = "github.com/nexus-rpc/nexus-proto-annotations/go/nexusannotations/v1"; + +extend google.protobuf.ServiceOptions { + optional ServiceOptions service = 8233; +} + +extend google.protobuf.MethodOptions { + optional OperationOptions operation = 8234; +} + +message OperationOptions { + // Nexus operation name (defaults to proto method name). + string name = 1; + // Tags to attach to the operation. Used by code generators to include and exclude operations. + repeated string tags = 2; +} + +message ServiceOptions { + // Nexus service name (defaults to proto service full name). + string name = 1; + // Tags to attach to the service. Used by code generators to include and exclude services. + repeated string tags = 2; +} diff --git a/temporal/api/workflowservice/v1/service.proto b/temporal/api/workflowservice/v1/service.proto index 5c1e900a3..78faee25b 100644 --- a/temporal/api/workflowservice/v1/service.proto +++ b/temporal/api/workflowservice/v1/service.proto @@ -10,9 +10,10 @@ option ruby_package = "Temporalio::Api::WorkflowService::V1"; option csharp_namespace = "Temporalio.Api.WorkflowService.V1"; -import "temporal/api/workflowservice/v1/request_response.proto"; import "google/api/annotations.proto"; +import "nexusannotations/v1/options.proto"; import "temporal/api/protometa/v1/annotations.proto"; +import "temporal/api/workflowservice/v1/request_response.proto"; // WorkflowService API defines how Temporal SDKs and other clients interact with the Temporal server // to create and interact with workflows and activities. @@ -487,6 +488,8 @@ service WorkflowService { // (-- api-linter: core::0136::prepositions=disabled // aip.dev/not-precedent: "With" is used to indicate combined operation. --) rpc SignalWithStartWorkflowExecution (SignalWithStartWorkflowExecutionRequest) returns (SignalWithStartWorkflowExecutionResponse) { + option (nexusannotations.v1.operation).tags = "exposed"; + option (google.api.http) = { post: "/namespaces/{namespace}/workflows/{workflow_id}/signal-with-start/{signal_name}" body: "*"