diff --git a/cmd/protoc-gen-openapi/examples/tests/noannotations/openapi_include_all_methods.yaml b/cmd/protoc-gen-openapi/examples/tests/noannotations/openapi_include_all_methods.yaml new file mode 100644 index 00000000..7b46dde9 --- /dev/null +++ b/cmd/protoc-gen-openapi/examples/tests/noannotations/openapi_include_all_methods.yaml @@ -0,0 +1,114 @@ +# Generated with protoc-gen-openapi +# https://github.com/fern-api/protoc-gen-openapi/tree/master/cmd/protoc-gen-openapi + +openapi: 3.0.3 +info: + title: "" + version: 0.0.1 +paths: + /tests.noannotations.message.v1.Messaging2/UpdateMessage: + post: + tags: + - Messaging2 + operationId: Messaging2_UpdateMessage + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/GoogleRpcStatus' + x-fern-sdk-group-name: Messaging2 + x-fern-sdk-method-name: UpdateMessage + /v1/messages/{message_id}: + patch: + tags: + - Messaging1 + operationId: Messaging1_UpdateMessage + parameters: + - name: message_id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/GoogleRpcStatus' + x-fern-sdk-group-name: Messaging1 + x-fern-sdk-method-name: UpdateMessage +components: + schemas: + GoogleProtobufAny: + type: object + properties: + '@type': + type: string + description: The type of the serialized message. + additionalProperties: true + description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message. + x-fern-encoding: + proto: + type: google.protobuf.Any + GoogleRpcStatus: + type: object + properties: + code: + type: integer + description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. + format: int32 + message: + type: string + description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. + details: + type: array + items: + $ref: '#/components/schemas/GoogleProtobufAny' + description: A list of messages that carry the error details. There is a common set of message types for APIs to use. + description: 'The `Status` type defines a logical error model that is suitable for different programming environments, including REST APIs and RPC APIs. It is used by [gRPC](https://github.com/grpc). Each `Status` message contains three pieces of data: error code, error message, and error details. You can find out more about this error model and how to work with it in the [API Design Guide](https://cloud.google.com/apis/design/errors).' + Message: + type: object + properties: + id: + type: integer + format: int64 + x-fern-encoding: + proto: + type: google.protobuf.Int64Value + label: + type: string + x-fern-encoding: + proto: + type: google.protobuf.StringValue + x-fern-type-name: Message + source: tests/noannotations/message.proto +tags: + - name: Messaging1 + - name: Messaging2 diff --git a/cmd/protoc-gen-openapi/generator/generator.go b/cmd/protoc-gen-openapi/generator/generator.go index eccd61a8..30f6310f 100644 --- a/cmd/protoc-gen-openapi/generator/generator.go +++ b/cmd/protoc-gen-openapi/generator/generator.go @@ -37,17 +37,18 @@ import ( ) type Configuration struct { - Version *string - Title *string - Description *string - Naming *string - FQSchemaNaming *bool - EnumType *string - CircularDepth *int - DefaultResponse *bool - OutputMode *string - SourceRoot *string - FlattenOneofs *bool + Version *string + Title *string + Description *string + Naming *string + FQSchemaNaming *bool + EnumType *string + CircularDepth *int + DefaultResponse *bool + OutputMode *string + SourceRoot *string + FlattenOneofs *bool + IncludeAllMethods *bool } const ( @@ -740,6 +741,54 @@ func (g *OpenAPIv3Generator) addPathsToDocumentV3(d *v3.Document, services []*pr rules = append(rules, rule.AdditionalBindings...) } + // When include_all_methods is enabled and there are no HTTP annotations, + // generate a default POST path using the fully-qualified gRPC method name. + if len(rules) == 0 && g.conf.IncludeAllMethods != nil && *g.conf.IncludeAllMethods { + annotationsCount++ + path := "/" + string(service.Desc.FullName()) + "/" + string(method.Desc.Name()) + methodName := "POST" + body := "*" + + defaultHost := proto.GetExtension(service.Desc.Options(), annotations.E_DefaultHost).(string) + + op, path2 := g.buildOperationV3( + d, operationID, service.GoName, comment, defaultHost, path, body, inputMessage, outputMessage) + + extFernSummary := proto.GetExtension(method.Desc.Options(), fernoptions.E_Summary) + if extFernSummary != nil { + if summary, ok := extFernSummary.(string); ok && summary != "" { + op.Summary = summary + } + } + + op.SpecificationExtension = append(op.SpecificationExtension, + &v3.NamedAny{ + Name: "x-fern-sdk-group-name", + Value: &v3.Any{Yaml: service.GoName}, + }, + &v3.NamedAny{ + Name: "x-fern-sdk-method-name", + Value: &v3.Any{Yaml: method.GoName}, + }, + ) + + if requestMessageUsageCount[string(inputMessage.Desc.FullName())] == 1 { + op.SpecificationExtension = append(op.SpecificationExtension, + &v3.NamedAny{ + Name: "x-fern-request-name", + Value: &v3.Any{Yaml: string(inputMessage.Desc.Name())}, + }, + ) + } + + extOperation := proto.GetExtension(method.Desc.Options(), v3.E_Operation) + if extOperation != nil { + proto.Merge(op, extOperation.(*v3.Operation)) + } + + g.addOperationToDocumentV3(d, op, path2, methodName) + } + for _, rule := range rules { var path string var methodName string @@ -916,7 +965,7 @@ func (g *OpenAPIv3Generator) addSchemasForMessagesToDocumentV3(d *v3.Document, m } var required []string - + for _, field := range message.Fields { // Skip fields that are part of an explicit oneOf (unless flattening is enabled). // Proto3 optional fields create synthetic oneofs that should be @@ -924,7 +973,7 @@ func (g *OpenAPIv3Generator) addSchemasForMessagesToDocumentV3(d *v3.Document, m if field.Oneof != nil && !field.Oneof.Desc.IsSynthetic() && !*g.conf.FlattenOneofs { continue } - + // Get the field description from the comments. description := g.filterCommentString(field.Comments.Leading) // Check the field annotations to see if this is a readonly or writeonly field. @@ -1055,7 +1104,6 @@ func (g *OpenAPIv3Generator) createEnumSchema(enumName string, enumValues protor } } - // addOneOfFieldsToSchema adds oneOf fields to the schema func (g *OpenAPIv3Generator) addOneOfFieldsToSchema(d *v3.Document, oneofs []*protogen.Oneof, schema *v3.Schema, schemaName string, filename string) { if oneofs == nil { @@ -1079,17 +1127,17 @@ func (g *OpenAPIv3Generator) addOneOfFieldsToSchema(d *v3.Document, oneofs []*pr if len(schema.Properties.AdditionalProperties) == 0 { // Flatten all oneofs to the message level allOneOfSchemas := make([]*v3.SchemaOrReference, 0) - + for _, oneOfProto := range oneofs { oneOfSchemas := make([]*v3.SchemaOrReference, 0, len(oneOfProto.Fields)) - + for _, fieldProto := range oneOfProto.Fields { fieldName := g.reflect.formatFieldName(fieldProto.Desc) - + // For google.protobuf.Empty, create a simple object schema - if fieldProto.Desc.Kind() == protoreflect.MessageKind && - g.reflect.fullMessageTypeName(fieldProto.Desc.Message()) == ".google.protobuf.Empty" { - + if fieldProto.Desc.Kind() == protoreflect.MessageKind && + g.reflect.fullMessageTypeName(fieldProto.Desc.Message()) == ".google.protobuf.Empty" { + emptySchema := &v3.SchemaOrReference{ Oneof: &v3.SchemaOrReference_Schema{ Schema: &v3.Schema{ @@ -1113,11 +1161,11 @@ func (g *OpenAPIv3Generator) addOneOfFieldsToSchema(d *v3.Document, oneofs []*pr }, } oneOfSchemas = append(oneOfSchemas, emptySchema) - + } else { // For other field types, wrap the field schema in an object with the specific property name fieldSchema := g.reflect.schemaOrReferenceForField(fieldProto.Desc) - + objectSchema := &v3.SchemaOrReference{ Oneof: &v3.SchemaOrReference_Schema{ Schema: &v3.Schema{ @@ -1142,11 +1190,11 @@ func (g *OpenAPIv3Generator) addOneOfFieldsToSchema(d *v3.Document, oneofs []*pr oneOfSchemas = append(oneOfSchemas, objectSchema) } } - + // Add all schemas from this oneof to the combined list allOneOfSchemas = append(allOneOfSchemas, oneOfSchemas...) } - + // Add null as an option nullSchema := &v3.SchemaOrReference{ Oneof: &v3.SchemaOrReference_Schema{ @@ -1156,12 +1204,12 @@ func (g *OpenAPIv3Generator) addOneOfFieldsToSchema(d *v3.Document, oneofs []*pr }, } allOneOfSchemas = append(allOneOfSchemas, nullSchema) - + // Replace the schema with the oneOf at the top level schema.OneOf = allOneOfSchemas schema.Type = "" schema.Properties = nil - + return } @@ -1169,14 +1217,14 @@ func (g *OpenAPIv3Generator) addOneOfFieldsToSchema(d *v3.Document, oneofs []*pr for _, oneOfProto := range oneofs { oneOfFieldName := string(oneOfProto.Desc.Name()) oneOfSchemas := make([]*v3.SchemaOrReference, 0, len(oneOfProto.Fields)) - + for _, fieldProto := range oneOfProto.Fields { fieldName := g.reflect.formatFieldName(fieldProto.Desc) - + // For google.protobuf.Empty, create a simple object schema - if fieldProto.Desc.Kind() == protoreflect.MessageKind && - g.reflect.fullMessageTypeName(fieldProto.Desc.Message()) == ".google.protobuf.Empty" { - + if fieldProto.Desc.Kind() == protoreflect.MessageKind && + g.reflect.fullMessageTypeName(fieldProto.Desc.Message()) == ".google.protobuf.Empty" { + emptySchema := &v3.SchemaOrReference{ Oneof: &v3.SchemaOrReference_Schema{ Schema: &v3.Schema{ @@ -1185,11 +1233,11 @@ func (g *OpenAPIv3Generator) addOneOfFieldsToSchema(d *v3.Document, oneofs []*pr }, } oneOfSchemas = append(oneOfSchemas, emptySchema) - + } else { // For other field types, wrap the field schema in an object with the specific property name fieldSchema := g.reflect.schemaOrReferenceForField(fieldProto.Desc) - + objectSchema := &v3.SchemaOrReference{ Oneof: &v3.SchemaOrReference_Schema{ Schema: &v3.Schema{ @@ -1214,7 +1262,7 @@ func (g *OpenAPIv3Generator) addOneOfFieldsToSchema(d *v3.Document, oneofs []*pr oneOfSchemas = append(oneOfSchemas, objectSchema) } } - + // Add null as an option nullSchema := &v3.SchemaOrReference{ Oneof: &v3.SchemaOrReference_Schema{ @@ -1224,7 +1272,7 @@ func (g *OpenAPIv3Generator) addOneOfFieldsToSchema(d *v3.Document, oneofs []*pr }, } oneOfSchemas = append(oneOfSchemas, nullSchema) - + // Create the oneOf schema oneOfSchema := &v3.SchemaOrReference{ Oneof: &v3.SchemaOrReference_Schema{ @@ -1233,7 +1281,7 @@ func (g *OpenAPIv3Generator) addOneOfFieldsToSchema(d *v3.Document, oneofs []*pr }, }, } - + schema.Properties.AdditionalProperties = append( schema.Properties.AdditionalProperties, &v3.NamedSchemaOrReference{ diff --git a/cmd/protoc-gen-openapi/main.go b/cmd/protoc-gen-openapi/main.go index 60fc9c51..7ac99a2b 100644 --- a/cmd/protoc-gen-openapi/main.go +++ b/cmd/protoc-gen-openapi/main.go @@ -44,7 +44,8 @@ func main() { DefaultResponse: flags.Bool("default_response", true, `add default response. If "true", automatically adds a default response to operations which use the google.rpc.Status message. Useful if you use envoy or grpc-gateway to transcode as they use this type for their default error responses.`), OutputMode: flags.String("output_mode", "merged", `output generation mode. By default, a single openapi.yaml is generated at the out folder. Use "source_relative' to generate a separate '[inputfile].openapi.yaml' next to each '[inputfile].proto'.`), SourceRoot: flags.String("source_root", "", `root directory of the source Protobuf files. This is used to add source information to the openapi.yaml file.`), - FlattenOneofs: flags.Bool("flatten_oneofs", false, `flatten oneof fields as regular properties on the parent message instead of generating a oneOf union`), + FlattenOneofs: flags.Bool("flatten_oneofs", false, `flatten oneof fields as regular properties on the parent message instead of generating a oneOf union`), + IncludeAllMethods: flags.Bool("include_all_methods", false, `include RPCs that lack google.api.http annotations by generating default POST paths using the pattern //`), } opts := protogen.Options{ diff --git a/cmd/protoc-gen-openapi/plugin_test.go b/cmd/protoc-gen-openapi/plugin_test.go index 0cda30e1..b953e0e1 100644 --- a/cmd/protoc-gen-openapi/plugin_test.go +++ b/cmd/protoc-gen-openapi/plugin_test.go @@ -311,6 +311,44 @@ func TestOpenAPIFlattenOneofs(t *testing.T) { } } +func TestOpenAPIIncludeAllMethods(t *testing.T) { + // Set PATH to include the protoc-gen-openapi plugin + os.Setenv("PATH", "../../:"+os.Getenv("PATH")) + + for _, tt := range openapiTests { + fixture := path.Join(tt.path, "openapi_include_all_methods.yaml") + if _, err := os.Stat(fixture); errors.Is(err, os.ErrNotExist) { + if !GENERATE_FIXTURES { + continue + } + } + t.Run(tt.name, func(t *testing.T) { + err := exec.Command("protoc", + "-I", "../../", + "-I", "../../third_party", + "-I", "examples", + path.Join(tt.path, tt.protofile), + "--plugin=protoc-gen-openapi=./protoc-gen-openapi", + "--openapi_out=naming=proto,include_all_methods=true:.").Run() + if err != nil { + t.Fatalf("protoc failed: %+v", err) + } + if GENERATE_FIXTURES { + err := CopyFixture(TEMP_FILE, fixture) + if err != nil { + t.Fatalf("Can't generate fixture: %+v", err) + } + } else { + err = exec.Command("diff", TEMP_FILE, fixture).Run() + if err != nil { + t.Fatalf("Diff failed: %+v", err) + } + } + os.Remove(TEMP_FILE) + }) + } +} + func TestOpenAPIDefaultResponse(t *testing.T) { // Set PATH to include the protoc-gen-openapi plugin os.Setenv("PATH", "../../:"+os.Getenv("PATH")) diff --git a/cmd/protoc-gen-openapi/protoc-gen-openapi b/cmd/protoc-gen-openapi/protoc-gen-openapi deleted file mode 100755 index d80380c3..00000000 Binary files a/cmd/protoc-gen-openapi/protoc-gen-openapi and /dev/null differ