diff --git a/cmd/protoc-gen-openapi/examples/tests/pathparams/message.proto b/cmd/protoc-gen-openapi/examples/tests/pathparams/message.proto index 7e427134..21d0a890 100644 --- a/cmd/protoc-gen-openapi/examples/tests/pathparams/message.proto +++ b/cmd/protoc-gen-openapi/examples/tests/pathparams/message.proto @@ -28,6 +28,18 @@ service Messaging { }; } + rpc GetMessage2(GetMessage2Request) returns (Message) { + option (google.api.http) = { + get : "/v2/messages/{message_id=*}" + }; + } + + rpc GetMessage3(GetMessage3Request) returns (Message) { + option (google.api.http) = { + get : "/v3/{name=user/messages/*}" + }; + } + rpc GetUserMessage(GetMessageRequest) returns (Message) { option (google.api.http) = { get : "/v1/users/{user_id}/messages/{message_id}" @@ -47,6 +59,15 @@ message GetMessageRequest { uint64 user_id = 2; } +message GetMessage2Request { + string message_id = 1; + uint64 user_id = 2; +} + +message GetMessage3Request { + string name = 1; +} + message Meta { string message_id = 1; uint64 user_id = 2; diff --git a/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi.yaml b/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi.yaml index f65b66d2..7a44cc12 100644 --- a/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi.yaml +++ b/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi.yaml @@ -92,6 +92,59 @@ paths: application/json: schema: $ref: '#/components/schemas/Status' + /v2/messages/{message_id}: + get: + tags: + - Messaging + operationId: Messaging_GetMessage2 + parameters: + - name: message_id + in: path + required: true + schema: + type: string + - name: user_id + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + /v3/user/messages/{message}: + get: + tags: + - Messaging + operationId: Messaging_GetMessage3 + parameters: + - name: message + in: path + description: The message id. + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' components: schemas: GoogleProtobufAny: diff --git a/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi_default_response.yaml b/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi_default_response.yaml index 470bab61..e616d0d0 100644 --- a/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi_default_response.yaml +++ b/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi_default_response.yaml @@ -92,6 +92,59 @@ paths: application/json: schema: $ref: '#/components/schemas/Status' + /v2/messages/{messageId}: + get: + tags: + - Messaging + operationId: Messaging_GetMessage2 + parameters: + - name: messageId + in: path + required: true + schema: + type: string + - name: userId + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + /v3/user/messages/{message}: + get: + tags: + - Messaging + operationId: Messaging_GetMessage3 + parameters: + - name: message + in: path + description: The message id. + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' components: schemas: GoogleProtobufAny: diff --git a/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi_fq_schema_naming.yaml b/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi_fq_schema_naming.yaml index f682585d..71d6a8e6 100644 --- a/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi_fq_schema_naming.yaml +++ b/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi_fq_schema_naming.yaml @@ -92,6 +92,59 @@ paths: application/json: schema: $ref: '#/components/schemas/google.rpc.Status' + /v2/messages/{messageId}: + get: + tags: + - Messaging + operationId: Messaging_GetMessage2 + parameters: + - name: messageId + in: path + required: true + schema: + type: string + - name: userId + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/tests.pathparams.message.v1.Message' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/google.rpc.Status' + /v3/user/messages/{message}: + get: + tags: + - Messaging + operationId: Messaging_GetMessage3 + parameters: + - name: message + in: path + description: The message id. + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/tests.pathparams.message.v1.Message' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/google.rpc.Status' components: schemas: google.protobuf.Any: diff --git a/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi_json.yaml b/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi_json.yaml index ff7d0a3d..8771eccd 100644 --- a/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi_json.yaml +++ b/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi_json.yaml @@ -92,6 +92,59 @@ paths: application/json: schema: $ref: '#/components/schemas/Status' + /v2/messages/{messageId}: + get: + tags: + - Messaging + operationId: Messaging_GetMessage2 + parameters: + - name: messageId + in: path + required: true + schema: + type: string + - name: userId + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + /v3/user/messages/{message}: + get: + tags: + - Messaging + operationId: Messaging_GetMessage3 + parameters: + - name: message + in: path + description: The message id. + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' components: schemas: GoogleProtobufAny: diff --git a/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi_string_enum.yaml b/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi_string_enum.yaml index 470bab61..e616d0d0 100644 --- a/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi_string_enum.yaml +++ b/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi_string_enum.yaml @@ -92,6 +92,59 @@ paths: application/json: schema: $ref: '#/components/schemas/Status' + /v2/messages/{messageId}: + get: + tags: + - Messaging + operationId: Messaging_GetMessage2 + parameters: + - name: messageId + in: path + required: true + schema: + type: string + - name: userId + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + /v3/user/messages/{message}: + get: + tags: + - Messaging + operationId: Messaging_GetMessage3 + parameters: + - name: message + in: path + description: The message id. + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' components: schemas: GoogleProtobufAny: diff --git a/cmd/protoc-gen-openapi/generator/generator.go b/cmd/protoc-gen-openapi/generator/generator.go index e548ab21..87d0c10d 100644 --- a/cmd/protoc-gen-openapi/generator/generator.go +++ b/cmd/protoc-gen-openapi/generator/generator.go @@ -456,88 +456,90 @@ func (g *OpenAPIv3Generator) buildOperationV3( // Find simple path parameters like {id} if allMatches := g.pathPattern.FindAllStringSubmatch(path, -1); allMatches != nil { for _, matches := range allMatches { + // {id} => id + name := matches[1] + // Add the value to the list of covered parameters. - coveredParameters = append(coveredParameters, matches[1]) - pathParameter := g.findAndFormatFieldName(matches[1], inputMessage) - path = strings.Replace(path, matches[1], pathParameter, 1) + coveredParameters = append(coveredParameters, name) + pathParameter := g.findAndFormatFieldName(name, inputMessage) + path = strings.Replace(path, name, pathParameter, 1) // Add the path parameters to the operation parameters. - var fieldSchema *v3.SchemaOrReference - - var fieldDescription string - field := g.findField(pathParameter, inputMessage) - if field != nil { - fieldSchema = g.reflect.schemaOrReferenceForField(field.Desc) - fieldDescription = g.filterCommentString(field.Comments.Leading) - } else { - // If field does not exist, it is safe to set it to string, as it is ignored downstream - fieldSchema = &v3.SchemaOrReference{ - Oneof: &v3.SchemaOrReference_Schema{ - Schema: &v3.Schema{ - Type: "string", - }, - }, + parameter := g.buildPathParameter(pathParameter, inputMessage) + parameters = append(parameters, parameter) + } + } + + // Find named path parameters like {user_id=*}, {user_id=/users/*}, {name=/user/messages/*}, {name=/} + if namedPathSegments := g.namedPathPattern.FindAllStringSubmatch(path, -1); namedPathSegments != nil { + for _, namedPathSegment := range namedPathSegments { + // {user_id=*} + segment := namedPathSegment[0] + // user_id + name := namedPathSegment[1] + // Add the "user_id=" "user_id" value to the list of covered parameters. + coveredParameters = append(coveredParameters, name) + + // * or /users/* ... etc + pathPattern := namedPathSegment[2] + // v1/{user_id=*} -> v1/{user_id} + if pathPattern == "*" { + pathParameter := g.findAndFormatFieldName(name, inputMessage) + path = strings.Replace(path, segment, fmt.Sprintf("{%s}", pathParameter), 1) + // Add the path parameters to the operation parameters. + parameter := g.buildPathParameter(pathParameter, inputMessage) + parameters = append(parameters, parameter) + } else if strings.Count(pathPattern, "*") > 0 { + // The starred path is assumed to be in the form "things/*/otherthings/*". + // We want to convert it to "things/{thingsId}/otherthings/{otherthingsId}". + // If it is in the form "firstthings/secondthings/*" + // We want "firstthings/secondthings/{secondthingId}" + + // Build a list of named path parameters. + namedPathParameters := make([]string, 0) + + // Convert the path from the starred form to use named path parameters. + parts := make([]string, 0) + lastSection := "" + for _, section := range strings.Split(pathPattern, "/") { + if section == "*" { + // Now we can determine the named path param + namedPathParameter := singular(lastSection) + namedPathParameter = g.findAndFormatFieldName(namedPathParameter, inputMessage) + parts = append(parts, "{"+namedPathParameter+"}") + namedPathParameters = append(namedPathParameters, namedPathParameter) + lastSection = "" + } else { + // It's a collection name, we know it should be on the path + parts = append(parts, section) + lastSection = section + } } - } - - parameters = append(parameters, - &v3.ParameterOrReference{ - Oneof: &v3.ParameterOrReference_Parameter{ - Parameter: &v3.Parameter{ - Name: pathParameter, - In: "path", - Description: fieldDescription, - Required: true, - Schema: fieldSchema, - }, - }, - }) - } - } - - // Find named path parameters like {name=shelves/*} - if matches := g.namedPathPattern.FindStringSubmatch(path); matches != nil { - // Build a list of named path parameters. - namedPathParameters := make([]string, 0) - - // Add the "name=" "name" value to the list of covered parameters. - coveredParameters = append(coveredParameters, matches[1]) - // Convert the path from the starred form to use named path parameters. - starredPath := matches[2] - parts := strings.Split(starredPath, "/") - // The starred path is assumed to be in the form "things/*/otherthings/*". - // We want to convert it to "things/{thingsId}/otherthings/{otherthingsId}". - for i := 0; i < len(parts)-1; i += 2 { - section := parts[i] - namedPathParameter := g.findAndFormatFieldName(section, inputMessage) - namedPathParameter = singular(namedPathParameter) - parts[i+1] = "{" + namedPathParameter + "}" - namedPathParameters = append(namedPathParameters, namedPathParameter) - } - // Rewrite the path to use the path parameters. - newPath := strings.Join(parts, "/") - path = strings.Replace(path, matches[0], newPath, 1) - - // Add the named path parameters to the operation parameters. - for _, namedPathParameter := range namedPathParameters { - parameters = append(parameters, - &v3.ParameterOrReference{ - Oneof: &v3.ParameterOrReference_Parameter{ - Parameter: &v3.Parameter{ - Name: namedPathParameter, - In: "path", - Required: true, - Description: "The " + namedPathParameter + " id.", - Schema: &v3.SchemaOrReference{ - Oneof: &v3.SchemaOrReference_Schema{ - Schema: &v3.Schema{ - Type: "string", + newPath := strings.Join(parts, "/") + path = strings.Replace(path, segment, newPath, 1) + + // Add the named path parameters to the operation parameters. + for _, namedPathParameter := range namedPathParameters { + parameters = append(parameters, + &v3.ParameterOrReference{ + Oneof: &v3.ParameterOrReference_Parameter{ + Parameter: &v3.Parameter{ + Name: namedPathParameter, + In: "path", + Required: true, + Description: "The " + namedPathParameter + " id.", + Schema: &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{ + Type: "string", + }, + }, }, }, }, - }, - }, - }) + }) + } + } } } @@ -668,6 +670,37 @@ func (g *OpenAPIv3Generator) buildOperationV3( return op, path } +func (g *OpenAPIv3Generator) buildPathParameter(pathParameter string, inputMessage *protogen.Message) *v3.ParameterOrReference { + var fieldSchema *v3.SchemaOrReference + var fieldDescription string + field := g.findField(pathParameter, inputMessage) + if field != nil { + fieldSchema = g.reflect.schemaOrReferenceForField(field.Desc) + fieldDescription = g.filterCommentString(field.Comments.Leading) + } else { + // If field does not exist, it is safe to set it to string, as it is ignored downstream + fieldSchema = &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{ + Type: "string", + }, + }, + } + } + parameter := &v3.ParameterOrReference{ + Oneof: &v3.ParameterOrReference_Parameter{ + Parameter: &v3.Parameter{ + Name: pathParameter, + In: "path", + Description: fieldDescription, + Required: true, + Schema: fieldSchema, + }, + }, + } + return parameter +} + // addOperationToDocumentV3 adds an operation to the specified path/method. func (g *OpenAPIv3Generator) addOperationToDocumentV3(d *v3.Document, op *v3.Operation, path string, methodName string) { var selectedPathItem *v3.NamedPathItem